diff --git a/dfx.json b/dfx.json index 7404066..47a8d8b 100644 --- a/dfx.json +++ b/dfx.json @@ -14,7 +14,7 @@ }, "defaults": { "build": { - "packtool": "mops sources", + "packtool": "vessel sources", "args": "" } } diff --git a/example/icrc1/main.mo b/example/icrc1/main.mo index 54238ac..a8b5ce7 100644 --- a/example/icrc1/main.mo +++ b/example/icrc1/main.mo @@ -59,15 +59,23 @@ shared ({ caller = _owner }) actor class Token( }; public shared ({ caller }) func icrc1_transfer(args : ICRC1.TransferArgs) : async ICRC1.TransferResult { - await ICRC1.transfer(token, args, caller); + await* ICRC1.transfer(token, args, caller); }; public shared ({ caller }) func mint(args : ICRC1.Mint) : async ICRC1.TransferResult { - await ICRC1.mint(token, args, caller); + await* ICRC1.mint(token, args, caller); }; public shared ({ caller }) func burn(args : ICRC1.BurnArgs) : async ICRC1.TransferResult { - await ICRC1.burn(token, args, caller); + await* ICRC1.burn(token, args, caller); + }; + + public shared ({ caller }) func approve(args : ICRC1.ApproveArgs) : async ICRC1.ApproveResult { + await* ICRC1.approve(token, args, caller); + }; + + public shared ({ caller }) func transfer_from(args : ICRC1.TransferFromArgs) : async ICRC1.TransferFromResult { + await* ICRC1.transfer_from(token, args, caller); }; // Functions from the rosetta icrc1 ledger @@ -77,7 +85,7 @@ shared ({ caller = _owner }) actor class Token( // Additional functions not included in the ICRC1 standard public shared func get_transaction(i : ICRC1.TxIndex) : async ?ICRC1.Transaction { - await ICRC1.get_transaction(token, i); + await* ICRC1.get_transaction(token, i); }; // Deposit cycles into this archive canister. diff --git a/makefile b/makefile index e08219f..90e9ce6 100644 --- a/makefile +++ b/makefile @@ -1,28 +1,27 @@ .PHONY: test docs actor-test -dfx-cache-install: +install-dfx-cache: dfx cache install -test: dfx-cache-install - $(shell dfx cache show)/moc -r $(shell mops sources) -wasi-system-api ./tests/**/**.Test.mo +test: install-dfx-cache + $(shell dfx cache show)/moc -r $(shell mops sources) -wasi-system-api ./tests/**/**.Test.mo --package base ~/.cache/dfinity/versions/0.13.1/base -no-warn: dfx-cache-install +no-warn: install-dfx-cache find src -type f -name '*.mo' -print0 | xargs -0 $(shell dfx cache show)/moc -r $(shell mops sources) -Werror -wasi-system-api docs: $(shell dfx cache show)/mo-doc $(shell dfx cache show)/mo-doc --format plain -actor-test: dfx-cache-install +actor-test: install-dfx-cache -dfx start --background - dfx deploy test + dfx deploy test --no-wallet --identity anonymous dfx ledger fabricate-cycles --canister test dfx canister call test run_tests ref-test: - -dfx start --background --clean - IDENTITY=$$(dfx identity whoami); \ - echo $$IDENTITY; \ - cat icrc1-default-args.txt | xargs -0 dfx deploy icrc1 --identity $$IDENTITY --no-wallet --argument ; \ + -dfx start --background + cat icrc1-default-args.txt | xargs -0 dfx deploy icrc1 --identity default --argument CANISTER=$$(dfx canister id icrc1); \ - cd Dfnity-ICRC1-Reference && cargo run --bin runner -- -u http://127.0.0.1:4943 -c $$CANISTER -s ~/.config/dfx/identity/$$IDENTITY/identity.pem \ No newline at end of file + USER=$$(dfx identity whoami); \ + cd Dfnity-ICRC1-Reference && cargo run --bin runner -- -u http://127.0.0.1:4943 -c $$CANISTER -s ~/.config/dfx/identity/$$USER/identity.pem \ No newline at end of file diff --git a/readme.md b/readme.md index 87f7a3e..7ba48c9 100644 --- a/readme.md +++ b/readme.md @@ -1,30 +1,19 @@ -# ICRC-1 Implementation +# ICRC-2 Implementation This repo contains the implementation of the -[ICRC-1](https://github.com/dfinity/ICRC-1) token standard. +[ICRC-2](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md). -## References and other implementations -- [demergent-labs/ICRC-1 (Typescript)](https://github.com/demergent-labs/ICRC-1) -- [Ledger ref in Motoko](https://github.com/dfinity/ledger-ref/blob/main/src/Ledger.mo) -- [ICRC1 Rosetta API](https://github.com/dfinity/ic/blob/master/rs/rosetta-api/icrc1/ledger) +## References +- [ICRC-1](https://github.com/NatLabs/icrc1) +- [ICRC1 test](https://github.com/NatLabs/icrc1/blob/main/example/icrc1/main.mo) -## Documentation -- [markdown](https://github.com/NatLabs/icrc1/blob/main/docs/ICRC1/lib.md#function-init) -- [web](https://natlabs.github.io/icrc1/ICRC1/lib.html#init) ## Getting Started -- Expose the ICRC-1 token functions from your canister +- Expose the ICRC-1 from your canister - Import the `icrc1` lib and expose them in an `actor` class. - Take a look at the [examples](./example/icrc1/main.mo) - -- Launch the basic token with all the standard functions for ICRC-1 - - Install the [mops](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/#/docs/install) package manager - - Replace the values enclosed in `< >` with your desired values and run in the terminal ```motoko - git clone https://github.com/NatLabs/icrc1 - cd icrc1 - mops install + git clone https://github.com/JingJingZhang9/I3-code.git dfx start --background --clean dfx deploy icrc1 --argument '( record { @@ -84,9 +73,12 @@ This repo contains the implementation of the > The fields for the `advanced_settings` record are documented [here](./docs/ICRC1/Types.md#type-advancedsettings) -## Textual Representation of the ICRC-1 Accounts -This library implements the [Textual Representation](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md#textual-representation-of-accounts) format for accounts defined by the standard. It utilizes this implementation to encode each account into a sequence of bytes for improved hashing and comparison. -To help with this process, the library provides functions in the [ICRC1/Account](./src/ICRC1/Account.mo) module for [encoding](./docs/ICRC1/Account.md#encode), [decoding](./docs/ICRC1/Account.md#decode), [converting from text](./docs/ICRC1/Account.md#fromText), and [converting to text](./docs/ICRC1/Account.md#toText). +## Textual Representation of the ICRC-2 +This library implements the https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md . + +ICRC-2 is an extension of the ICRC-1 standard. ICRC-2 provides a way for account owners to delegate token transfer authorization to a third party, allowing the third party to perform transfers on behalf of the owner: +icrc2_approve: Authorizes the spender to transfer a certain amount of tokens on behalf of the caller from the account { owner = caller; subaccount = from_subaccount }. The number of transfers the spender can initiate from the caller's account is unlimited as long as the total amounts and fees of these transfers do not exceed the allowance. +icrc2_transfer_from: Transfers a certain amount of tokens between two accounts. ## Tests @@ -95,18 +87,8 @@ To help with this process, the library provides functions in the [ICRC1/Account] - Run `make test` - Run `make actor-test` -#### [Dfinity's ICRC-1 Reference Tests](https://github.com/dfinity/ICRC-1/tree/main/test) -- Install Rust and Cargo via [rustup](https://rustup.rs/) - -``` - curl https://sh.rustup.rs -sSf | sh -``` -- Then run the `ref-test` command -``` - make ref-test -``` ## Funding -This library was initially incentivized by [ICDevs](https://icdevs.org/). You can view more about the bounty on the [forum](https://forum.dfinity.org/t/completed-icdevs-org-bounty-26-icrc-1-motoko-up-to-10k/14868/54) or [website](https://icdevs.org/bounties/2022/08/14/ICRC-1-Motoko.html). The bounty was funded by The ICDevs.org community and the DFINITY Foundation and the award was paid to [@NatLabs](https://github.com/NatLabs). If you use this library and gain value from it, please consider a [donation](https://icdevs.org/donations.html) to ICDevs. \ No newline at end of file +This library was initially incentivized by [ICDevs](https://icdevs.org/). You can view more about the bounty on the [forum](https://forum.dfinity.org/t/completed-icdevs-org-bounty-26-icrc-1-motoko-up-to-10k/14868/54) or [website](https://icdevs.org/bounties/2022/08/14/ICRC-1-Motoko.html). The bounty was funded by The ICDevs.org community and the DFINITY Foundation and the award was paid to [@NatLabs](https://github.com/NatLabs). If you use this library and gain value from it, please consider a [donation](https://icdevs.org/donations.html) to ICDevs. diff --git a/src/ICRC1/Account.mo b/src/ICRC1/Account.mo index 8a46ade..7514a72 100644 --- a/src/ICRC1/Account.mo +++ b/src/ICRC1/Account.mo @@ -14,10 +14,10 @@ import Result "mo:base/Result"; import Text "mo:base/Text"; import Time "mo:base/Time"; -import ArrayModule "mo:array/Array"; -import Itertools "mo:itertools/Iter"; -import StableBuffer "mo:StableBuffer/StableBuffer"; -import STMap "mo:StableTrieMap"; +import ArrayModule "array/Array"; +import Itertools "itertools/Iter"; +import StableBuffer "stable/StableBuffer"; +import STMap "stable/StableTrieMap"; import T "Types"; diff --git a/src/ICRC1/Canisters/Archive.mo b/src/ICRC1/Canisters/Archive.mo index 74f7022..589dbf7 100644 --- a/src/ICRC1/Canisters/Archive.mo +++ b/src/ICRC1/Canisters/Archive.mo @@ -12,8 +12,8 @@ import Result "mo:base/Result"; import ExperimentalCycles "mo:base/ExperimentalCycles"; import ExperimentalStableMemory "mo:base/ExperimentalStableMemory"; -import Itertools "mo:itertools/Iter"; -import StableTrieMap "mo:StableTrieMap"; +import Itertools "../itertools/Iter"; +import StableTrieMap "../stable/StableTrieMap"; import U "../Utils"; import T "../Types"; diff --git a/src/ICRC1/Canisters/Token.mo b/src/ICRC1/Canisters/Token.mo index bef65a9..02a9399 100644 --- a/src/ICRC1/Canisters/Token.mo +++ b/src/ICRC1/Canisters/Token.mo @@ -5,13 +5,13 @@ import Time "mo:base/Time"; import ExperimentalCycles "mo:base/ExperimentalCycles"; -import SB "mo:StableBuffer/StableBuffer"; +import SB "../stable/StableBuffer"; import ICRC1 ".."; import Archive "Archive"; shared ({ caller = _owner }) actor class Token( - init_args : ICRC1.TokenInitArgs, + init_args : ICRC1.TokenInitArgs ) : async ICRC1.FullInterface { let icrc1_args : ICRC1.InitArgs = { @@ -75,6 +75,18 @@ shared ({ caller = _owner }) actor class Token( await* ICRC1.burn(token, args, caller); }; + public shared ({ caller }) func icrc2_approve(args : ICRC1.ApproveArgs) : async ICRC1.ApproveResult { + await* ICRC1.approve(token, args, caller); + }; + + public shared ({ caller }) func icrc2_transfer_from(args : ICRC1.TransferFromArgs) : async ICRC1.TransferFromResult { + await* ICRC1.transfer_from(token, args, caller); + }; + + public shared query func icrc2_allowance(args : ICRC1.AllowanceArgs) : async ICRC1.Allowance { + ICRC1.get_allowance_of(token, args.account, args.spender); + }; + // Functions for integration with the rosetta standard public shared query func get_transactions(req : ICRC1.GetTransactionsRequest) : async ICRC1.GetTransactionsResponse { ICRC1.get_transactions(token, req); diff --git a/src/ICRC1/Transfer.mo b/src/ICRC1/Transfer.mo index bea9e28..cf59fd7 100644 --- a/src/ICRC1/Transfer.mo +++ b/src/ICRC1/Transfer.mo @@ -11,9 +11,9 @@ import Principal "mo:base/Principal"; import Result "mo:base/Result"; import Time "mo:base/Time"; -import Itertools "mo:itertools/Iter"; -import StableBuffer "mo:StableBuffer/StableBuffer"; -import STMap "mo:StableTrieMap"; +import Itertools "itertools/Iter"; +import StableBuffer "stable/StableBuffer"; +import STMap "stable/StableTrieMap"; import Account "Account"; @@ -159,7 +159,7 @@ module { #GenericError({ error_code = 0; message = "The sender cannot have the same account as the recipient."; - }), + }) ); }; @@ -167,8 +167,8 @@ module { return #err( #GenericError({ error_code = 0; - message = "Invalid account entered for sender. " # debug_show(tx_req.from); - }), + message = "Invalid account entered for sender. " # debug_show (tx_req.from); + }) ); }; @@ -176,8 +176,8 @@ module { return #err( #GenericError({ error_code = 0; - message = "Invalid account entered for recipient " # debug_show(tx_req.to); - }), + message = "Invalid account entered for recipient " # debug_show (tx_req.to); + }) ); }; @@ -186,7 +186,7 @@ module { #GenericError({ error_code = 0; message = "Memo must not be more than 32 bytes"; - }), + }) ); }; @@ -195,7 +195,7 @@ module { #GenericError({ error_code = 0; message = "Amount must be greater than 0"; - }), + }) ); }; @@ -205,7 +205,7 @@ module { return #err( #BadFee { expected_fee = token._fee; - }, + } ); }; @@ -214,8 +214,8 @@ module { tx_req.encoded.from, ); - if (tx_req.amount + token._fee > balance) { - return #err(#InsufficientFunds { balance }); + if (tx_req.amount > balance + token._fee) { + return #err(#InsufficientFunds { balance = balance }); }; }; @@ -227,14 +227,14 @@ module { #GenericError({ error_code = 0; message = "Cannot mint more than " # Nat.toText(remaining_tokens) # " tokens"; - }), + }) ); }; }; case (#burn) { if (tx_req.to == token.minting_account and tx_req.amount < token.min_burn_amount) { return #err( - #BadBurn { min_burn_amount = token.min_burn_amount }, + #BadBurn { min_burn_amount = token.min_burn_amount } ); }; @@ -244,7 +244,7 @@ module { ); if (balance < tx_req.amount) { - return #err(#InsufficientFunds { balance }); + return #err(#InsufficientFunds { balance = balance }); }; }; }; @@ -261,7 +261,7 @@ module { return #err( #CreatedInFuture { ledger_time = Nat64.fromNat(Int.abs(Time.now())); - }, + } ); }; @@ -270,7 +270,7 @@ module { return #err( #Duplicate { duplicate_of = tx_index; - }, + } ); }; case (_) {}; @@ -281,4 +281,169 @@ module { #ok(); }; + public func validate_approve_request( + token : T.TokenData, + tx_req : T.ApproveTxRequest, + ) : Result.Result<(), T.ApproveError> { + // TODO: The spender's allowance for the { owner = caller; subaccount = from_subaccount } + // increases by the amount (or decreases if the amount is negative). If the total allowance + // is negative, the ledger MUST reset the allowance to zero. + if (tx_req.from.owner == tx_req.spender.owner) { + return #err( + #GenericError({ + error_code = 0; + message = "The approve Principal cannot be the same Principal as the approver."; + }) + ); + }; + + if (not Account.validate(tx_req.from)) { + return #err( + #GenericError({ + error_code = 0; + message = "Invalid account entered for sender. " # debug_show (tx_req.from); + }) + ); + }; + + if (not Account.validate(tx_req.spender)) { + return #err( + #GenericError({ + error_code = 0; + message = "Invalid account entered for recipient " # debug_show (tx_req.spender); + }) + ); + }; + + if (not validate_memo(tx_req.memo)) { + return #err( + #GenericError({ + error_code = 0; + message = "Memo must not be more than 32 bytes"; + }) + ); + }; + // seems it's not need to let amount < 0, cause type Nat is >= 0 always + if (tx_req.amount < 0) { + return #err( + #GenericError({ + error_code = 0; + message = "Amount must be greater than or euqal 0"; + }) + ); + }; + + switch (tx_req.kind) { + case (#approve) { + if (not validate_fee(token, tx_req.fee)) { + return #err( + #BadFee { + expected_fee = token._fee; + } + ); + }; + + let balance : T.Balance = Utils.get_balance( + token.accounts, + tx_req.encoded.from, + ); + + if (tx_req.amount > balance + token._fee) { + return #err(#InsufficientFunds { balance = balance }); + }; + }; + }; + + switch (tx_req.created_at_time) { + case (null) {}; + case (?created_at_time) { + + if (is_too_old(token, created_at_time)) { + return #err(#TooOld); + }; + + if (is_in_future(token, created_at_time)) { + return #err( + #CreatedInFuture { + ledger_time = Nat64.fromNat(Int.abs(Time.now())); + } + ); + }; + }; + }; + + #ok(); + }; + + /// Checks if a transfer request is valid + public func validate_transfer_from_request( + token : T.TokenData, + tx_req : T.TransactionFromRequest, + ) : Result.Result<(), T.TransferFromError> { + + let encoded_caller_account = Account.encode({ + owner = tx_req.caller; + subaccount = null; + }); + + let account_pair = Utils.gen_account_from_two_account(tx_req.encoded.from, encoded_caller_account); + // check allowance + let allowance_pair : T.Allowance = Utils.get_allowance( + token.approve_accounts, + account_pair, + ); + + if (tx_req.amount > allowance_pair.allowance + token._fee) { + return #err(#InsufficientAllowance { allowance = allowance_pair.allowance }); + }; + + // check balance + let balance : T.Balance = Utils.get_balance( + token.accounts, + tx_req.encoded.from, + ); + + if (tx_req.amount > balance + token._fee) { + return #err(#InsufficientFunds { balance = balance }); + }; + + // check expire time + // TODO: let expire time be a new type of error + switch (allowance_pair.expires_at) { + case (null) {}; + case (?expires_at_time) { + switch (tx_req.created_at_time) { + case (null) {}; + case (?created_at_time) { + if (created_at_time > expires_at_time) { + return #err(#InsufficientFunds { balance = 0 }); + }; + }; + }; + }; + }; + + switch (tx_req.created_at_time) { + case (null) {}; + case (?created_at_time) { + + if (is_too_old(token, created_at_time)) { + return #err(#TooOld); + }; + + if (is_in_future(token, created_at_time)) { + return #err( + #CreatedInFuture { + ledger_time = Nat64.fromNat(Int.abs(Time.now())); + } + ); + }; + + // check deduplicate is in transfer validate_request + }; + }; + + #ok(); + }; + }; diff --git a/src/ICRC1/Types.mo b/src/ICRC1/Types.mo index b85e096..dd39451 100644 --- a/src/ICRC1/Types.mo +++ b/src/ICRC1/Types.mo @@ -3,8 +3,8 @@ import List "mo:base/List"; import Time "mo:base/Time"; import Result "mo:base/Result"; -import STMap "mo:StableTrieMap"; -import StableBuffer "mo:StableBuffer/StableBuffer"; +import STMap "./stable/StableTrieMap"; +import StableBuffer "./stable/StableBuffer"; module { @@ -13,6 +13,7 @@ module { public type BlockIndex = Nat; public type Subaccount = Blob; public type Balance = Nat; + public type StableBuffer = StableBuffer.StableBuffer; public type StableTrieMap = STMap.StableTrieMap; @@ -37,12 +38,38 @@ module { public type MetaDatum = (Text, Value); public type MetaData = [MetaDatum]; + public type ApproveError = { + #BadFee : { expected_fee : Nat }; + // The caller does not have enough funds to pay the approval fee. + #InsufficientFunds : { balance : Nat }; + // The approval request expired before the ledger had a chance to apply it. + #Expired : { ledger_time : Nat64 }; + #TooOld; + #CreatedInFuture : { ledger_time : Nat64 }; + #Duplicate : { duplicate_of : Nat }; + #TemporarilyUnavailable; + #GenericError : { error_code : Nat; message : Text }; + }; + + public type ApproveResult = { + #Ok : TxIndex; + #Err : ApproveError; + }; + public type TxKind = { #mint; #burn; #transfer; }; + public type ICRC2TxKind = { + #transfer_from; + }; + + public type OperationKind = { + #approve; + }; + public type Mint = { to : Account; amount : Balance; @@ -77,6 +104,19 @@ module { created_at_time : ?Nat64; }; + /// Arguments for a transfer from operation + public type TransferFromArgs = { + from_subaccount : Account; + to : Account; + amount : Balance; + fee : ?Balance; + memo : ?Blob; + + /// The time at which the transaction was created. + /// If this is set, the canister will check for duplicate transactions and reject them. + created_at_time : ?Nat64; + }; + public type Transfer = { from : Account; to : Account; @@ -86,6 +126,37 @@ module { created_at_time : ?Nat64; }; + public type ApproveArgs = { + from_subaccount : ?Blob; + spender : Principal; + amount : Nat; + expires_at : ?Nat64; + fee : ?Nat; + memo : ?Blob; + created_at_time : ?Nat64; + }; + + public type Approve = { + kind : OperationKind; + from : Account; + spender : Account; + amount : Balance; + expires_at : ?Nat64; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + }; + + public type AllowanceArgs = { + account : Account; + spender : Principal; + }; + + public type Allowance = { + allowance : Nat; + expires_at : ?Nat64; + }; + /// Internal representation of a transaction request public type TransactionRequest = { kind : TxKind; @@ -101,6 +172,37 @@ module { }; }; + /// Internal representation of a transaction request + public type TransactionFromRequest = { + kind : ICRC2TxKind; + from : Account; + to : Account; + caller : Principal; + amount : Balance; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + encoded : { + from : EncodedAccount; + to : EncodedAccount; + }; + }; + + public type ApproveTxRequest = { + kind : OperationKind; + from : Account; + spender : Account; + amount : Balance; + expires_at : ?Nat64; + fee : ?Balance; + memo : ?Blob; + created_at_time : ?Nat64; + encoded : { + from : EncodedAccount; + to : EncodedAccount; + }; + }; + public type Transaction = { kind : Text; mint : ?Mint; @@ -110,6 +212,13 @@ module { timestamp : Timestamp; }; + // apart from icrc1 + public type ApproveTransaction = { + approve : Approve; + index : TxIndex; + timestamp : Timestamp; + }; + public type TimeError = { #TooOld; #CreatedInFuture : { ledger_time : Timestamp }; @@ -123,12 +232,29 @@ module { #TemporarilyUnavailable; #GenericError : { error_code : Nat; message : Text }; }; - + public type TransferResult = { #Ok : TxIndex; #Err : TransferError; }; + public type TransferFromError = { + #BadFee : { expected_fee : Balance }; + #BadBurn : { min_burn_amount : Balance }; + #InsufficientFunds : { balance : Balance }; + #InsufficientAllowance : { allowance : Balance }; + #TooOld; + #CreatedInFuture : { ledger_time : Timestamp }; + #Duplicate : { duplicate_of : TxIndex }; + #TemporarilyUnavailable; + #GenericError : { error_code : Nat; message : Text }; + }; + + public type TransferFromResult = { + #Ok : TxIndex; + #Err : TransferFromError; + }; + /// Interface for the ICRC token canister public type TokenInterface = actor { @@ -198,7 +324,7 @@ module { min_burn_amount : Balance; /// optional settings for the icrc1 canister - advanced_settings: ?AdvancedSettings + advanced_settings : ?AdvancedSettings; }; /// [InitArgs](#type.InitArgs) with optional fields for initializing a token canister @@ -214,19 +340,21 @@ module { /// optional value that defaults to the caller if not provided minting_account : ?Account; - advanced_settings: ?AdvancedSettings; + advanced_settings : ?AdvancedSettings; }; /// Additional settings for the [InitArgs](#type.InitArgs) type during initialization of an icrc1 token canister public type AdvancedSettings = { /// needed if a token ever needs to be migrated to a new canister - burned_tokens : Balance; + burned_tokens : Balance; transaction_window : Timestamp; permitted_drift : Timestamp; }; public type AccountBalances = StableTrieMap; + public type ApproveBalances = StableTrieMap; + /// The details of the archive canister public type ArchiveData = { /// The reference to the archive canister @@ -266,6 +394,9 @@ module { /// The balances of all accounts accounts : AccountBalances; + /// The balances of all appro + approve_accounts : ApproveBalances; + /// The metadata for the token metadata : StableBuffer; @@ -285,6 +416,8 @@ module { /// Only the last 2000 transactions are stored before being archived. transactions : StableBuffer; + approve_transactions : StableBuffer; + /// The record that stores the details to the archive canister and number of transactions stored in it archive : ArchiveData; }; @@ -297,7 +430,7 @@ module { }; public type TransactionRange = { - transactions: [Transaction]; + transactions : [Transaction]; }; public type QueryArchiveFn = shared query (GetTransactionsRequest) -> async TransactionRange; @@ -309,7 +442,7 @@ module { length : Nat; /// The callback function to query the archive canister - callback: QueryArchiveFn; + callback : QueryArchiveFn; }; public type GetTransactionsResponse = { @@ -326,7 +459,7 @@ module { archived_transactions : [ArchivedTransaction]; }; - /// Functions supported by the rosetta + /// Functions supported by the rosetta public type RosettaInterface = actor { get_transactions : shared query (GetTransactionsRequest) -> async GetTransactionsResponse; }; diff --git a/src/ICRC1/Utils.mo b/src/ICRC1/Utils.mo index 786f335..3c5137a 100644 --- a/src/ICRC1/Utils.mo +++ b/src/ICRC1/Utils.mo @@ -12,11 +12,12 @@ import Option "mo:base/Option"; import Principal "mo:base/Principal"; import Result "mo:base/Result"; import Time "mo:base/Time"; +import Buffer "mo:base/Buffer"; -import ArrayModule "mo:array/Array"; -import Itertools "mo:itertools/Iter"; -import STMap "mo:StableTrieMap"; -import StableBuffer "mo:StableBuffer/StableBuffer"; +import ArrayModule "array/Array"; +import Itertools "itertools/Iter"; +import STMap "stable/StableTrieMap"; +import StableBuffer "stable/StableBuffer"; import Account "Account"; import T "Types"; @@ -38,11 +39,16 @@ module { url = "https://github.com/dfinity/ICRC-1"; }; + public let icrc2_standard : T.SupportedStandard = { + name = "ICRC-2"; + url = "https://github.com/dfinity/ICRC-1/blob/roman-icrc2-cap/standards/ICRC-2"; + }; + // Creates a Stable Buffer with the default supported standards and returns it. public func init_standards() : StableBuffer.StableBuffer { let standards = SB.initPresized(4); SB.add(standards, default_standard); - + SB.add(standards, icrc2_standard); standards; }; @@ -50,7 +56,7 @@ module { // not specify it. public func default_subaccount() : T.Subaccount { Blob.fromArray( - Array.tabulate(32, func(_ : Nat) : Nat8 { 0 }), + Array.tabulate(32, func(_ : Nat) : Nat8 { 0 }) ); }; @@ -84,9 +90,9 @@ module { public func create_transfer_req( args : T.TransferArgs, owner : Principal, - tx_kind: T.TxKind, + tx_kind : T.TxKind, ) : T.TransactionRequest { - + let from = { owner; subaccount = args.from_subaccount; @@ -124,6 +130,62 @@ module { }; }; + // Formats the different operation arguements into + // a `TransactionFromRequest`, an new internal type to access fields easier for icrc2. + public func create_transfer_from_req( + args : T.TransferFromArgs, + caller : Principal, + tx_kind : T.ICRC2TxKind, + ) : T.TransactionFromRequest { + + let encoded = { + from = Account.encode(args.from_subaccount); + to = Account.encode(args.to); + }; + + { + args with kind = #transfer_from; + from = args.from_subaccount; + caller; + encoded; + }; + }; + + public func create_approve_req( + args : T.ApproveArgs, + owner : Principal, + tx_kind : T.OperationKind, + ) : T.ApproveTxRequest { + + let from = { + owner; + subaccount = args.from_subaccount; + }; + + let to = { + owner = args.spender; + subaccount = null; + }; + + let encoded = { + from = Account.encode(from); + to = Account.encode(to); + }; + + { + kind = tx_kind; + from = from; + spender = to; + amount = args.amount; + expires_at = args.expires_at; + fee = args.fee; + memo = args.memo; + created_at_time = args.created_at_time; + // args with kind = #approve; + encoded; + }; + }; + // Transforms the transaction kind from `variant` to `Text` public func kind_to_text(kind : T.TxKind) : Text { switch (kind) { @@ -134,7 +196,7 @@ module { }; // Formats the tx request into a finalised transaction - public func req_to_tx(tx_req : T.TransactionRequest, index: Nat) : T.Transaction { + public func req_to_tx(tx_req : T.TransactionRequest, index : Nat) : T.Transaction { { kind = kind_to_text(tx_req.kind); @@ -152,7 +214,17 @@ module { case (#transfer) { ?tx_req }; case (_) null; }; - + + index; + timestamp = Nat64.fromNat(Int.abs(Time.now())); + }; + }; + + public func approve_req_to_tx(tx_req : T.ApproveTxRequest, index : Nat) : T.ApproveTransaction { + + { + kind = "APPROVE"; + approve = tx_req; index; timestamp = Nat64.fromNat(Int.abs(Time.now())); }; @@ -179,6 +251,28 @@ module { }; }; + /// Retrieves the balance of an account + public func get_allowance(accounts : T.ApproveBalances, encoded_account : T.EncodedAccount) : T.Allowance { + let res = STMap.get( + accounts, + Blob.equal, + Blob.hash, + encoded_account, + ); + + switch (res) { + case (?balance) { + balance; + }; + case (_) { + { + allowance = 0; + expires_at = null; + }; + }; + }; + }; + /// Updates the balance of an account public func update_balance( accounts : T.AccountBalances, @@ -199,12 +293,51 @@ module { }; }; + public func update_approve_balance( + accounts : T.ApproveBalances, + encoded_account : T.EncodedAccount, + update_allowance : (T.Allowance) -> T.Allowance, + change_expires_at : Bool, + ) { + let prev_balance = get_allowance(accounts, encoded_account); + let updated_balance = update_allowance(prev_balance); + + let prev_allowance = prev_balance.allowance; + let prev_expires_at = prev_balance.expires_at; + + let updated_allowance = updated_balance.allowance; + let updated_expires_at = updated_balance.expires_at; + + // update expire time + var expires_at : ?Nat64 = null; + if (change_expires_at) { + expires_at := updated_expires_at; + } else { + expires_at := prev_expires_at; + }; + + let insert_balance = { + allowance = updated_allowance; + expires_at; + }; + + if (updated_balance != prev_balance) { + STMap.put( + accounts, + Blob.equal, + Blob.hash, + encoded_account, + insert_balance, + ); + }; + }; + // Transfers tokens from the sender to the // recipient in the tx request public func transfer_balance( token : T.TokenData, tx_req : T.TransactionRequest, - ) { + ) { let { encoded; amount } = tx_req; update_balance( @@ -224,6 +357,34 @@ module { ); }; + public func approve( + token : T.TokenData, + tx_req : T.ApproveTxRequest, + ) { + let { encoded; amount; expires_at } = tx_req; + + update_approve_balance( + token.approve_accounts, + gen_account_from_two_account(encoded.from, encoded.to), + func(balance) { + { + allowance = amount; + expires_at = expires_at; + }; + }, + true, + ); + }; + + /// create an account from Approver as `from` account and Spender as `to` account + public func gen_account_from_two_account(from : T.EncodedAccount, to : T.EncodedAccount) : T.EncodedAccount { + let from_buffer : Buffer.Buffer = Buffer.fromArray(Blob.toArray(from)); + let to_buffer : Buffer.Buffer = Buffer.fromArray(Blob.toArray(to)); + from_buffer.append(to_buffer); + let final_array = Buffer.toArray(from_buffer); + Blob.fromArray(final_array); + }; + public func mint_balance( token : T.TokenData, encoded_account : T.EncodedAccount, @@ -256,6 +417,25 @@ module { token._burned_tokens += amount; }; + public func decrease_allowance( + token : T.TokenData, + encoded_account : T.EncodedAccount, + amount : T.Balance, + ) { + update_approve_balance( + token.approve_accounts, + encoded_account, + func(balance) { + { + allowance = balance.allowance - amount; + expires_at = null; + }; + }, + false, + ); + + }; + // Stable Buffer Module with some additional functions public let SB = { StableBuffer with slice = func(buffer : T.StableBuffer, start : Nat, end : Nat) : [A] { diff --git a/src/ICRC1/array/Array.mo b/src/ICRC1/array/Array.mo new file mode 100644 index 0000000..9f9879e --- /dev/null +++ b/src/ICRC1/array/Array.mo @@ -0,0 +1,44 @@ +import Prim "mo:⛔"; + +module { + // Checks whether an array contains a given value. + public func contains(xs : [T], y : T, equal : (T, T) -> Bool) : Bool { + for (x in xs.vals()) { + if (equal(x, y)) return true; + }; false; + }; + + // Drops the first 'n' elements of an array, returns the remainder of that array. + public func drop(xs : [T], n : Nat) : [T] { + let xS = xs.size(); + if (xS <= n) return []; + let s = xS - n : Nat; + Prim.Array_tabulate(s, func (i : Nat) : T { xs[n + i]; }); + }; + + // Slices out [i-j[ elements of an array. + public func slice(xs : [T], i : Nat, j : Nat) : [T] { + if (j < i) return []; + if (j == i) return [xs[i]]; + Prim.Array_tabulate(j - i, func (k : Nat) : T { xs[i+k]; }); + }; + + // Splits an array in two parts, based on the given element index. + public func split(xs : [T], n : Nat) : ([T], [T]) { + if (n == 0) { return (xs, [] : [T]); }; + let xS = xs.size(); + if (xS <= n) { return ([] : [T], xs); }; + let s = xS - n : Nat; + ( + Prim.Array_tabulate(n, func (i : Nat) : T { xs[i]; }), + Prim.Array_tabulate(s, func (i : Nat) : T { xs[n + i]; }) + ); + }; + + // Returns the first 'n' elements of an array. + public func take(xs : [T], n : Nat) : [T] { + let xS = xs.size(); + if (xS <= n) return xs; + Prim.Array_tabulate(n, func (i : Nat) : T { xs[i]; }); + }; +}; \ No newline at end of file diff --git a/src/ICRC1/itertools/Deiter.mo b/src/ICRC1/itertools/Deiter.mo new file mode 100644 index 0000000..f6a98ec --- /dev/null +++ b/src/ICRC1/itertools/Deiter.mo @@ -0,0 +1,230 @@ +/// Double Ended Iterator +/// +/// This type of iterator allows for both forward and backward iteration +/// Double Ended Iterators are useful for iterating over data structures in reverse without allocating extra space for the reverse iteration. +/// +/// The `Deiter` type is an extension of the `Iter` type built in Motoko +/// so it is compatible with all the function defined for the `Iter` type. +/// +/// +/// The `Deiter` is intended to be used with functions for the `Iter` type to avoid rewriting similar functions for both types. +/// +/// - An example reversing a list of integers and breaking them into chunks of size `n`: +/// +/// ```motoko +/// +/// import Itertools "mo:itertools/Iter"; +/// import Deiter "mo:itertools/Deiter"; +/// +/// let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +/// +/// // create a double ended iterator from an array +/// let deiter = Deiter.fromArray(arr); +/// +/// // reverse double ended iterator +/// let revDeiter = Deiter.reverse(deiter); +/// +/// // Double Ended Iter gets typecasted to an Iter typw +/// let chunks = Itertools.chunks(revDeiter, 3); +/// +/// assert chunks.next() == ?[10, 9, 8]; +/// assert chunks.next() == ?[7, 6, 5]; +/// assert chunks.next() == ?[4, 3, 2]; +/// assert chunks.next() == ?[1]; +/// assert chunks.next() == null; +/// +/// ``` + +import Array "mo:base/Array"; +import Iter "mo:base/Iter"; +import Int "mo:base/Int"; +import List "mo:base/List"; +import Deque "mo:base/Deque"; + +module { + /// Double Ended Iterator Type + public type Deiter = Iter.Iter and { + next_back : () -> ?T; + }; + + /// Returns a Double Ended Iterator over a range of natural, `Nat` numbers from [start, end) + public func range(start : Nat, end : Nat) : Deiter { + let intIter = intRange(start, end); + + func optIntToNat(optInt : ?Int) : ?Nat { + switch (optInt) { + case (null) null; + case (?val) ?Int.abs(val); + }; + }; + + return object { + public func next() : ?Nat { + optIntToNat(intIter.next()); + }; + public func next_back() : ?Nat { + optIntToNat(intIter.next_back()); + }; + }; + }; + + /// Returns a Double Ended Iterator over a range of integers (`Int`) from [start, end) + public func intRange(start : Int, end : Int) : Deiter { + var i = start; + var j = end; + + return object { + public func next() : ?Int { + if (i < end and i < j) { + let tmp = i; + i += 1; + return ?tmp; + } else { + return null; + }; + }; + + public func next_back() : ?Int { + if (j > start and j > i) { + j -= 1; + return ?j; + } else { + return null; + }; + }; + }; + }; + + /// @deprecated in favor of `reverse` + public func rev(deiter : Deiter) : Deiter { + reverse(deiter); + }; + + /// Returns an iterator that iterates over the elements in reverse order. + /// #### Example + /// + /// ```motoko + /// + /// let arr = [1, 2, 3]; + /// let deiter = Deiter.fromArray(arr); + /// + /// assert deiter.next() == ?1; + /// assert deiter.next() == ?2; + /// assert deiter.next() == ?3; + /// assert deiter.next() == null; + /// + /// let deiter2 = Deiter.fromArray(arr); + /// let revIter = Deiter.reverse(deiter2); + /// + /// assert revIter.next() == ?3; + /// assert revIter.next() == ?2; + /// assert revIter.next() == ?1; + /// assert revIter.next() == null; + /// + /// ``` + public func reverse(deiter : Deiter) : Deiter { + return object { + public func next() : ?T { + deiter.next_back(); + }; + public func next_back() : ?T { + deiter.next(); + }; + }; + }; + + /// Creates an iterator for the elements of an array. + /// + /// #### Example + /// + /// ```motoko + /// + /// let arr = [1, 2, 3]; + /// let deiter = Deiter.fromArray(arr); + /// + /// assert deiter.next() == ?1; + /// assert deiter.next_back() == ?3; + /// assert deiter.next_back() == ?2; + /// assert deiter.next_back() == null; + /// assert deiter.next() == null; + /// + /// ``` + public func fromArray(array : [T]) : Deiter { + var left = 0; + var right = array.size(); + + return { + next = func() : ?T { + if (left < right) { + left += 1; + ?array[left - 1]; + } else { + null; + }; + }; + next_back = func() : ?T { + if (left < right) { + right -= 1; + ?array[right]; + } else { + null; + }; + }; + }; + }; + + public func toArray(deiter : Deiter) : [T] { + Iter.toArray(deiter); + }; + + public func fromArrayMut(array : [var T]) : Deiter { + fromArray(Array.freeze(array)); + }; + + public func toArrayMut(deiter : Deiter) : [var T] { + Iter.toArrayMut(deiter); + }; + + /// Type Conversion from Deiter to Iter + public func toIter(iter : Iter.Iter) : Iter.Iter { + iter; + }; + + /// Returns an iterator for a deque. + public func fromDeque(deque : Deque.Deque) : Deiter { + + var deq = deque; + return object { + public func next() : ?T { + switch (Deque.popFront(deq)) { + case (?(val, next)) { + deq := next; + ?val; + }; + case (null) null; + }; + }; + + public func next_back() : ?T { + switch (Deque.popBack(deq)) { + case (?(prev, val)) { + deq := prev; + ?val; + }; + case (null) null; + }; + }; + }; + }; + + /// Converts an iterator to a deque. + public func toDeque(deiter : Deiter) : Deque.Deque { + var dq = Deque.empty(); + + for (item in deiter) { + dq := Deque.pushBack(dq, item); + }; + + dq; + }; +}; diff --git a/src/ICRC1/itertools/Iter.mo b/src/ICRC1/itertools/Iter.mo new file mode 100644 index 0000000..0eb70fd --- /dev/null +++ b/src/ICRC1/itertools/Iter.mo @@ -0,0 +1,3036 @@ +/// Main module with utility functions for working efficiently with iterators. +/// +/// See the [`Iter`](https://internetcomputer.org/docs/current/references/motoko-ref/iter#iter-1) module from the base lib for more information on the `Iter` type. +/// +/// +/// ## Getting started +/// +/// To get started, you'll need to import the `Iter` module from both the base library and this one. +/// +/// ```motoko +/// import Iter "mo:base/Iter"; +/// import Itertools "mo:itertools/Iter"; +/// ``` +/// +/// Converting data types to iterators is the next step. +/// - Array +/// - `[1, 2, 3, 4, 5].vals()` +/// - `Iter.fromArray([1, 2, 3, 4, 5])` +/// +/// - List +/// - `Iter.fromList(list)` +/// +/// - Text +/// - `"Hello, world!".chars()` +/// - `Text.split("a,b,c", #char ',')` +/// +/// - Buffer +/// - `Buffer.toArray(buffer).vals()` +/// +/// - [HashMap](https://internetcomputer.org/docs/current/references/motoko-ref/hashmap#hashmap-1) +/// - `map.entries()` +/// +/// For conversion of other data types to iterators, you can look in the [base library](https://internetcomputer.org/docs/current/references/motoko-ref/array) for the specific data type's documentation. +/// +/// +/// Here are some examples of using the functions in this library to create simple and +/// efficient iterators for solving different problems: +/// +/// - An example, using `range` and `sum` to find the sum of values from 1 to 25: +/// +/// ```motoko +/// let range = Itertools.range(1, 25 + 1); +/// let sum = Itertools.sum(range, Nat.add); +/// +/// assert sum == ?325; +/// ``` +/// +/// +/// - An example, using multiple functions to retrieve the indices of all even numbers in an array: +/// +/// ```motoko +/// let vals = [1, 2, 3, 4, 5, 6].vals(); +/// let iterWithIndices = Itertools.enumerate(vals); +/// +/// let isEven = func ( x : (Int, Int)) : Bool { x.1 % 2 == 0 }; +/// let mapIndex = func (x : (Int, Int)) : Int { x.0 }; +/// let evenIndices = Itertools.mapFilter(iterWithIndices, isEven, mapIndex); +/// +/// assert Iter.toArray(evenIndices) == [1, 3, 5]; +/// ``` +/// +/// +/// - An example to find the difference between consecutive elements in an array: +/// +/// ```motoko +/// let vals = [5, 3, 3, 7, 8, 10].vals(); +/// +/// let tuples = Itertools.slidingTuples(vals); +/// // Iter.toArray(tuples) == [(5, 3), (3, 3), (3, 7), (7, 8), (8, 10)] +/// +/// let diff = func (x : (Int, Int)) : Int { x.1 - x.0 }; +/// let iter = Iter.map(tuples, diff); +/// +/// assert Iter.toArray(iter) == [-2, 0, 4, 1, 2]; +/// ``` + +import Order "mo:base/Order"; +import Buffer "mo:base/Buffer"; +import Int "mo:base/Int"; +import Nat "mo:base/Nat"; +import Iter "mo:base/Iter"; +import Char "mo:base/Char"; +import Func "mo:base/Func"; +import Array "mo:base/Array"; +import Debug "mo:base/Debug"; +import Hash "mo:base/Hash"; +import Text "mo:base/Text"; +import Trie "mo:base/Trie"; +import TrieSet "mo:base/TrieSet"; +import Heap "mo:base/Heap"; +import TrieMap "mo:base/TrieMap"; +import Stack "mo:base/Stack"; +import List "mo:base/List"; +import Deque "mo:base/Deque"; +import Prelude "mo:base/Prelude"; + +import PeekableIter "PeekableIter"; +import Deiter "Deiter"; + +import ArrayMut_Utils "Utils/ArrayMut"; +import Nat_Utils "Utils/Nat"; +import TrieMap_Utils "Utils/TrieMap"; + +module { + + /// Returns a reference to a modified iterator that returns the accumulated values based on the given predicate. + /// + /// ### Example + /// - An example calculating the running sum of a iterator: + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4].vals(); + /// let it = Itertools.accumulate(vals, func(a, b) { a + b }); + /// + /// assert it.next() == ?1; + /// assert it.next() == ?3; + /// assert it.next() == ?6; + /// assert it.next() == ?10; + /// assert it.next() == ?null; + /// ``` + + public func accumulate(iter : Iter.Iter, predicate : (A, A) -> A) : Iter.Iter { + var acc = iter.next(); + + return object { + public func next() : ?A { + switch (acc, iter.next()) { + case (?_acc, ?n) { + let tmp = acc; + acc := ?predicate(_acc, n); + return tmp; + }; + case (?_acc, null) { + acc := null; + return ?_acc; + }; + case (_, _) { + return null; + }; + }; + }; + }; + }; + + /// Checks if all elements in the iterable satisfy the predicate. + /// + /// ### Example + /// - An example checking if all elements in a iterator of integers are even: + /// + /// ```motoko + /// + /// let a = [1, 2, 3, 4].vals(); + /// let b = [2, 4, 6, 8].vals(); + /// + /// let isEven = func(a: Int): Bool { a % 2 == 0 }; + /// + /// assert Itertools.all(a, isEven) == false; + /// assert Itertools.all(b, isEven) == true; + /// ``` + public func all(iter : Iter.Iter, predicate : (A) -> Bool) : Bool { + for (item in iter) { + if (not predicate(item)) { + return false; + }; + }; + return true; + }; + + /// Checks if at least one element in the iterator satisfies the predicate. + /// + /// ### Example + /// - An example checking if any element in a iterator of integers is even: + /// + /// ```motoko + /// + /// let a = [1, 2, 3, 4].vals(); + /// let b = [1, 3, 5, 7].vals(); + /// + /// let isEven = func(a: Nat) : Bool { a % 2 == 0 }; + /// + /// assert Itertools.any(a, isEven) == true; + /// assert Itertools.any(b, isEven) == false; + /// ``` + public func any(iter : Iter.Iter, predicate : (A) -> Bool) : Bool { + for (item in iter) { + if (predicate(item)) { + return true; + }; + }; + return false; + }; + + /// Adds an element to the end of an iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let iter = [1, 2, 3, 4].vals(); + /// let new_iter = Itertools.add(iter, 5); + /// + /// assert Iter.toArray(new_iter) == [1, 2, 3, 4, 5] + /// + public func add(iter : Iter.Iter, elem : A) : Iter.Iter { + var popped = false; + + object { + public func next() : ?A { + switch (iter.next()) { + case (?val) { + ?val; + }; + case (_) { + if (popped) { + null; + } else { + popped := true; + ?elem; + }; + }; + }; + }; + }; + }; + + /// Returns the cartesian product of the given iterables as an iterator of tuples. + /// + /// The resulting iterator contains all the combinations between elements in the two given iterators. + /// + /// ### Example + /// + /// ```motoko + /// + /// let a = [1, 2, 3].vals(); + /// let b = "abc".chars(); + /// + /// let it = Itertools.cartesianProduct(a, b); + /// + /// assert Iter.toArray(it) == [ + /// (1, 'a'), (1, 'b'), (1, 'c'), + /// (2, 'a'), (2, 'b'), (2, 'c'), + /// (3, 'a'), (3, 'b'), (3, 'c') + /// ]; + /// + /// ``` + public func cartesianProduct(iterA : Iter.Iter, iterB : Iter.Iter) : Iter.Iter<(A, B)> { + var optionA = iterA.next(); + + let buffer = Buffer.Buffer(8); + var i = 0; + + return object { + public func next() : ?(A, B) { + switch (optionA, iterB.next()) { + case (?a, ?b) { + buffer.add(b); + return ?(a, b); + }; + case (?a, _) { + if (i == buffer.size() or i == 0) { + switch (iterA.next()) { + case (?a) { + i := 1; + optionA := ?a; + return ?(a, buffer.get(0)); + }; + case (null) { + return null; + }; + }; + } else { + let tmp = buffer.get(i); + i += 1; + ?(a, tmp); + }; + }; + case (_) { + return null; + }; + }; + }; + }; + }; + + /// Counts the frequency of an element in the iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let a = [1, 2, 3, 1, 2, 3].vals(); + /// + /// let freq = Itertools.count(a, 1, Nat.equal); + /// + /// assert freq == 2; + /// ``` + public func count(iter : Iter.Iter, element : A, isEq : (A, A) -> Bool) : Nat { + var count = 0; + + for (item in iter) { + if (isEq(element, item)) { + count += 1; + }; + }; + + count; + }; + + /// Returns a TrieMap where the elements in the iterator are stored + /// as keys and the frequency of the elements are values + /// + /// ### Example + /// + /// ```motoko + /// + /// let a = "motoko".chars(); + /// + /// let freqMap = Itertools.countAll(a, Char.hash, Char.equal); + /// let res = Iter.toArray(freqMap.entries()); + /// + /// assert res == [('k', 1), ('m', 1), ('o', 3), ('t', 1)]; + /// ``` + public func countAll(iter : Iter.Iter, hashFn : (A) -> Hash.Hash, isEq : (A, A) -> Bool) : TrieMap.TrieMap { + var map = TrieMap.TrieMap(isEq, hashFn); + + func increment(n : Nat) : Nat { + n + 1; + }; + + for (item in iter) { + TrieMap_Utils.putOrUpdate(map, item, 1, increment); + }; + + map; + }; + + /// Chains two iterators of the same type together, so that all the + /// elements in the first iterator come before the second one. + /// + /// ### Example + /// ```motoko + /// + /// let iter1 = [1, 2].vals(); + /// let iter2 = [3, 4].vals(); + /// let chained = Itertools.chain(iter1, iter2); + /// + /// assert chained.next() == ?1 + /// assert chained.next() == ?2 + /// assert chained.next() == ?3 + /// assert chained.next() == ?4 + /// assert chained.next() == null + /// ``` + public func chain(a : Iter.Iter, b : Iter.Iter) : Iter.Iter { + return object { + public func next() : ?A { + switch (a.next()) { + case (?x) { + ?x; + }; + case (null) { + b.next(); + }; + }; + }; + }; + }; + + /// Returns an iterator that accumulates elements into arrays with a size less that or equal to the given `size`. + /// + /// ### Example + /// - An example grouping a iterator of integers into arrays of size `3`: + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].vals(); + /// let it = Itertools.chunks(vals, 3); + /// + /// assert it.next() == ?[1, 2, 3]; + /// assert it.next() == ?[4, 5, 6]; + /// assert it.next() == ?[7, 8, 9]; + /// assert it.next() == ?[10]; + /// assert it.next() == null; + /// ``` + public func chunks(iter : Iter.Iter, size : Nat) : Iter.Iter<[A]> { + assert size > 0; + var buf = Buffer.Buffer(size); + + object { + public func next() : ?[A] { + var i = 0; + + label l while (i < size) { + switch (iter.next()) { + case (?val) { + buf.add(val); + i := i + 1; + }; + case (_) { + break l; + }; + }; + }; + + if (buf.size() == 0) { + null; + } else { + let tmp = ?Buffer.toArray(buf); + buf.clear(); + tmp; + }; + }; + }; + }; + + /// Returns an iterator that accumulates elements into arrays with sizes exactly equal to the given one. + /// If the iterator is shorter than `n` elements, `null` is returned. + /// + /// ### Example + /// - An example grouping a iterator of integers into arrays of size `3`: + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].vals(); + /// let it = Itertools.chunksExact(vals, 3); + /// + /// assert it.next() == ?[1, 2, 3]; + /// assert it.next() == ?[4, 5, 6]; + /// assert it.next() == ?[7, 8, 9]; + /// assert it.next() == null; + /// ``` + public func chunksExact(iter : Iter.Iter, size : Nat) : Iter.Iter<[A]> { + assert size > 0; + + let chunksIter = chunks(iter, size); + + object { + public func next() : ?[A] { + switch (chunksIter.next()) { + case (?chunk) { + if (chunk.size() == size) { + ?chunk; + } else { + null; + }; + }; + case (null) { + null; + }; + }; + }; + }; + }; + + /// Returns all the combinations of a given iterator. + /// + /// ### Example + /// - An example grouping a iterator of integers into arrays of size `3`: + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4].vals(); + /// let it = Itertools.combinations(vals, 3); + /// + /// assert it.next() == ?[1, 2, 3]; + /// assert it.next() == ?[1, 2, 4]; + /// assert it.next() == ?[1, 3, 4]; + /// assert it.next() == ?[2, 3, 4]; + /// assert it.next() == null; + /// ``` + /// + /// - An example grouping a iterator of integers into arrays of size `2`: + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4].vals(); + /// let it = Itertools.combinations(vals, 2); + /// + /// assert it.next() == ?[1, 2]; + /// assert it.next() == ?[1, 3]; + /// assert it.next() == ?[1, 4]; + /// assert it.next() == ?[2, 3]; + /// assert it.next() == ?[2, 4]; + /// assert it.next() == ?[3, 4]; + /// assert it.next() == null; + /// ``` + public func combinations(iter : Iter.Iter, size : Nat) : Iter.Iter<[Nat]> { + assert size > 0; + + let buffer = Buffer.Buffer(8); + let cbns = Buffer.Buffer(size); + + let indices = Buffer.Buffer(size); + for (i in range(0, size)) { + indices.add(i); + }; + + var bufferIsFilled = false; + + object { + public func next() : ?[Nat] { + // fill buffer incrementally + if (not bufferIsFilled) { + switch (iter.next()) { + case (?n) { + buffer.add(n); + }; + case (_) { + bufferIsFilled := true; + }; + }; + }; + + // recursively build combinations + if (indices.size() == 0) { + null; + } else if (cbns.size() == size) { + let res = Buffer.toArray(cbns); + ignore cbns.removeLast(); + ?res; + } else { + if (indices.size() > cbns.size()) { + let i = indices.get(cbns.size()); + + if (i >= buffer.size()) { + if (cbns.size() == 0) { + null; + } else { + + ignore indices.removeLast(); + + if (indices.size() == 0) { + return null; + }; + + ignore cbns.removeLast(); + next(); + }; + } else { + indices.put(cbns.size(), i + 1); + cbns.add(buffer.get(i)); + + next(); + }; + } else { + indices.add( + indices.get(indices.size() - 1), + ); + next(); + }; + }; + }; + }; + }; + + /// Creates an iterator that loops over the values of a + /// given iterator `n` times. + /// + /// ### Example + /// + /// ```motoko + /// + /// let chars = "abc".chars(); + /// let it = Itertools.cycle(chars, 3); + /// + /// assert it.next() == ?'a'; + /// assert it.next() == ?'b'; + /// assert it.next() == ?'c'; + /// + /// assert it.next() == ?'a'; + /// assert it.next() == ?'b'; + /// assert it.next() == ?'c'; + /// + /// assert it.next() == ?'a'; + /// assert it.next() == ?'b'; + /// assert it.next() == ?'c'; + /// + /// assert it.next() == null; + /// ``` + public func cycle(iter : Iter.Iter, n : Nat) : Iter.Iter { + var buf = Buffer.Buffer(1); + var buf_index = 0; + var i = 0; + + return object { + public func next() : ?A { + if (i == n) { + null; + } else { + switch (iter.next()) { + case (?x) { + buf.add(x); + ?x; + }; + case (null) { + if (buf.size() == 0) { + null; + } else { + if (buf_index < buf.size()) { + buf_index += 1; + ?buf.get(buf_index - 1); + } else { + i += 1; + if (i < n) { + buf_index := 1; + ?buf.get(buf_index - 1); + } else { + null; + }; + }; + }; + }; + }; + }; + }; + }; + }; + + /// Returns an iterator that returns tuples with the index of the element + /// and the element. + /// + /// The index starts at 0 and is the first item in the tuple. + /// + /// ```motoko + /// + /// let chars = "abc".chars(); + /// let iter = Itertools.enumerate(chars); + /// + /// for ((i, c) in iter){ + /// Debug.print((i, c)); + /// }; + /// + /// // (0, 'a') + /// // (1, 'b') + /// // (2, 'c') + /// ``` + public func enumerate(iter : Iter.Iter) : Iter.Iter<(Nat, A)> { + var i = 0; + return object { + public func next() : ?(Nat, A) { + let nextVal = iter.next(); + + switch nextVal { + case (?v) { + let val = ?(i, v); + i += 1; + + return val; + }; + case (_) null; + }; + }; + }; + }; + + /// Creates an empty iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let it = Itertools.empty(); + /// assert it.next() == null; + /// + /// ``` + public func empty() : Iter.Iter { + return object { + public func next() : ?A { + null; + }; + }; + }; + + /// Checks if two iterators are equal. + /// + /// ### Example + /// + /// ```motoko + /// + /// let it1 = Itertools.range(1, 10); + /// let it2 = Itertools.range(1, 10); + /// + /// assert Itertools.equal(it1, it2, Nat.equal); + /// + /// let it3 = Itertools.range(1, 5); + /// let it4 = Itertools.range(1, 10); + /// + /// assert not Itertools.equal(it3, it4, Nat.equal); + /// ``` + public func equal(iter1 : Iter.Iter, iter2 : Iter.Iter, isEq : (A, A) -> Bool) : Bool { + + switch ((iter1.next(), iter2.next())) { + case ((?a, ?b)) { + if (isEq(a, b)) { + equal(iter1, iter2, isEq); + } else { + false; + }; + }; + case ((null, ?b)) false; + case ((?a, null)) false; + case ((null, null)) true; + }; + + }; + + /// Looks for an element in an iterator that matches a predicate. + /// + /// ### Example + /// - An example finding the first even number in an iterator: + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// + /// let isEven = func( x : Int ) : Bool {x % 2 == 0}; + /// let res = Itertools.find(vals, isEven); + /// + /// assert res == ?2 + /// ``` + public func find(iter : Iter.Iter, predicate : (A) -> Bool) : ?A { + for (val in iter) { + if (predicate(val)) { + return ?val; + }; + }; + return null; + }; + + /// Return the index of an element in an iterator that matches a predicate. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// + /// let isEven = func( x : Int ) : Bool {x % 2 == 0}; + /// let res = Itertools.findIndex(vals, isEven); + /// + /// assert res == ?1; + /// ``` + public func findIndex(iter : Iter.Iter, predicate : (A) -> Bool) : ?Nat { + var i = 0; + for (val in iter) { + if (predicate(val)) { + return ?i; + }; + i += 1; + }; + return null; + }; + + /// Returns an iterator with the indices of all the elements that match the predicate. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5, 6].vals(); + /// + /// let isEven = func( x : Int ) : Bool {x % 2 == 0}; + /// let res = Itertools.findIndices(vals, isEven); + /// + /// assert Iter.toArray(res) == [1, 3, 5]; + /// + /// ``` + public func findIndices(iter : Iter.Iter, predicate : (A) -> Bool) : Iter.Iter { + var i = 0; + return object { + public func next() : ?Nat { + for (val in iter) { + i += 1; + + if (predicate(val)) { + return ?(i - 1); + }; + + }; + + return null; + }; + }; + }; + + /// Returns the accumulated result of applying of the given + /// function to each element and the previous result starting with + /// the initial value. + /// + /// This method is similar to [reduce](#reduce) but it takes an initial + /// value and does not return an optional value. + /// + /// ### Example + /// + /// ```motoko + /// import Nat8 "mo:base/Nat8"; + /// + /// let arr : [Nat8] = [1, 2, 3, 4, 5]; + /// let sumToNat = func(acc: Nat, n: Nat8): Nat { + /// acc + Nat8.toNat(n) + /// }; + /// + /// let sum = Itertools.fold( + /// arr.vals(), + /// 200, + /// sumToNat + /// ); + /// + /// assertTrue(sum == 215) + /// ``` + /// + /// You can easily fold from the right to left using a + /// [`Deiter`](Deiter.html) to reverse the iterator before folding. + + public func fold(iter : Iter.Iter, initial : B, f : (B, A) -> B) : B { + var res = initial; + for (val in iter) { + res := f(res, val); + }; + + return res; + }; + + /// Flattens nested iterators into a single iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let nestedIter = [ + /// [1].vals(), + /// [2, 3].vals(), + /// [4, 5, 6].vals() + /// ].vals(); + /// + /// let flattened = Itertools.flatten(nestedIter); + /// assert Iter.toArray(flattened) == [1, 2, 3, 4, 5, 6]; + /// ``` + + public func flatten(nestedIter : Iter.Iter>) : Iter.Iter { + var iter : Iter.Iter = switch (nestedIter.next()) { + case (?_iter) { + _iter; + }; + case (_) { + return empty(); + }; + }; + + object { + public func next() : ?A { + switch (iter.next()) { + case (?val) ?val; + case (_) { + switch (nestedIter.next()) { + case (?_iter) { + iter := _iter; + iter.next(); + }; + case (_) null; + }; + }; + }; + }; + }; + }; + + /// Returns an flattened iterator with all the values in a nested array + /// + /// ### Example + /// + /// ```motoko + /// + /// let arr = [[1], [2, 3], [4, 5, 6]]; + /// let flattened = Itertools.flatten(arr); + /// + /// assert Iter.toArray(flattened) == [1, 2, 3, 4, 5, 6]; + /// ``` + public func flattenArray(nestedArray : [[A]]) : Iter.Iter { + flatten( + Iter.map( + nestedArray.vals(), + func(arr : [A]) : Iter.Iter { + arr.vals(); + }, + ), + ); + }; + + /// Groups nearby elements into arrays based on result from the given function and returns them along with the result of elements in that group. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].vals(); + /// + /// let isFactorOf30 = func( x : Int ) : Bool {x % 30 == 0}; + /// let groups = Itertools.groupBy(vals, isFactorOf30); + /// + /// assert Iter.toArray(groups) == [ + /// ([1, 2, 3], true), + /// ([4], false), + /// ([5, 6], true), + /// ([7, 8, 9], false), + /// ([10], true) + /// ]; + /// + /// ``` + public func groupBy(iter : Iter.Iter, pred : (A) -> Bool) : Iter.Iter<([A], Bool)> { + let group = Buffer.Buffer(8); + + func nextGroup() : ?([A], Bool) { + switch (iter.next()) { + case (?val) { + if (group.size() == 0) { + group.add(val); + return nextGroup(); + }; + + if (pred(group.get(0)) == pred(val)) { + group.add(val); + nextGroup(); + } else { + let arr = Buffer.toArray(group); + + group.clear(); + group.add(val); + + ?(arr, pred(arr[0])); + }; + }; + case (_) { + if (group.size() == 0) { + null; + } else { + let arr = Buffer.toArray(group); + + group.clear(); + + ?(arr, pred(arr[0])); + }; + }; + }; + }; + + return object { + public func next() : ?([A], Bool) { + nextGroup(); + }; + }; + }; + + /// Pass in a callback function to the iterator that performs a task every time the iterator is advanced. + /// + /// ### Example + /// + /// ```motoko + /// import Debug "mo:base/Debug"; + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// + /// let printIfEven = func(n: Int) { + /// if (n % 2 == 0){ + /// Debug.print("This value [ " # debug_show n # " ] is even."); + /// } + /// }; + /// + /// let iter = Itertools.inspect(vals, printIfEven); + /// + /// assert Iter.toArray(iter) == [1, 2, 3, 4, 5]; + /// ``` + /// + /// - console: + /// ```bash + /// This value [ +2 ] is even. + /// This value [ +4 ] is even. + /// ``` + public func inspect(iter : Iter.Iter, callback : (A) -> ()) : Iter.Iter { + + object { + public func next() : ?A { + switch (iter.next()) { + case (?a) { + callback(a); + ?a; + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Alternates between two iterators of the same type until one is exhausted. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4].vals(); + /// let vals2 = [10, 20].vals(); + /// + /// let iter = Itertools.interleave(vals, vals2); + /// + /// assert iter.next() == ?1 + /// assert iter.next() == ?10 + /// assert iter.next() == ?2 + /// assert iter.next() == ?20 + /// assert iter.next() == null + /// ``` + + public func interleave(_iter1 : Iter.Iter, _iter2 : Iter.Iter) : Iter.Iter { + var iter1 = _iter1; + var iter2 = _iter2; + + return object { + public func next() : ?A { + + switch (iter1.next(), iter2.next()) { + case (?val, ?val2) { + + let tmp = iter1; + iter1 := prepend(val2, iter2); + iter2 := tmp; + + return ?val; + }; + + case (_) null; + }; + }; + }; + }; + + /// Alternates between two iterators of the same type until both are exhausted. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4].vals(); + /// let vals2 = [10, 20].vals(); + /// + /// let iter = Itertools.interleave(vals, vals2); + /// + /// assert iter.next() == ?1 + /// assert iter.next() == ?10 + /// assert iter.next() == ?2 + /// assert iter.next() == ?20 + /// assert iter.next() == ?3 + /// assert iter.next() == ?4 + /// assert iter.next() == null + /// ``` + public func interleaveLongest(_iter1 : Iter.Iter, _iter2 : Iter.Iter) : Iter.Iter { + var iter1 = _iter1; + var iter2 = _iter2; + + return object { + public func next() : ?A { + + switch (iter1.next()) { + case (?val) { + let tmp = iter1; + iter1 := iter2; + iter2 := tmp; + + return ?val; + }; + + case (_) iter2.next(); + }; + }; + }; + }; + + /// Returns an iterator that inserts a value between each pair + /// of values in an iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3].vals(); + /// let iter = Itertools.intersperse(vals, 10); + /// + /// assert Iter.toArray(iter) == [1, 10, 2, 10, 3]; + /// + /// ``` + public func intersperse(_iter : Iter.Iter, val : A) : Iter.Iter { + let iter = peekable(_iter); + var even = true; + + return object { + public func next() : ?A { + switch (iter.peek()) { + case (?item) { + if (even) { + even := false; + return iter.next(); + } else { + even := true; + return ?val; + }; + }; + case (_) null; + }; + }; + }; + }; + + /// Checks if all the elements in an iterator are sorted in ascending order + /// that for every element `a` ans its proceding element `b`, `a <= b`. + /// + /// Returns true if iterator is empty + /// + /// #Example + /// + /// ```motoko + /// import Nat "mo:base/Nat"; + /// + /// let a = [1, 2, 3, 4]; + /// let b = [1, 4, 2, 3]; + /// let c = [4, 3, 2, 1]; + /// + /// assert Itertools.isSorted(a.vals(), Nat.compare) == true; + /// assert Itertools.isSorted(b.vals(), Nat.compare) == false; + /// assert Itertools.isSorted(c.vals(), Nat.compare) == false; + /// + /// ``` + public func isSorted(iter : Iter.Iter, cmp : (A, A) -> Order.Order) : Bool { + var prev = switch (iter.next()) { + case (?n) { n }; + case (null) return true; + }; + + for (item in iter) { + if (cmp(prev, item) == #greater) { + return false; + }; + prev := item; + }; + + true; + }; + + /// Checks if all the elements in an iterator are sorted in descending order + /// + /// Returns true if iterator is empty + /// + /// #Example + /// + /// ```motoko + /// import Nat "mo:base/Nat"; + /// + /// let a = [1, 2, 3, 4]; + /// let b = [1, 4, 2, 3]; + /// let c = [4, 3, 2, 1]; + /// + /// assert Itertools.isSortedDesc(a.vals(), Nat.compare) == false; + /// assert Itertools.isSortedDesc(b.vals(), Nat.compare) == false; + /// assert Itertools.isSortedDesc(c.vals(), Nat.compare) == true; + /// + /// ``` + public func isSortedDesc(iter : Iter.Iter, cmp : (A, A) -> Order.Order) : Bool { + var prev = switch (iter.next()) { + case (?n) { n }; + case (null) return true; + }; + + for (item in iter) { + if (cmp(prev, item) == #less) { + return false; + }; + prev := item; + }; + + true; + }; + + /// Returns an iterator adaptor that mutates elements of an iterator by applying the given function to each entry. + /// Each entry consists of the index of the element and the element itself. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [2, 2, 2, 2, 2].vals(); + /// let mulWithIndex = func(i: Nat, val: Nat) { + /// i * val; + /// }; + /// + /// let iter = Itertools.mapEntries(vals, mulWithIndex); + /// + /// assert Iter.toArray(iter) == [0, 2, 4, 6, 8]; + /// + /// ``` + public func mapEntries(iter : Iter.Iter, f : (Nat, A) -> B) : Iter.Iter { + let entries = enumerate(iter); + + return object { + public func next() : ?B { + switch (entries.next()) { + case (?(i, val)) { + return ?f(i, val); + }; + case (_) null; + }; + }; + }; + }; + + /// Returns an iterator that filters elements based on a predicate and + /// maps them to a new value based on the second argument. + /// + /// ### Example + /// - An example filtering odd numbers and squaring them: + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].vals(); + /// + /// let filterOddSquareEven = func( x : Nat ) : Nat { + /// if (x % 2 == 1){ + /// null + /// }else{ + /// ?(x * x) + /// } + /// }; + /// + /// let it = Itertools.mapFilter(vals, filterOddSquareEven); + /// + /// assert it.next() == ?4 + /// assert it.next() == ?16 + /// assert it.next() == ?36 + /// assert it.next() == ?64 + /// assert it.next() == ?100 + /// assert it.next() == null + /// ``` + public func mapFilter(iter : Iter.Iter, optMapFn : (A) -> ?B) : Iter.Iter { + + func getNext() : ?B { + switch (iter.next()) { + case (?val) { + switch (optMapFn(val)) { + case (?newVal) { ?newVal }; + case (_) { getNext() }; + }; + }; + case (_) null; + }; + }; + + object { + public func next() : ?B { + getNext(); + }; + }; + }; + + /// Maps the elements of an iterator and accumulates them into a single value. + /// + /// ### Example + /// + /// - decode numeric representation of characters into a string + /// ```motoko + /// + /// let vals = [13, 15, 20, 15, 11, 15].vals(); + /// + /// let natToChar = func (x : Nat) : Text { + /// Char.toText( + /// Char.fromNat32( + /// Nat32.fromNat(x) + 96 + /// ) + /// ) + /// }; + /// + /// let concat = func (a : Text, b : Text) : Text { + /// a # b + /// }; + /// + /// let res = Itertools.mapReduce(vals, natToChar, concat); + /// + /// assert res == ?"motoko"; + /// + /// ``` + public func mapReduce(iter : Iter.Iter, f : (A) -> B, accFn : (B, B) -> B) : ?B { + reduce(Iter.map(iter, f), accFn); + }; + + /// Returns an iterator that maps and yields elements while the + /// predicate is true. + /// The predicate is true if it returns an optional value and + /// false if it + /// returns null. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// + /// let squareIntLessThan4 = func( x : Int ) : ?Int { + /// if (x < 4){ + /// return ?(x * x); + /// }else{ + /// return null; + /// }; + /// }; + /// + /// let it = Itertools.mapWhile(vals, squareIntLessThan4); + /// + /// assert it.next() == ?1; + /// assert it.next() == ?4; + /// assert it.next() == ?9; + /// assert it.next() == null; + /// assert it.next() == null; + /// + /// ``` + public func mapWhile(iter : Iter.Iter, pred : (A) -> ?B) : Iter.Iter { + var ctrl = true; + return object { + public func next() : ?B { + if (ctrl == false) { + return null; + }; + + switch (iter.next()) { + case (?n) { + switch (pred(n)) { + case (?v) { + ?v; + }; + case (null) { + ctrl := false; + null; + }; + }; + }; + case (_) null; + }; + }; + }; + }; + + /// Returns the maximum value in an iterator. + /// A null value is returned if the iterator is empty. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let max = Itertools.max(vals, Nat.compare); + /// + /// assert max == ?5; + /// ``` + /// + /// - max on an empty iterator + /// + /// ```motoko + /// + /// let vals = [].vals(); + /// let max = Itertools.max(vals, Nat.compare); + /// + /// assert max == null; + /// ``` + public func max(iter : Iter.Iter, cmp : (A, A) -> Order.Order) : ?A { + var max : ?A = null; + + for (val in iter) { + switch (max) { + case (?m) { + if (cmp(val, m) == #greater) { + max := ?val; + }; + }; + case (null) { + max := ?val; + }; + }; + }; + + return max; + }; + + /// Returns the minimum value in an iterator. + /// A null value is returned if the iterator is empty. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [8, 4, 6, 9].vals(); + /// let min = Itertools.min(vals, Nat.compare); + /// + /// assert min == ?4; + /// ``` + /// + /// - min on an empty iterator + /// + /// ```motoko + /// + /// let vals: [Nat] = [].vals(); + /// let min = Itertools.min(vals, Nat.compare); + /// + /// assert min == null; + /// ``` + public func min(iter : Iter.Iter, cmp : (A, A) -> Order.Order) : ?A { + var min : ?A = null; + + for (val in iter) { + switch (min) { + case (?m) { + if (cmp(val, m) == #less) { + min := ?val; + }; + }; + case (null) { + min := ?val; + }; + }; + }; + + return min; + }; + + /// Returns a tuple of the minimum and maximum value in an iterator. + /// The first element is the minimum, the second the maximum. + /// + /// A null value is returned if the iterator is empty. + /// + /// If the iterator contains only one element, then it is returned as both + /// the minimum and the maximum. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [8, 4, 6, 9].vals(); + /// let minmax = Itertools.minmax(vals); + /// + /// assert minmax == ?(4, 9); + /// ``` + /// + /// - minmax on an empty iterator + /// + /// ```motoko + /// + /// let vals = [].vals(); + /// let minmax = Itertools.minmax(vals); + /// + /// assert minmax == null; + /// ``` + /// - minmax on an iterator with one element + /// + /// ```motoko + /// + /// let vals = [8].vals(); + /// let minmax = Itertools.minmax(vals); + /// + /// assert minmax == ?(8, 8); + /// ``` + public func minmax(iter : Iter.Iter, cmp : (A, A) -> Order.Order) : ?(A, A) { + let (_min, _max) = switch (iter.next()) { + case (?a) { + switch (iter.next()) { + case (?b) { + switch (cmp(a, b)) { + case (#less) { (a, b) }; + case (_) { (b, a) }; + }; + }; + case (_) { (a, a) }; + }; + }; + case (_) { + return null; + }; + }; + + var min = _min; + var max = _max; + + for (val in iter) { + if (cmp(val, min) == #less) { + min := val; + }; + + if (cmp(val, max) == #greater) { + max := val; + }; + }; + + ?(min, max) + + }; + + /// Returns an iterator that merges two iterators in order. + /// + /// The two iterators must have be of the same type + /// + /// ### Example + /// + /// - merge two sorted lists + /// + /// ```motoko + /// + /// let vals1 = [5, 6, 7].vals(); + /// let vals2 = [1, 3, 4].vals(); + /// let merged = Itertools.merge(vals1, vals2, Nat.compare); + /// + /// assert Iter.toArray(merged) == [1, 3, 4, 5, 6, 7]; + /// ``` + /// + /// - merge two unsorted lists + /// + /// ```motoko + /// + /// let vals1 = [5, 2, 3].vals(); + /// let vals2 = [8, 4, 1].vals(); + /// let merged = Itertools.merge(vals1, vals2, Nat.compare); + /// + /// assert Iter.toArray(merged) == [5, 2, 3, 8, 4, 1]; + /// ``` + public func merge(iter1 : Iter.Iter, iter2 : Iter.Iter, cmp : (A, A) -> Order.Order) : Iter.Iter { + let p1 = peekable(iter1); + let p2 = peekable(iter2); + + object { + public func next() : ?A { + switch (p1.peek(), p2.peek()) { + case (?a, ?b) { + if (cmp(a, b) == #less) { + p1.next(); + } else { + p2.next(); + }; + }; + case (_, ?b) { + p2.next(); + }; + case (?a, _) { + p1.next(); + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Returns an iterator that merges `k` iterators in order based on the `cmp` function. + /// + /// > Note: The iterators must have be of the same type + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals1 = [5, 6, 7].vals(); + /// let vals2 = [1, 3, 4].vals(); + /// let vals3 = [8, 4, 1].vals(); + /// let merged = Itertools.kmerge([vals1, vals2, vals3], Nat.compare); + /// + /// assert Iter.toArray(merged) == [1, 3, 4, 5, 6, 7, 8, 4, 1]; + /// ``` + + public func kmerge(iters : [Iter.Iter], cmp : (A, A) -> Order.Order) : Iter.Iter { + type Index = (A, Nat); + + let cmpIters = func(a : Index, b : Index) : Order.Order { + cmp(a.0, b.0); + }; + + let heap = Heap.Heap>(cmpIters); + + for ((i, iter) in enumerate(iters.vals())) { + switch (iter.next()) { + case (?a) { + heap.put((a, i)); + }; + case (_) { + + }; + }; + }; + + object { + public func next() : ?A { + switch (heap.removeMin()) { + case (?(min, i)) { + switch (iters[i].next()) { + case (?a) { + heap.put((a, i)); + }; + case (_) {}; + }; + + ?min; + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Returns the run-length encoding of an Iterator + /// + /// ### Example + /// + /// ```motoko + /// + /// let chars = "aaaabbbccd".chars(); + /// + /// let iter = Itertools.runLength(text.chars(), Char.equal); + /// let res = Iter.toArray(iter); + /// + /// assert res == [('a', 4), ('b', 3), ('c', 2), ('d', 1)] + /// + /// ``` + public func runLength(iter : Iter.Iter, isEqual : (A, A) -> Bool) : Iter.Iter<(A, Nat)> { + var cnt = 1; + var curr = switch (iter.next()) { + case (?a) { ?a }; + case (_) { return empty() }; + }; + + object { + public func next() : ?(A, Nat) { + switch (curr, iter.next()) { + case (?a, ?b) { + if (isEqual(a, b)) { + cnt += 1; + next(); + } else { + let tmp = (a, cnt); + curr := ?b; + cnt := 1; + + ?tmp; + }; + }; + case (?a, _) { + curr := null; + + ?(a, cnt); + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Checks if two iterators are not equal. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals1 = [5, 6, 7].vals(); + /// let vals2 = [1, 3, 4].vals(); + /// + /// assert Itertools.notEqual(vals1, vals2, Nat.equal); + /// + /// let vals3 = [1, 3, 4].vals(); + /// let vals4 = [1, 3, 4].vals(); + /// + /// assert not Itertools.notEqual(vals3, vals4, Nat.equal)); + /// ``` + public func notEqual(iter1 : Iter.Iter, iter2 : Iter.Iter, isEq : (A, A) -> Bool) : Bool { + switch (iter1.next(), iter2.next()) { + case (?a, ?b) { + if (not isEq(a, b)) { + true; + } else { + notEqual(iter1, iter2, isEq); + }; + }; + case (_, ?b) true; + case (?a, _) true; + case (_, _) false; + }; + }; + + /// Returns the nth element of an iterator. + /// Consumes the first n elements of the iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [0, 1, 2, 3, 4, 5].vals(); + /// let nth = Itertools.nth(vals, 3); + /// + /// assert nth == ?3; + /// ``` + /// + public func nth(iter : Iter.Iter, n : Nat) : ?A { + let skippedIter = skip(iter, n); + return skippedIter.next(); + }; + + /// Returns the nth elements of an iterator or a given default value. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [0, 1, 2, 3, 4, 5].vals(); + /// + /// assert Itertools.nthOrDefault(vals, 3, -1) == ?3; + /// assert Itertools.nthOrDefault(vals, 3, -1) == ?-1; + /// ``` + public func nthOrDefault(iter : Iter.Iter, n : Nat, defaultValue : A) : A { + switch (nth(iter, n)) { + case (?a) { + return a; + }; + case (_) { + return defaultValue; + }; + }; + }; + + /// Pads an iterator with a given value until it is of a certain length. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3].vals(); + /// let padded = Itertools.pad(vals, 6, 0); + /// + /// assert Iter.toArray(padded) == [1, 2, 3, 0, 0, 0]; + /// ``` + public func pad(iter : Iter.Iter, length : Nat, value : A) : Iter.Iter { + var count = 0; + + object { + public func next() : ?A { + switch (iter.next()) { + case (?a) { + count += 1; + ?a; + }; + case (_) { + if (count < length) { + count += 1; + ?value; + } else { + null; + }; + }; + }; + }; + }; + }; + + /// Pads an iterator with the result of a given function until it is of a certain length. + /// + /// ### Example + /// + /// ```motoko + /// + /// + /// let vals = [1, 2, 3].vals(); + /// let incrementIndex = func (i: Nat) { i + 1 }; + /// + /// let padded = Itertools.padWithFn(vals, 6, incrementIndex); + ///assert Iter.toArray(padded) == [1, 2, 3, 4, 5, 6]; + /// ``` + public func padWithFn(iter : Iter.Iter, length : Nat, f : (Nat) -> A) : Iter.Iter { + var count : Nat = 0; + + object { + public func next() : ?A { + switch (iter.next()) { + case (?a) { + count += 1; + ?a; + }; + case (_) { + if (count < length) { + count += 1; + ?f(count - 1); + } else { + null; + }; + }; + }; + }; + }; + }; + + /// Takes a partition function that returns `true` or `false` + /// for each element in the iterator. + /// The iterator is partitioned into a tuple of two arrays. + /// The first array contains the elements all elements that + /// returned `true` and the second array contains the elements + /// that returned `false`. + /// + /// If the iterator is empty, it returns a tuple of two empty arrays. + /// ### Example + /// + /// ```motoko + /// + /// let vals = [0, 1, 2, 3, 4, 5].vals(); + /// let isEven = func (n: Nat) : Bool { n % 2 == 0 }; + /// + /// let (even, odd) = Itertools.partition(vals, isEven); + /// + /// assert even == [0, 2, 4]; + /// assert odd == [1, 3, 5]; + /// + /// ``` + public func partition(iter : Iter.Iter, f : (A) -> Bool) : ([A], [A]) { + let firstGroup = Buffer.Buffer(8); + let secondGroup = Buffer.Buffer(8); + + for (val in iter) { + if (f(val)) { + firstGroup.add(val); + } else { + secondGroup.add(val); + }; + }; + + (Buffer.toArray(firstGroup), Buffer.toArray(secondGroup)); + }; + + /// Partitions an iterator in place so that the values that + /// return `true` from the `predicate` are on the left and the + /// values that return `false` are on the right. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [0, 1, 2, 3, 4, 5].vals(); + /// let isEven = func (n: Nat) : Bool { n % 2 == 0 }; + /// + /// let iter = Itertools.partitionInPlace(vals, isEven); + /// + /// assert Iter.toArray(iter) == [0, 2, 4, 1, 3, 5]; + /// + /// ``` + public func partitionInPlace(iter : Iter.Iter, f : (A) -> Bool) : Iter.Iter { + let secondGroup = Buffer.Buffer(8); + var i = 0; + + object { + public func next() : ?A { + label l loop { + switch (iter.next()) { + case (?a) { + if (f(a)) { + return ?a; + } else { + secondGroup.add(a); + }; + }; + case (_) { + if (i >= secondGroup.size()) { + return null; + } else { + break l; + }; + }; + }; + }; + + let tmp_index = i; + i += 1; + return ?secondGroup.get(tmp_index); + }; + }; + }; + + /// Checks if an iterator is partitioned by a predicate into + /// two consecutive groups. + /// The first n elements of the iterator return `true` when + /// passed to the predicate, and the rest return `false`. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [0, 2, 4, 1, 3, 5].vals(); + /// let isEven = func (n: Nat) : Bool { n % 2 == 0 }; + /// + /// let res = Itertools.isPartitioned(vals, isEven); + /// + /// assert res == true; + /// ``` + + public func isPartitioned(iter : Iter.Iter, f : (A) -> Bool) : Bool { + var inFirstGroup = true; + + for (val in iter) { + if (f(val)) { + if (not inFirstGroup) { + return false; + }; + } else { + if (inFirstGroup) { + inFirstGroup := false; + }; + }; + }; + + return true; + }; + + /// Returns a peekable iterator. + /// The iterator has a `peek` method that returns the next value + /// without consuming the iterator. + /// + /// ### Example + /// ```motoko + /// + /// let vals = Iter.fromArray([1, 2]); + /// let peekIter = Itertools.peekable(vals); + /// + /// assert peekIter.peek() == ?1; + /// assert peekIter.next() == ?1; + /// + /// assert peekIter.peek() == ?2; + /// assert peekIter.peek() == ?2; + /// assert peekIter.next() == ?2; + /// + /// assert peekIter.peek() == null; + /// assert peekIter.next() == null; + /// ``` + public func peekable(iter : Iter.Iter) : PeekableIter.PeekableIter { + PeekableIter.fromIter(iter); + }; + + /// Returns an iterator that yeilds all the permutations of the + /// elements of the iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3].vals(); + /// let perms = Itertools.permutations(vals, Nat.compare); + /// + /// assert Iter.toArray(perms) == [ + /// [1, 2, 3], [1, 3, 2], + /// [2, 1, 3], [2, 3, 1], + /// [3, 1, 2], [3, 2, 1] + /// ]; + /// ``` + public func permutations(iter : Iter.Iter, cmp : (A, A) -> Order.Order) : Iter.Iter<[A]> { + let arr = Iter.toArrayMut(iter); + let n = arr.size(); + + let totalPermutations = Nat_Utils.factorial(n); + var permutationsLeft = totalPermutations; + + object { + public func next() : ?[A] { + if (permutationsLeft == totalPermutations) { + permutationsLeft -= 1; + return ?Array.freeze(arr); + }; + + if (permutationsLeft == 0) { + return null; + }; + + permutationsLeft -= 1; + + var i = Int.abs(n - 2); + + while (i > 0 and not (cmp(arr[i], arr[i + 1]) == #less)) { + i -= 1; + }; + + var j = i +1; + + for (k in range(i + 1, n)) { + if (cmp(arr[k], arr[i]) == #greater) { + if (cmp(arr[k], arr[j]) == #less) { + j := k; + }; + }; + }; + + ArrayMut_Utils.swap(arr, i, j); + ArrayMut_Utils.reverseFrom(arr, i + 1); + + ?Array.freeze(arr); + }; + }; + }; + + /// Add a value to the front of an iterator. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [2, 3].vals(); + /// let iter = Itertools.prepend(1, vals); + /// + /// assert Iter.toArray(iter) == [1, 2, 3]; + /// ``` + public func prepend(value : A, iter : Iter.Iter) : Iter.Iter { + var popped = false; + object { + public func next() : ?A { + if (popped) { + iter.next(); + } else { + popped := true; + ?value; + }; + }; + }; + }; + + /// Consumes an iterator of integers and returns the product of all values. + /// An empty iterator returns null. + /// + /// ### Example + /// ```motoko + /// + /// let vals = [1, 2, 3, 4].vals(); + /// let prod = Itertools.product(vals, Nat.mul); + /// + /// assert prod == ?24; + /// ``` + public func product(iter : Iter.Iter, mul : (A, A) -> A) : ?A { + var acc : A = switch (iter.next()) { + case (?n) n; + case (_) return null; + }; + + for (n in iter) { + acc := mul(acc, n); + }; + + ?acc; + }; + + /// Returns a `Nat` iterator that yields numbers in range [start, end). + /// The base library provides a `range` function that returns an iterator from with start and end both inclusive. + /// + /// ### Example + /// + /// ```motoko + /// + /// let iter = Itertools.range(1, 5); + /// + /// assert iter.next() == ?1; + /// assert iter.next() == ?2; + /// assert iter.next() == ?3; + /// assert iter.next() == ?4; + /// assert iter.next() == null; + /// ``` + public func range(start : Nat, end : Nat) : Iter.Iter { + var i : Int = start; + + return object { + public func next() : ?Nat { + if (i < end) { + i += 1; + return ?Int.abs(i - 1); + } else { + return null; + }; + }; + }; + }; + + /// Returns a `Int` iterator that yields numbers in range [start, end). + /// + /// ### Example + /// + /// ```motoko + /// + /// let iter = Itertools.intRange(1, 4); + /// + /// assert iter.next() == ?1; + /// assert iter.next() == ?2; + /// assert iter.next() == ?3; + /// assert iter.next() == null; + /// ``` + public func intRange(start : Int, end : Int) : Iter.Iter { + var i : Int = start; + + return object { + public func next() : ?Int { + if (i < end) { + i += 1; + return ?i; + } else { + return null; + }; + }; + }; + }; + + /// Returns an optional value representing the application of the given + /// function to each element and the accumulated result. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let add = func (a: Int, b: Int) : Int { a + b }; + /// + /// let sum = Itertools.reduce(vals, add); + /// + /// assert sum == ?15; + /// ``` + public func reduce(iter : Iter.Iter, f : (A, A) -> A) : ?A { + switch (iter.next()) { + case (?a) { + var acc = a; + + for (val in iter) { + acc := f(acc, val); + }; + + ?acc; + }; + case (_) { + return null; + }; + }; + }; + + /// Returns an iterator that repeats a given value `n` times. + /// To repeat a value infinitely, use `Iter.make` from the base library. + /// + /// ### Example + /// ```motoko + /// + /// let iter = Itertools.repeat(1, 3); + /// + /// assert iter.next() == ?1; + /// assert iter.next() == ?1; + /// assert iter.next() == ?1; + /// assert iter.next() == null; + /// ``` + public func repeat(item : A, n : Nat) : Iter.Iter { + var i = 0; + return object { + public func next() : ?A { + if (i < n) { + i += 1; + return ?item; + } else { + null; + }; + }; + }; + }; + + /// Skips the first n elements of the iter + /// + /// ### Example + /// ```motoko + /// + /// let iter = [1, 2, 3, 4, 5].vals(); + /// Itertools.skip(iter, 2); + /// + /// assert iter.next() == ?3; + /// assert iter.next() == ?4; + /// assert iter.next() == ?5; + /// assert iter.next() == null; + /// ``` + public func skip(iter : Iter.Iter, n : Nat) : Iter.Iter { + var i = 0; + label l while (i < n) { + switch (iter.next()) { + case (?val) { + i := i + 1; + }; + case (_) { + break l; + }; + }; + }; + + iter; + }; + + /// Skips elements continuously while the predicate is true. + /// + /// ### Example + /// ```motoko + /// + /// let iter = [1, 2, 3, 4, 5].vals(); + /// let lessThan3 = func (a: Int) : Bool { a < 3 }; + /// + /// Itertools.skipWhile(iter, lessThan3); + /// + /// assert Iter.toArray(iter) == [3, 4, 5]; + /// + /// ``` + public func skipWhile(iter : Iter.Iter, pred : (A) -> Bool) : Iter.Iter { + let peekableIter = peekable(iter); + + label l loop { + switch (peekableIter.peek()) { + case (?val) { + if (not pred(val)) { + break l; + }; + + ignore peekableIter.next(); + }; + case (_) { + break l; + }; + }; + }; + + peekableIter; + }; + + /// Returns overlapping tuple pairs from the given iterator. + /// The first element of the iterator is paired with the second element, and the + /// second is paired with the third element, and so on. + /// ?(a, b), ?(b, c), ?(c, d), ... + /// + /// If the iterator has fewer than two elements, an null value is returned. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let pairs = Itertools.slidingTuples(vals); + /// + /// assert pairs.next() == ?(1, 2); + /// assert pairs.next() == ?(2, 3); + /// assert pairs.next() == ?(3, 4); + /// assert pairs.next() == ?(4, 5); + /// assert pairs.next() == null; + /// ``` + public func slidingTuples(iter : Iter.Iter) : Iter.Iter<(A, A)> { + var prev = iter.next(); + + return object { + public func next() : ?(A, A) { + switch (prev, iter.next()) { + case (?_prev, ?curr) { + let tmp = (_prev, curr); + prev := ?curr; + ?tmp; + }; + case (_) { + return null; + }; + }; + }; + }; + }; + + /// Returns consecutive, overlapping triplets from the given iterator. + /// The iterator returns a tuple of three elements, which include the current element and the two proceeding ones. + /// ?(a, b, c), ?(b, c, d), ?(c, d, e), ... + /// + /// If the iterator has fewer than three elements, an null value is returned. + /// + /// ### Example + /// + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let triples = Itertools.slidingTriples(vals); + /// + /// assert triples.next() == ?(1, 2, 3); + /// assert triples.next() == ?(2, 3, 4); + /// assert triples.next() == ?(3, 4, 5); + /// assert triples.next() == null; + /// ``` + public func slidingTriples(iter : Iter.Iter) : Iter.Iter<(A, A, A)> { + var a = iter.next(); + var b = iter.next(); + + return object { + public func next() : ?(A, A, A) { + switch (a, b, iter.next()) { + case (?_a, ?_b, ?curr) { + let tmp = (_a, _b, curr); + a := b; + b := ?curr; + ?tmp; + }; + case (_) { + return null; + }; + }; + }; + }; + }; + + /// Returns an iterator where all the elements are sorted in ascending order. + /// + /// ### Example + /// ```motoko + /// + /// let vals = [8, 3, 5, 4, 1].vals(); + /// let sorted = Itertools.sort(vals); + /// + /// assert Iter.toArray(sorted) == [1, 3, 4, 5, 8]; + /// ``` + public func sort(iter : Iter.Iter, cmp : (A, A) -> Order.Order) : Iter.Iter { + let heap = Heap.Heap(cmp); + + for (val in iter) { + heap.put(val); + }; + + object { + public func next() : ?A { + heap.removeMin(); + }; + }; + }; + + /// Returns a tuple of iterators where the first element is the first n elements of the iterator, and the second element is the remaining elements. + /// + /// ### Example + /// ```motoko + /// + /// let iter = [1, 2, 3, 4, 5].vals(); + /// let (left, right) = Itertools.splitAt(iter, 3); + /// + /// assert left.next() == ?1; + /// assert right.next() == ?4; + /// + /// assert left.next() == ?2; + /// assert right.next() == ?5; + /// + /// assert left.next() == ?3; + /// + /// assert left.next() == null; + /// assert right.next() == null; + /// ``` + public func splitAt(iter : Iter.Iter, n : Nat) : (Iter.Iter, Iter.Iter) { + var left = Iter.toArray(take(iter, n)).vals(); + (left, iter); + }; + + /// Returns a tuple of iterators where the first element is an iterator with a copy of + /// the first n elements of the iterator, and the second element is the original iterator + /// with all the elements + /// + /// ### Example + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let (copy, iter) = Itertools.spy(vals, 3); + /// + /// assert copy.next() == ?1; + /// assert copy.next() == ?2; + /// assert copy.next() == ?3; + /// assert copy.next() == null; + /// + /// assert vals.next() == ?1; + /// assert vals.next() == ?2; + /// assert vals.next() == ?3; + /// assert vals.next() == ?4; + /// assert vals.next() == ?5; + /// assert vals.next() == null; + /// ``` + + public func spy(iter : Iter.Iter, n : Nat) : (Iter.Iter, Iter.Iter) { + // let firstN = + var copy = Iter.toArray(take(iter, n)); + (copy.vals(), chain(copy.vals(), iter)); + }; + + /// Returns every nth element of the iterator. + /// n must be greater than zero. + /// + /// ### Example + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let iter = Itertools.stepBy(vals, 2); + /// + /// assert iter.next() == ?1; + /// assert iter.next() == ?3; + /// assert iter.next() == ?5; + /// assert iter.next() == null; + /// ``` + public func stepBy(iter : Iter.Iter, n : Nat) : Iter.Iter { + assert n > 0; + + return object { + public func next() : ?A { + switch (iter.next()) { + case (?item) { + ignore skip(iter, Int.abs(n - 1)); + ?item; + }; + case (_) { + return null; + }; + }; + }; + }; + }; + + /// Creates an iterator from the given value a where the next + /// elements are the results of the given function applied to + /// the previous element. + /// + /// The function takes the previous value and returns an Optional + /// value. If the function returns null when the function + /// returns null. + /// + /// ### Example + /// ```motoko + /// import Nat "mo:base/Nat"; + /// + /// let optionSquaresOfSquares = func(n: Nat) : ?Nat{ + /// let square = n * n; + /// + /// if (square <= Nat.pow(2, 64)) { + /// return ?square; + /// }; + /// + /// return null; + /// }; + /// + /// let succIter = Itertools.successor( + /// 2, + /// optionSquaresOfSquares + /// ); + /// + /// let res = Iter.toArray(succIter); + /// + /// assert res == [ + /// 2, 4, 16, 256, 65_536, 4_294_967_296, + /// ]; + /// + /// ``` + public func successor(start : A, f : (A) -> ?A) : Iter.Iter { + var curr = start; + + object { + public func next() : ?A { + switch (f(curr)) { + case (?n) { + let prev = curr; + curr := n; + + ?prev; + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Consumes an iterator of integers and returns the sum of all values. + /// An empty iterator returns `null`. + /// + /// ### Example + /// ```motoko + /// + /// let vals = [1, 2, 3, 4].vals(); + /// let sum = Itertools.sum(vals, Nat.add); + /// + /// assert sum == ?10; + /// ``` + public func sum(iter : Iter.Iter, add : (A, A) -> A) : ?A { + var acc : A = switch (iter.next()) { + case (?n) n; + case (_) return null; + }; + + for (n in iter) { + acc := add(acc, n); + }; + + ?acc; + }; + + /// Returns an iterator with the first n elements of the given iter + /// > Be aware that this returns a reference to the original iterator so + /// > using it will cause the original iterator to be skipped. + /// + /// If you want to keep the original iterator, use `spy` instead. + /// + /// Note that using the returned iterator and the given iterator at the same time will cause the values in both iterators to be skipped. + /// + /// ### Example + /// ```motoko + /// + /// let iter = Iter.fromArray([1, 2, 3, 4, 5]); + /// let it = Itertools.take(iter, 3); + /// + /// assert it.next() == ?1; + /// assert it.next() == ?2; + /// assert it.next() == ?3; + /// assert it.next() == null; + /// + /// // the first n elements of the original iterator are skipped + /// assert iter.next() == ?4; + /// assert iter.next() == ?5; + /// assert iter.next() == null; + /// ``` + + public func take(iter : Iter.Iter, n : Nat) : Iter.Iter { + var i = 0; + return object { + public func next() : ?A { + if (i < n) { + i := i + 1; + iter.next(); + } else { + null; + }; + }; + }; + }; + + /// Creates an iterator that returns returns elements from the given iter while the predicate is true. + /// + /// ### Example + /// ```motoko + /// + /// let vals = Iter.fromArray([1, 2, 3, 4, 5]); + /// + /// let lessThan3 = func (x: Int) : Bool { x < 3 }; + /// let it = Itertools.takeWhile(vals, lessThan3); + /// + /// assert it.next() == ?1; + /// assert it.next() == ?2; + /// assert it.next() == null; + /// ``` + public func takeWhile(iter : Iter.Iter, predicate : A -> Bool) : Iter.Iter { + var iterate = true; + return object { + public func next() : ?A { + if (iterate) { + switch (iter.next()) { + case (?item) { + if (predicate(item)) { + ?item; + } else { + iterate := false; + null; + }; + }; + case (_) { + iterate := false; + return null; + }; + }; + } else { + return null; + }; + }; + }; + }; + + /// Consumes an iterator and returns a tuple of cloned iterators. + /// + /// ### Example + /// ```motoko + /// + /// let iter = [1, 2, 3].vals(); + /// let (iter1, iter2) = Itertools.tee(iter); + /// + /// assert iter1.next() == ?1; + /// assert iter1.next() == ?2; + /// assert iter1.next() == ?3; + /// assert iter1.next() == null; + /// + /// assert iter2.next() == ?1; + /// assert iter2.next() == ?2; + /// assert iter2.next() == ?3; + /// assert iter2.next() == null; + /// ``` + public func tee(iter : Iter.Iter) : (Iter.Iter, Iter.Iter) { + let array = Iter.toArray(iter); + + return (array.vals(), array.vals()); + }; + + /// Returns an iterator of consecutive, non-overlapping tuple pairs of elements from a single iter. + /// The first element is paired with the second element, the third element with the fourth, and so on. + /// ?(a, b), ?(c, d), ?(e, f) ... + /// + /// If the iterator has less than two elements, it will return a null. + /// > For overlappping pairs use slidingTuples. + /// + /// ### Example + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let it = Itertools.tuples(vals); + /// + /// assert it.next() == ?(1, 2); + /// assert it.next() == ?(3, 4); + /// assert it.next() == null; + /// + /// ``` + public func tuples(iter : Iter.Iter) : Iter.Iter<(A, A)> { + return object { + public func next() : ?(A, A) { + switch (iter.next(), iter.next()) { + case (?a, ?b) { + ?(a, b); + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Returns an iterator of consecutive, non-overlapping triplets of elements from a single iter. + /// ?(a, b, c), ?(d, e, f) ... + /// + /// If the iterator has less than three elements, it will return a null. + /// + /// ### Example + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5, 6, 7].vals(); + /// let it = Itertools.triples(vals); + /// + /// assert it.next() == ?(1, 2, 3); + /// assert it.next() == ?(4, 5, 6); + /// assert it.next() == null; + /// + /// ``` + public func triples(iter : Iter.Iter) : Iter.Iter<(A, A, A)> { + return object { + public func next() : ?(A, A, A) { + switch (iter.next(), iter.next(), iter.next()) { + case (?a, ?b, ?c) { + ?(a, b, c); + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Returns an iterator with unique elements from the given iter. + /// + /// ### Example + /// ```motoko + /// import Nat "mo:base/Nat"; + /// import Hash "mo:base/Hash"; + /// + /// let vals = [1, 2, 3, 1, 2, 3].vals(); + /// let it = Itertools.unique(vals, Hash.hash, Nat.equal); + /// + /// assert it.next() == ?1; + /// assert it.next() == ?2; + /// assert it.next() == ?3; + /// assert it.next() == null; + /// + /// ``` + public func unique(iter : Iter.Iter, hashFn : (A) -> Hash.Hash, isEq : (A, A) -> Bool) : Iter.Iter { + var set = TrieSet.empty(); + + return object { + public func next() : ?A { + var res : ?A = null; + + label l loop { + switch (iter.next()) { + case (?item) { + let hash = hashFn(item); + + if (TrieSet.mem(set, item, hash, isEq)) { + continue l; + }; + + set := TrieSet.put(set, item, hash, isEq); + res := ?item; + + break l; + }; + case (_) { + break l; + }; + }; + }; + + return res; + }; + }; + }; + + /// Returns an iterator with the elements of the given iter and a boolean + /// indicating if the element is unique. + /// + /// ### Example + /// ```motoko + /// import Nat "mo:base/Nat"; + /// import Hash "mo:base/Hash"; + /// + /// let vals = [1, 2, 3, 1, 2, 3].vals(); + /// let it = Itertools.uniqueCheck(vals, Hash.hash, Nat.equal); + /// + /// assert Iter.toArray(it) == [ + /// (1, true), (2, true), (3, true), + /// (1, false), (2, false), (3, false) + /// ]; + /// + /// ``` + public func uniqueCheck( + iter : Iter.Iter, + hashFn : (A) -> Hash.Hash, + isEq : (A, A) -> Bool, + ) : Iter.Iter<(A, Bool)> { + var set = TrieSet.empty(); + + return object { + public func next() : ?(A, Bool) { + var res : ?(A, Bool) = null; + + switch (iter.next()) { + case (?item) { + let hash = hashFn(item); + if (TrieSet.mem(set, item, hash, isEq)) { + ?(item, false); + } else { + set := TrieSet.put(set, item, hash, isEq); + ?(item, true); + }; + }; + case (_) { + null; + }; + }; + }; + }; + }; + + /// Returns `true` if all the elements in the given iter are unique. + /// The hash function and equality function are used to compare elements. + /// + /// > Note: If the iterator is empty, it will return `true`. + /// ### Example + /// + /// ```motoko + /// import Nat "mo:base/Nat"; + /// import Hash "mo:base/Hash"; + /// + /// let vals = [1, 2, 3, 1, 2, 3].vals(); + /// let res = Itertools.isUnique(vals, Hash.hash, Nat.equal); + /// + /// assert res == false; + /// + /// ``` + public func isUnique(iter : Iter.Iter, hashFn : (A) -> Hash.Hash, isEq : (A, A) -> Bool) : Bool { + var set = TrieSet.empty(); + + for (item in iter) { + let hash = hashFn(item); + + if (TrieSet.mem(set, item, hash, isEq)) { + return false; + }; + + set := TrieSet.put(set, item, hash, isEq); + }; + + return true; + }; + + /// Unzips an iterator of tuples into a tuple of arrays. + /// + /// ### Example + /// ```motoko + /// + /// let iter = [(1, 'a'), (2, 'b'), (3, 'c')].vals(); + /// let (arr1, arr2) = Itertools.unzip(iter); + /// + /// assert arr1 == [1, 2, 3]; + /// assert arr2 == ['a', 'b', 'c']; + /// ``` + public func unzip(iter : Iter.Iter<(A, A)>) : ([A], [A]) { + var buf1 = Buffer.Buffer(1); + var buf2 = Buffer.Buffer(1); + + for ((a, b) in iter) { + buf1.add(a); + buf2.add(b); + }; + + (Buffer.toArray(buf1), Buffer.toArray(buf2)); + }; + + /// Zips two iterators into one iterator of tuples + /// The length of the zipped iterator is equal to the length + /// of the shorter iterator + /// + /// ### Example + /// ```motoko + /// + /// let iter1 = [1, 2, 3, 4, 5].vals(); + /// let iter2 = "abc".chars(); + /// let zipped = Itertools.zip(iter1, iter2); + /// + /// assert zipped.next() == ?(1, 'a'); + /// assert zipped.next() == ?(2, 'b'); + /// assert zipped.next() == ?(3, 'c'); + /// assert zipped.next() == null; + /// ``` + + public func zip(a : Iter.Iter, b : Iter.Iter) : Iter.Iter<(A, B)> { + object { + public func next() : ?(A, B) { + switch (a.next(), b.next()) { + case (?valueA, ?valueB) ?(valueA, valueB); + case (_, _) null; + }; + }; + }; + }; + + /// Zips three iterators into one iterator of tuples + /// The length of the zipped iterator is equal to the length + /// of the shorter iterator + /// + /// ### Example + /// ```motoko + /// + /// let iter1 = [1, 2, 3, 4, 5].vals(); + /// let iter2 = "abc".chars(); + /// let iter3 = [1.35, 2.92, 3.74, 4.12, 5.93].vals(); + /// + /// let zipped = Itertools.zip3(iter1, iter2, iter3); + /// + /// assert zipped.next() == ?(1, 'a', 1.35); + /// assert zipped.next() == ?(2, 'b', 2.92); + /// assert zipped.next() == ?(3, 'c', 3.74); + /// assert zipped.next() == null; + /// ``` + public func zip3(a : Iter.Iter, b : Iter.Iter, c : Iter.Iter) : Iter.Iter<(A, B, C)> { + object { + public func next() : ?(A, B, C) { + switch (a.next(), b.next(), c.next()) { + case (?valueA, ?valueB, ?valueC) ?(valueA, valueB, valueC); + case (_) null; + }; + }; + }; + }; + + public type Either = { + #left : A; + #right : B; + }; + + public type EitherOr = Either or { + #both : (A, B); + }; + + /// Zips two iterators until both iterators are exhausted. + /// The length of the zipped iterator is equal to the length + /// of the longest iterator. + /// + /// The iterator returns a [`EitherOr`](#EitherOr) type of the two iterators. + /// + /// ### Example + /// ```motoko + /// + /// let iter1 = [1, 2, 3, 4, 5].vals(); + /// let iter2 = "abc".chars(); + /// let zipped = Itertools.zipLongest(iter1, iter2); + /// + /// assert zipped.next() == ?#both(1, 'a'); + /// assert zipped.next() == ?#both(2, 'b'); + /// assert zipped.next() == ?#both(3, 'c'); + /// assert zipped.next() == ?#left(4); + /// assert zipped.next() == ?#left(5); + /// assert zipped.next() == null; + /// ``` + public func zipLongest(iterA : Iter.Iter, iterB : Iter.Iter) : Iter.Iter> { + object { + public func next() : ?EitherOr { + switch (iterA.next(), iterB.next()) { + case (?a, ?b) ?#both(a, b); + case (?a, _) ?#left(a); + case (_, ?b) ?#right(b); + case (_, _) null; + }; + }; + }; + }; + + // ============================================================================================== + // =============================== Iterator Collection Methods =============================== + // ============================================================================================== + + /// Transforms a slice of an array into an iterator + /// + /// ### Example + /// ```motoko + /// + /// let arr = [1, 2, 3, 4, 5]; + /// let slicedIter = Itertools.fromArraySlice(arr, 2, arr.size()); + /// + /// assert Iter.toArray(slicedIter) == [3, 4, 5]; + /// ``` + public func fromArraySlice(arr : [A], start : Nat, end : Nat) : Iter.Iter { + var i = start; + var j = Nat.min(end, arr.size()); + + object { + public func next() : ?A { + if (i < j) { + i += 1; + ?arr[i - 1]; + } else { + null; + }; + }; + }; + }; + + /// Collects an iterator of any type into a buffer + /// + /// ### Example + /// ```motoko + /// + /// let vals = [1, 2, 3, 4, 5].vals(); + /// let buf = Itertools.toBuffer(vals); + /// + /// assert buf.toArray() == [1, 2, 3, 4, 5]; + /// ``` + public func toBuffer(iter : Iter.Iter) : Buffer.Buffer { + let buf = Buffer.Buffer(8); + for (item in iter) { + buf.add(item); + }; + + return buf; + }; + + /// Converts an iterator to a deque. + public func toDeque(iter : Iter.Iter) : Deque.Deque { + var dq = Deque.empty(); + + for (item in iter) { + dq := Deque.pushBack(dq, item); + }; + + dq; + }; + + /// Converts an Iter into a List + public func toList(iter : Iter.Iter) : List.List { + var list = List.nil(); + + for (item in iter) { + list := List.push(item, list); + }; + + list; + }; + + /// Collects an iterator of characters into a text + /// + /// ### Example + /// ```motoko + /// + /// let chars = "abc".chars(); + /// let text = Itertools.toText(chars); + /// + /// assert text == "abc"; + /// ``` + public func toText(charIter : Iter.Iter) : Text { + let textIter = Iter.map(charIter, func(c) { Char.toText(c) }); + Text.join("", textIter); + }; + + /// Converts a TrieSet into an Iter + public func fromTrieSet(set : TrieSet.Set) : Iter.Iter { + Iter.map<(A, ()), A>( + Trie.iter(set), + func((item, _)) { item }, + ); + }; + + /// Collects an iterator into a TrieSet + /// + /// ### Example + /// ```motoko + /// import Hash "mo:base/Hash"; + /// import TrieSet "mo:base/TrieSet"; + /// + /// let vals = [1, 1, 2, 3, 4, 4, 5].vals(); + /// let set = Itertools.toTrieSet(vals, Hash.hash, Nat.equal); + /// + /// let setIter = Itertools.fromTrieSet(set); + /// assert Iter.toArray(setIter) == [1, 2, 3, 4, 5]; + /// + /// ``` + public func toTrieSet(iter : Iter.Iter, hashFn : (A) -> Hash.Hash, isEq : (A, A) -> Bool) : TrieSet.Set { + var set = TrieSet.empty(); + + label l for (item in iter) { + let hash = hashFn(item); + + if (TrieSet.mem(set, item, hash, isEq)) { + continue l; + } else { + set := TrieSet.put(set, item, hash, isEq); + }; + }; + + set; + }; +}; diff --git a/src/ICRC1/itertools/PeekableIter.mo b/src/ICRC1/itertools/PeekableIter.mo new file mode 100644 index 0000000..5bc98ce --- /dev/null +++ b/src/ICRC1/itertools/PeekableIter.mo @@ -0,0 +1,55 @@ +/// Peekable Iterator +/// +/// An iterator equipped with a `peek` method that returns the next value without advancing the iterator. +/// +/// The `PeekableIter` type is an extension of the `Iter` type built in Motoko +/// so it is compatible with all the function defined for the `Iter` type. +/// +import Iter "mo:base/Iter"; + +module { + /// Peekable Iterator Type. + public type PeekableIter = Iter.Iter and { + peek : () -> ?T; + }; + + /// Creates a `PeekableIter` from an `Iter`. + /// + /// #### Example: + /// let vals = [1, 2].vals(); + /// let peekableIter = PeekableIter.fromIter(vals); + /// + /// assert peekableIter.peek() == ?1; + /// assert peekableIter.peek() == ?1; + /// assert peekableIter.next() == ?1; + /// + /// assert peekableIter.peek() == ?2; + /// assert peekableIter.peek() == ?2; + /// assert peekableIter.peek() == ?2; + /// assert peekableIter.next() == ?2; + /// + /// assert peekableIter.peek() == null; + /// assert peekableIter.next() == null; + /// ``` + public func fromIter(iter : Iter.Iter) : PeekableIter { + var next_item = iter.next(); + + return object { + public func peek() : ?T { + next_item; + }; + + public func next() : ?T { + switch (next_item) { + case (?val) { + next_item := iter.next(); + ?val; + }; + case (null) { + null; + }; + }; + }; + }; + }; +}; diff --git a/src/ICRC1/itertools/Utils/ArrayMut.mo b/src/ICRC1/itertools/Utils/ArrayMut.mo new file mode 100644 index 0000000..d5ed60a --- /dev/null +++ b/src/ICRC1/itertools/Utils/ArrayMut.mo @@ -0,0 +1,27 @@ +import Iter "mo:base/Iter"; +import Int "mo:base/Int"; + +module { + public func swap(arr:[var A], a: Nat, b: Nat) { + let tmp = arr[a]; + arr[a] := arr[b]; + arr[b] := tmp; + }; + + public func reverseFrom(arr: [var A], start: Nat) { + reverseRange(arr, start, Int.abs(arr.size() - 1)); + }; + + public func reverseRange(arr: [var A], start: Nat, end: Nat) { + assert(end < arr.size()); + + var i = start; + var j = end; + + while (i < j){ + swap(arr, i, j); + i += 1; + j -= 1; + }; + }; +} \ No newline at end of file diff --git a/src/ICRC1/itertools/Utils/Deque.mo b/src/ICRC1/itertools/Utils/Deque.mo new file mode 100644 index 0000000..ea919d3 --- /dev/null +++ b/src/ICRC1/itertools/Utils/Deque.mo @@ -0,0 +1,37 @@ +import Buffer "mo:base/Buffer"; +import Deque "mo:base/Deque"; + +module DequeUtils { + public func toArray(dq : Deque.Deque) : [A] { + var deque = dq; + var buffer = Buffer.Buffer(0); + + label l loop { + if (Deque.isEmpty(deque)) { + break l; + }; + + switch (Deque.popFront(deque)) { + case (?(x, xs)) { + deque := xs; + buffer.add(x); + }; + case (_) { + break l; + }; + }; + }; + + return Buffer.toArray(buffer); + }; + + public func fromArray(array : [A]) : Deque.Deque { + var deque = Deque.empty(); + + for (elem in array.vals()) { + deque := Deque.pushBack(deque, elem); + }; + + return deque; + }; +}; diff --git a/src/ICRC1/itertools/Utils/Nat.mo b/src/ICRC1/itertools/Utils/Nat.mo new file mode 100644 index 0000000..9d107f5 --- /dev/null +++ b/src/ICRC1/itertools/Utils/Nat.mo @@ -0,0 +1,10 @@ +import Int "mo:base/Int"; +module{ + public func factorial(n: Nat) : Nat { + if (n == 0){ + 1 + } else { + n * factorial(Int.abs(n - 1)) + } + }; +}; diff --git a/src/ICRC1/itertools/Utils/TrieMap.mo b/src/ICRC1/itertools/Utils/TrieMap.mo new file mode 100644 index 0000000..6e794a2 --- /dev/null +++ b/src/ICRC1/itertools/Utils/TrieMap.mo @@ -0,0 +1,19 @@ +import TrieMap "mo:base/TrieMap"; + +module{ + public func containsKey(map: TrieMap.TrieMap, key: A): Bool { + switch(map.get(key)){ + case (?a) true; + case (_) false + } + }; + + public func putOrUpdate(map: TrieMap.TrieMap, key: A, defaultVal: B, update: (B) -> B){ + let newVal = switch (map.get(key)){ + case (?val) update(val); + case (_) defaultVal; + }; + + map.put(key, newVal) + }; +}; \ No newline at end of file diff --git a/src/ICRC1/lib.mo b/src/ICRC1/lib.mo index f442e11..7c48a5f 100644 --- a/src/ICRC1/lib.mo +++ b/src/ICRC1/lib.mo @@ -11,8 +11,8 @@ import Option "mo:base/Option"; import Principal "mo:base/Principal"; import EC "mo:base/ExperimentalCycles"; -import Itertools "mo:itertools/Iter"; -import StableTrieMap "mo:StableTrieMap"; +import Itertools "itertools/Iter"; +import StableTrieMap "stable/StableTrieMap"; import Account "Account"; import T "Types"; @@ -32,6 +32,7 @@ module { public type Transaction = T.Transaction; public type Balance = T.Balance; public type TransferArgs = T.TransferArgs; + public type TransferFromArgs = T.TransferFromArgs; public type Mint = T.Mint; public type BurnArgs = T.BurnArgs; public type TransactionRequest = T.TransactionRequest; @@ -59,11 +60,20 @@ module { public type ArchivedTransaction = T.ArchivedTransaction; public type TransferResult = T.TransferResult; + public type TransferFromResult = T.TransferFromResult; public let MAX_TRANSACTIONS_IN_LEDGER = 2000; public let MAX_TRANSACTION_BYTES : Nat64 = 196; public let MAX_TRANSACTIONS_PER_REQUEST = 5000; + public type ApproveArgs = T.ApproveArgs; + + public type AllowanceArgs = T.AllowanceArgs; + + public type Allowance = T.Allowance; + + public type ApproveResult = T.ApproveResult; + /// Initialize a new ICRC-1 token public func init(args : T.InitArgs) : T.TokenData { let { @@ -82,13 +92,13 @@ module { var permitted_drift = 60_000_000_000; var transaction_window = 86_400_000_000_000; - switch(advanced_settings){ - case(?options) { + switch (advanced_settings) { + case (?options) { _burned_tokens := options.burned_tokens; permitted_drift := Nat64.toNat(options.permitted_drift); transaction_window := Nat64.toNat(options.transaction_window); }; - case(null) { }; + case (null) {}; }; if (not Account.validate(minting_account)) { @@ -97,13 +107,15 @@ module { let accounts : T.AccountBalances = StableTrieMap.new(); + let approve_accounts : T.ApproveBalances = StableTrieMap.new(); + var _minted_tokens = _burned_tokens; for ((i, (account, balance)) in Itertools.enumerate(initial_balances.vals())) { if (not Account.validate(account)) { Debug.trap( - "Invalid Account: Account at index " # debug_show i # " is invalid in 'initial_balances'", + "Invalid Account: Account at index " # debug_show i # " is invalid in 'initial_balances'" ); }; @@ -131,9 +143,12 @@ module { min_burn_amount; minting_account; accounts; + approve_accounts; metadata = Utils.init_metadata(args); supported_standards = Utils.init_standards(); transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER); + approve_transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER); + // approve_transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER); permitted_drift; transaction_window; archive = { @@ -209,6 +224,18 @@ module { Utils.get_balance(accounts, encoded_account); }; + /// Retrieve the balance of a given account and spender + public func get_allowance_of({ approve_accounts } : T.TokenData, account : T.Account, spender : Principal) : T.Allowance { + let encoded_account = Account.encode(account); + let spender_account = { + owner = spender; + subaccount = null; + }; + let encoded_account_spender = Account.encode(spender_account); + let gen_account = Utils.gen_account_from_two_account(encoded_account, encoded_account_spender); + Utils.get_allowance(approve_accounts, gen_account); + }; + /// Returns an array of standards supported by this token public func supported_standards(token : T.TokenData) : [T.SupportedStandard] { SB.toArray(token.supported_standards); @@ -238,11 +265,11 @@ module { }; let tx_kind = if (from == token.minting_account) { - #mint + #mint; } else if (args.to == token.minting_account) { - #burn + #burn; } else { - #transfer + #transfer; }; let tx_req = Utils.create_transfer_req(args, caller, tx_kind); @@ -254,17 +281,17 @@ module { case (#ok(_)) {}; }; - let { encoded; amount } = tx_req; + let { encoded; amount } = tx_req; // process transaction - switch(tx_req.kind){ - case(#mint){ + switch (tx_req.kind) { + case (#mint) { Utils.mint_balance(token, encoded.to, amount); }; - case(#burn){ + case (#burn) { Utils.burn_balance(token, encoded.from, amount); }; - case(#transfer){ + case (#transfer) { Utils.transfer_balance(token, tx_req); // burn fee @@ -283,6 +310,116 @@ module { #Ok(tx.index); }; + /// Transfers tokens from one account to another account (minting and burning included) + public func transfer_from( + token : T.TokenData, + args : T.TransferFromArgs, + caller : Principal, + ) : async* T.TransferFromResult { + + let tx_kind = #transfer_from; + + let tx_transfer_from_req = Utils.create_transfer_from_req(args, caller, tx_kind); + + switch (Transfer.validate_transfer_from_request(token, tx_transfer_from_req)) { + case (#err(errorType)) { + return #Err(errorType); + }; + case (#ok(_)) {}; + }; + + // icrc2 storage is complete, use normal transfer instead + let normal_transfer_args = { + from_subaccount = tx_transfer_from_req.from.subaccount; + to = tx_transfer_from_req.to; + amount = tx_transfer_from_req.amount; + fee = tx_transfer_from_req.fee; + memo = tx_transfer_from_req.memo; + created_at_time = tx_transfer_from_req.created_at_time; + }; + + let normal_tx_kind = #transfer; + let tx_req = Utils.create_transfer_req(normal_transfer_args, + args.from_subaccount.owner, + normal_tx_kind + ); + + switch (Transfer.validate_request(token, tx_req)) { + case (#err(errorType)) { + return #Err(errorType); + }; + case (#ok(_)) {}; + }; + + let { encoded; amount } = tx_req; + + // process transaction + Utils.transfer_balance(token, tx_req); + + // burn fee + Utils.burn_balance(token, encoded.from, token._fee); + + // decrease allowance + let caller_encoded = Account.encode({ + owner = caller; + subaccount = null; + }); + let allowance_key_account = Utils.gen_account_from_two_account(encoded.from, caller_encoded); + Utils.decrease_allowance(token, allowance_key_account, amount); + + // store transaction + let index = SB.size(token.transactions) + token.archive.stored_txs; + let tx = Utils.req_to_tx(tx_req, index); + SB.add(token.transactions, tx); + + // transfer transaction to archive if necessary + await* update_canister(token); + + #Ok(tx.index); + }; + /// Approve tokens from one account to another account + public func approve(token : T.TokenData, args : T.ApproveArgs, caller : Principal) : async* T.ApproveResult { + + let from = { + owner = caller; + subaccount = args.from_subaccount; + }; + + let tx_kind = #approve; + + let tx_req = Utils.create_approve_req(args, caller, tx_kind); + + switch (Transfer.validate_approve_request(token, tx_req)) { + case (#err(errorType)) { + return #Err(errorType); + }; + case (#ok(_)) {}; + }; + + let { encoded; amount } = tx_req; + + // process transaction + switch (tx_req.kind) { + case (#approve) { + Utils.approve(token, tx_req); + + // burn fee + // attention: fee is from caller account + Utils.burn_balance(token, encoded.from, token._fee); + }; + }; + + // store transaction + let index = SB.size(token.approve_transactions) + token.archive.stored_txs; + let tx = Utils.approve_req_to_tx(tx_req, index); + SB.add(token.approve_transactions, tx); + + // transfer transaction to archive if necessary + await* update_canister(token); + + #Ok(tx.index); + }; + /// Helper function to mint tokens with minimum args public func mint(token : T.TokenData, args : T.Mint, caller : Principal) : async* T.TransferResult { @@ -291,7 +428,7 @@ module { #GenericError { error_code = 401; message = "Unauthorized: Only the minting_account can mint tokens."; - }, + } ); }; @@ -343,13 +480,13 @@ module { let req_end = req.start + req.length; let tx_end = archive.stored_txs + SB.size(transactions); - var txs_in_canister: [T.Transaction] = []; - + var txs_in_canister : [T.Transaction] = []; + if (req.start < tx_end and req_end >= archive.stored_txs) { first_index := Nat.max(req.start, archive.stored_txs); let tx_start_index = (first_index - archive.stored_txs) : Nat; - txs_in_canister:= SB.slice(transactions, tx_start_index, req.length); + txs_in_canister := SB.slice(transactions, tx_start_index, req.length); }; let archived_range = if (req.start < archive.stored_txs) { @@ -414,7 +551,7 @@ module { }; let res = await archive.canister.append_transactions( - SB.toArray(transactions), + SB.toArray(transactions) ); switch (res) { diff --git a/src/ICRC1/stable/StableBuffer/lib.mo b/src/ICRC1/stable/StableBuffer/lib.mo new file mode 100644 index 0000000..6412c1d --- /dev/null +++ b/src/ICRC1/stable/StableBuffer/lib.mo @@ -0,0 +1,173 @@ +/// Generic, extensible buffers +/// +/// `StableBuffer` is adapted directly from https://github.com/dfinity/motoko-base/blob/master/src/Buffer.mo, +/// ripping all functions and instance variables out of the `Buffer` class in order to make a stable, persistent +/// buffer. +/// +/// Generic, mutable sequences that grow to accommodate arbitrary numbers of elements. +/// +/// `StableBuffer` provides extensible, mutable sequences of elements of type `X`. +/// that can be efficiently produced and consumed with imperative code. +/// A buffer object can be extended by a single element or the contents of another buffer object. +/// +/// When required, the current state of a buffer object can be converted to a fixed-size array of its elements. +/// +/// Buffers complement Motoko's non-extensible array types +/// (arrays do not support efficient extension, because the size of an array is +/// determined at construction and cannot be changed). + +import Prim "mo:⛔"; + +module { + + public type StableBuffer = { + initCapacity: Nat; + var count: Nat; + var elems: [var X]; + }; + + /// Initializes a buffer of given initial capacity. Note that this capacity is not realized until an element + /// is added to the buffer. + public func initPresized(initCapacity: Nat): StableBuffer = { + initCapacity = initCapacity; + var count = 0; + var elems = [var]; + }; + + /// Initializes a buffer of initial capacity 0. When the first element is added the size will grow to one + public func init(): StableBuffer = { + initCapacity = 0; + var count = 0; + var elems = [var]; + }; + + /// Adds a single element to the buffer. + public func add(buffer: StableBuffer, elem: X): () { + if (buffer.count == buffer.elems.size()) { + let size = + if (buffer.count == 0) { + if (buffer.initCapacity > 0) { buffer.initCapacity } else { 1 } + } else { + 2 * buffer.elems.size() + }; + let elems2 = Prim.Array_init(size, elem); + var i = 0; + label l loop { + if (i >= buffer.count) break l; + elems2[i] := buffer.elems[i]; + i += 1; + }; + buffer.elems := elems2; + }; + buffer.elems[buffer.count] := elem; + buffer.count += 1; + }; + + /// Removes the item that was inserted last and returns it or `null` if no + /// elements had been added to the buffer. + public func removeLast(buffer: StableBuffer) : ?X { + if (buffer.count == 0) { + null + } else { + buffer.count -= 1; + ?buffer.elems[buffer.count] + }; + }; + + /// Adds all elements in buffer `b` to buffer `a`. + public func append(a: StableBuffer, b : StableBuffer): () { + let i = vals(b); + loop { + switch (i.next()) { + case null return; + case (?x) { add(a, x) }; + }; + }; + }; + + /// Returns the count of elements in the buffer + public func size(buffer: StableBuffer) : Nat { buffer.count }; + + /// Resets the buffer. + public func clear(buffer: StableBuffer): () { + buffer.count := 0; + }; + + /// Returns a copy of this buffer. + public func clone(buffer: StableBuffer) : StableBuffer { + let c = initPresized(buffer.elems.size()); + var i = 0; + label l loop { + if (i >= buffer.count) break l; + add(c, buffer.elems[i]); + i += 1; + }; + c + }; + + /// Returns an `Iter` over the elements of this buffer. + public func vals(buffer: StableBuffer) : { next : () -> ?X } = object { + var pos = 0; + public func next() : ?X { + if (pos == buffer.count) { null } else { + let elem = ?buffer.elems[pos]; + pos += 1; + elem + } + } + }; + + /// Creates a Buffer from an Array + public func fromArray(xs: [X]): StableBuffer { + let ys: StableBuffer = initPresized(xs.size()); + for (x in xs.vals()) { + add(ys, x); + }; + + ys + }; + + /// Creates a new array containing this buffer's elements. + public func toArray(buffer: StableBuffer) : [X] = + // immutable clone of array + Prim.Array_tabulate( + buffer.count, + func(x : Nat) : X { buffer.elems[x] } + ); + + /// Creates a mutable array containing this buffer's elements. + public func toVarArray(buffer: StableBuffer) : [var X] { + if (buffer.count == 0) { [var] } else { + let a = Prim.Array_init(buffer.count, buffer.elems[0]); + var i = 0; + label l loop { + if (i >= buffer.count) break l; + a[i] := buffer.elems[i]; + i += 1; + }; + a + } + }; + + /// Gets the `i`-th element of this buffer. Traps if `i >= count`. Indexing is zero-based. + public func get(buffer: StableBuffer, i : Nat) : X { + assert(i < buffer.count); + buffer.elems[i] + }; + + /// Gets the `i`-th element of the buffer as an option. Returns `null` when `i >= count`. Indexing is zero-based. + public func getOpt(buffer: StableBuffer, i : Nat) : ?X { + if (i < buffer.count) { + ?buffer.elems[i] + } + else { + null + } + }; + + /// Overwrites the current value of the `i`-entry of this buffer with `elem`. Traps if the + /// index is out of bounds. Indexing is zero-based. + public func put(buffer: StableBuffer, i : Nat, elem : X) { + buffer.elems[i] := elem; + }; +} \ No newline at end of file diff --git a/src/ICRC1/stable/StableTrieMap/lib.mo b/src/ICRC1/stable/StableTrieMap/lib.mo new file mode 100644 index 0000000..ce5756b --- /dev/null +++ b/src/ICRC1/stable/StableTrieMap/lib.mo @@ -0,0 +1,195 @@ +import Trie "mo:base/Trie"; +import Hash "mo:base/Hash"; +import Iter "mo:base/Iter"; +import List "mo:base/List"; + +module { + public type StableTrieMap = { + var trie : Trie.Trie; + var _size : Nat; + + }; + + public func new() : StableTrieMap { + { + var trie = Trie.empty(); + var _size = 0; + }; + }; + + public func size(self : StableTrieMap) : Nat { + self._size; + }; + + public func replace( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + key : K, + val : V, + ) : ?V { + let keyObj = { key; hash = keyHash(key) }; + + let (updatedMap, prevVal) = Trie.put(self.trie, keyObj, keyEq, val); + + self.trie := updatedMap; + + switch (prevVal) { + case (null) { self._size += 1 }; + case (_) {}; + }; + + prevVal; + }; + + public let put = func( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + key : K, + val : V, + ) { ignore replace(self, keyEq, keyHash, key, val) }; + + public func get( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + key : K, + ) : ?V { + let keyObj = { key; hash = keyHash(key) }; + Trie.find(self.trie, keyObj, keyEq); + }; + + public func remove( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + key : K, + ) : ?V { + let keyObj = { key; hash = keyHash(key) }; + + let (updatedMap, prevVal) = Trie.remove(self.trie, keyObj, keyEq); + self.trie := updatedMap; + + switch (prevVal) { + case (?_) { self._size -= 1 }; + case (null) {}; + }; + + prevVal; + }; + + public func delete( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + key : K, + ) { + ignore remove(self, keyEq, keyHash, key); + }; + + public func entries(self : StableTrieMap) : Iter.Iter<(K, V)> { + object { + var stack = ?(self.trie, null) : List.List>; + + public func next() : ?(K, V) { + switch stack { + case null { null }; + case (?(trie, stack2)) { + switch trie { + case (#empty) { + stack := stack2; + next(); + }; + case (#leaf({ keyvals = null })) { + stack := stack2; + next(); + }; + case (#leaf({ size = c; keyvals = ?((k, v), kvs) })) { + stack := ?(#leaf({ size = c -1; keyvals = kvs }), stack2); + ?(k.key, v); + }; + case (#branch(br)) { + stack := ?(br.left, ?(br.right, stack2)); + next(); + }; + }; + }; + }; + }; + }; + }; + + public func keys(self : StableTrieMap) : Iter.Iter { + Iter.map<(K, V), K>(entries(self), func((key, _)) { key }); + }; + + public func vals(self : StableTrieMap) : Iter.Iter { + Iter.map<(K, V), V>(entries(self), func((_, val)) { val }); + }; + + public func fromEntries( + _entries : Iter.Iter<(K, V)>, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + ) : StableTrieMap { + let triemap = new(); + + for ((key, val) in _entries) { + put(triemap, keyEq, keyHash, key, val); + }; + + triemap; + }; + + public func clone( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + ) : StableTrieMap { + fromEntries(entries(self), keyEq, keyHash); + }; + + public func clear(self : StableTrieMap) { + self.trie := Trie.empty(); + self._size := 0; + }; + + // additional helper functions + public func isEmpty(self : StableTrieMap) : Bool { + self._size == 0; + }; + + public func containsKey( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + key : K, + ) : Bool { + switch (get(self, keyEq, keyHash, key)) { + case (?v) true; + case (_) false; + }; + }; + + /// Adds the given `defaultVal` if the key does not exist in the map + /// and updates the value of an existing key + public func putOrUpdate( + self : StableTrieMap, + keyEq : (K, K) -> Bool, + keyHash : (K) -> Hash.Hash, + key : K, + defaultVal : V, + update : (V) -> V, + ) { + switch (get(self, keyEq, keyHash, key)) { + case (?val) { + put(self, keyEq, keyHash, key, update(val)); + }; + case (_) { + put(self, keyEq, keyHash, key, defaultVal); + }; + }; + }; + +}; \ No newline at end of file diff --git a/tests/ICRC1/Account.Test.mo b/tests/ICRC1/Account.Test.mo index 9198ddd..56242fe 100644 --- a/tests/ICRC1/Account.Test.mo +++ b/tests/ICRC1/Account.Test.mo @@ -3,7 +3,7 @@ import Debug "mo:base/Debug"; import Iter "mo:base/Iter"; import Principal "mo:base/Principal"; -import Itertools "mo:itertools/Iter"; +import Itertools "../../src/ICRC1/itertools/Iter"; import Account "../../src/ICRC1/Account"; import ActorSpec "../utils/ActorSpec"; diff --git a/tests/ICRC1/ICRC1.ActorTest.mo b/tests/ICRC1/ICRC1.ActorTest.mo index a26cd9c..5110f2c 100644 --- a/tests/ICRC1/ICRC1.ActorTest.mo +++ b/tests/ICRC1/ICRC1.ActorTest.mo @@ -5,8 +5,8 @@ import Nat "mo:base/Nat"; import Nat8 "mo:base/Nat8"; import Principal "mo:base/Principal"; -import Itertools "mo:itertools/Iter"; -import StableBuffer "mo:StableBuffer/StableBuffer"; +import Itertools "../../src/ICRC1/itertools/Iter"; +import StableBuffer "../../src/ICRC1/stable/StableBuffer"; import ActorSpec "../utils/ActorSpec"; @@ -237,7 +237,7 @@ module { token.max_supply == args.max_supply, token.minting_account == args.minting_account, - SB.toArray(token.supported_standards) == [U.default_standard], + SB.toArray(token.supported_standards) == [U.default_standard, U.icrc2_standard], SB.size(token.transactions) == 0, ]); }, @@ -370,7 +370,7 @@ module { ICRC1.supported_standards(token) == [{ name = "ICRC-1"; url = "https://github.com/dfinity/ICRC-1"; - }], + }, U.icrc2_standard], ); }, ), @@ -562,6 +562,121 @@ module { ), ], ), + describe( + "approve()", + [ + it( + "Alice approve llowance to canister account", + do { + let args = default_token_args; + let token = ICRC1.init(args); + Debug.print(debug_show("expect ap:", 1200 * (10 ** Nat8.toNat(token.decimals)))); + + let mint_args = { + to = user1; + amount = 11200 * (10 ** Nat8.toNat(token.decimals)); + memo = null; + created_at_time = null; + }; + + ignore await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + let approve_args : T.ApproveArgs = { + from_subaccount = null; + spender = canister.owner; + amount = 1200 * (10 ** Nat8.toNat(token.decimals)); + fee = ?token._fee; + memo = null; + created_at_time = null; + expires_at = null; + }; + + let res = await* ICRC1.approve( + token, + approve_args, + user1.owner, + ); + + assertAllTrue([ + res == #Ok(0), + ICRC1.get_allowance_of(token, user1, canister.owner).allowance == 1200 * (10 ** Nat8.toNat(token.decimals)), + token._burned_tokens == ICRC1.balance_from_float(token, 5), + ICRC1.balance_of(token, user1) == ICRC1.balance_from_float(token, 11195), + ICRC1.total_supply(token) == ICRC1.balance_from_float(token, 11195), + ]); + }, + ), + ], + ), + describe( + "transfer_from()", + [ + it( + "Transfer from Alice account", + do { + let args = default_token_args; + let token = ICRC1.init(args); + + let mint_args = { + to = user1; + amount = 200 * (10 ** Nat8.toNat(token.decimals)); + memo = null; + created_at_time = null; + }; + + ignore await* ICRC1.mint( + token, + mint_args, + args.minting_account.owner, + ); + + let approve_args : T.ApproveArgs = { + from_subaccount = user1.subaccount; + spender = canister.owner; + amount = 60 * (10 ** Nat8.toNat(token.decimals)); + fee = ?token._fee; + memo = null; + created_at_time = null; + expires_at = null; + }; + + ignore await* ICRC1.approve( + token, + approve_args, + user1.owner, + ); + + let transfer_from_args : T.TransferFromArgs = { + from_subaccount = user1; + to = user2; + amount = 50 * (10 ** Nat8.toNat(token.decimals)); + fee = ?token._fee; + memo = null; + created_at_time = null; + }; + + let res = await* ICRC1.transfer_from( + token, + transfer_from_args, + canister.owner, + ); + + assertAllTrue([ + res == #Ok(1), + ICRC1.get_allowance_of(token, user1, canister.owner).allowance == 10 * (10 ** Nat8.toNat(token.decimals)), + ICRC1.balance_of(token, user1) == ICRC1.balance_from_float(token, 140), + token._burned_tokens == ICRC1.balance_from_float(token, 10), + ICRC1.balance_of(token, user2) == ICRC1.balance_from_float(token, 50), + ICRC1.total_supply(token) == ICRC1.balance_from_float(token, 190), + ]); + }, + ), + ], + ), describe( "Internal Archive Testing",