diff --git a/src/ICRC1/Canisters/Archive.mo b/src/ICRC1/Canisters/Archive.mo index 74f7022..cbcc185 100644 --- a/src/ICRC1/Canisters/Archive.mo +++ b/src/ICRC1/Canisters/Archive.mo @@ -49,7 +49,10 @@ shared ({ caller = ledger_canister_id }) actor class Archive() : async T.Archive return #err("Unauthorized Access: Only the ledger canister can access this archive canister"); }; - var txs_iter = txs.vals(); + // Ensure no pre-registered transaction is stored + let last_tx_index = get_last_tx_index(); + let filtered_txs = Array.filter(txs, func tx = tx.index > last_tx_index); + var txs_iter = filtered_txs.vals(); if (trailing_txs > 0) { let last_bucket = StableTrieMap.get( @@ -65,16 +68,16 @@ shared ({ caller = ledger_canister_id }) actor class Archive() : async T.Archive Itertools.take( Itertools.chain( last_bucket.vals(), - Iter.map(txs.vals(), store_tx), + Iter.map(filtered_txs.vals(), store_tx), ), BUCKET_SIZE, - ), + ) ); if (new_bucket.size() == BUCKET_SIZE) { let offset = (BUCKET_SIZE - last_bucket.size()) : Nat; - txs_iter := Itertools.fromArraySlice(txs, offset, txs.size()); + txs_iter := Itertools.fromArraySlice(filtered_txs, offset, txs.size()); } else { txs_iter := Itertools.empty(); }; @@ -92,6 +95,27 @@ shared ({ caller = ledger_canister_id }) actor class Archive() : async T.Archive #ok(); }; + func get_last_tx_index() : Int { + if (total_txs() == 0) return -1; + + let bucket_index = if (trailing_txs > 0) filled_buckets else Nat.max(filled_buckets - 1, 0); + let last_bucket_opt = StableTrieMap.get( + txStore, + Nat.equal, + U.hash, + bucket_index, + ); + + let last_bucket = switch (last_bucket_opt) { + case (?last_bucket) last_bucket; + case (null) Debug.trap("Unexpected Error: Last Bucket not found"); + }; + + if (last_bucket.size() == 0) Debug.trap("Unexpected Error: Last Bucket is not filled"); + + get_tx(last_bucket[last_bucket.size() - 1]).index; + }; + func total_txs() : Nat { (filled_buckets * BUCKET_SIZE) + trailing_txs; }; @@ -160,7 +184,7 @@ shared ({ caller = ledger_canister_id }) actor class Archive() : async T.Archive Iter.map( Itertools.take(iter, MAX_TRANSACTIONS_PER_REQUEST), get_tx, - ), + ) ); { transactions }; diff --git a/tests/ICRC1/Archive.ActorTest.mo b/tests/ICRC1/Archive.ActorTest.mo index 477197a..c8f2560 100644 --- a/tests/ICRC1/Archive.ActorTest.mo +++ b/tests/ICRC1/Archive.ActorTest.mo @@ -7,6 +7,7 @@ import Float "mo:base/Float"; import Nat64 "mo:base/Nat64"; import Principal "mo:base/Principal"; import EC "mo:base/ExperimentalCycles"; +import Buffer "mo:base/Buffer"; import Archive "../../src/ICRC1/Canisters/Archive"; import T "../../src/ICRC1/Types"; @@ -55,7 +56,7 @@ module { func create_canister_and_add_cycles(n : Float) { EC.add( - CREATE_CANISTER + Int.abs(Float.toInt(n * 1_000_000_000_000)), + CREATE_CANISTER + Int.abs(Float.toInt(n * 1_000_000_000_000)) ); }; @@ -78,6 +79,29 @@ module { ]); }, ), + it( + "append_transactions() doesn't duplicate", + do { + create_canister_and_add_cycles(0.1); + let archive = await Archive.Archive(); + + let txs = new_txs(1000); + ignore await archive.append_transactions(txs); + + // added 1 extra transaction that is not duplicated, and should be appended + let duplicated_txs = new_txs(1001); + ignore await archive.append_transactions(duplicated_txs); + + // also testing when tx falls inside the bucket + let duplicated_and_inside_bucket_txs = new_txs(1501); + + assertAllTrue([ + (await archive.total_transactions()) == 1001, + (await archive.append_transactions(duplicated_and_inside_bucket_txs)) == #ok(), + (await archive.total_transactions()) == 1501, + ]); + }, + ), it( "get_transaction()", do {