Skip to content

Commit e2ac704

Browse files
committed
Merge branch 'FI-733' into 'master'
feat(FI-733) [icrc index] add list_subaccounts See merge request dfinity-lab/public/ic!12548
2 parents b9e9f4a + 3c25f37 commit e2ac704

File tree

4 files changed

+169
-8
lines changed

4 files changed

+169
-8
lines changed

rs/rosetta-api/icrc1/index-ng/index-ng.did

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,15 @@ type GetTransactionsResult = variant {
9494
Err : GetTransactionsErr;
9595
};
9696

97+
type ListSubaccountsArgs = record {
98+
owner: principal;
99+
start: opt SubAccount;
100+
};
101+
97102
service : (index_arg: IndexArg) -> {
98103
get_account_transactions : (GetAccountTransactionsArgs) -> (GetTransactionsResult);
99104
get_blocks : (GetBlocksRequest) -> (GetBlocksResponse) query;
100105
icrc1_balance_of : (Account) -> (Tokens) query;
101106
ledger_id : () -> (principal) query;
107+
list_subaccounts : (ListSubaccountsArgs) -> (vec SubAccount) query;
102108
}

rs/rosetta-api/icrc1/index-ng/src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use candid::{CandidType, Deserialize, Nat, Principal};
2-
use icrc_ledger_types::icrc1::account::Account;
2+
use icrc_ledger_types::icrc1::account::{Account, Subaccount};
33
use icrc_ledger_types::icrc1::transfer::BlockIndex;
44
use icrc_ledger_types::icrc3::blocks::GenericBlock;
55
use icrc_ledger_types::icrc3::transactions::Transaction;
66

7+
/// The maximum number of blocks to return in a single [get_blocks] request.
8+
pub const DEFAULT_MAX_BLOCKS_PER_RESPONSE: u64 = 2000;
9+
710
#[derive(CandidType, Debug, Deserialize)]
811
pub enum IndexArg {
912
InitArg(InitArg),
@@ -56,3 +59,13 @@ pub struct GetAccountTransactionsError {
5659

5760
pub type GetAccountTransactionsResult =
5861
Result<GetAccountTransactionsResponse, GetAccountTransactionsError>;
62+
63+
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
64+
pub struct ListSubaccountsArgs {
65+
pub owner: Principal,
66+
// The last subaccount seen by the client for the given principal.
67+
// This subaccount is excluded in the result.
68+
// If None then the results will start from the first
69+
// in natural order.
70+
pub start: Option<Subaccount>,
71+
}

rs/rosetta-api/icrc1/index-ng/src/main.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use ic_icrc1::blocks::{encoded_block_to_generic_block, generic_block_to_encoded_
77
use ic_icrc1::{Block, Operation};
88
use ic_icrc1_index_ng::{
99
GetAccountTransactionsArgs, GetAccountTransactionsResponse, GetAccountTransactionsResult,
10-
IndexArg, TransactionWithId,
10+
IndexArg, ListSubaccountsArgs, TransactionWithId, DEFAULT_MAX_BLOCKS_PER_RESPONSE,
1111
};
1212
use ic_ledger_core::block::{BlockIndex as BlockIndex64, BlockType, EncodedBlock};
1313
use ic_stable_structures::memory_manager::{MemoryId, VirtualMemory};
@@ -16,7 +16,7 @@ use ic_stable_structures::{
1616
memory_manager::MemoryManager, BoundedStorable, DefaultMemoryImpl, StableBTreeMap, StableCell,
1717
StableLog, Storable,
1818
};
19-
use icrc_ledger_types::icrc1::account::Account;
19+
use icrc_ledger_types::icrc1::account::{Account, Subaccount};
2020
use icrc_ledger_types::icrc3::archive::{ArchivedRange, QueryBlockArchiveFn};
2121
use icrc_ledger_types::icrc3::blocks::{
2222
BlockRange, GenericBlock, GetBlocksRequest, GetBlocksResponse,
@@ -30,11 +30,9 @@ use std::cell::RefCell;
3030
use std::cmp::Reverse;
3131
use std::convert::TryFrom;
3232
use std::hash::Hash;
33+
use std::ops::Bound::{Excluded, Included};
3334
use std::time::Duration;
3435

35-
/// The maximum number of blocks to return in a single [get_blocks] request.
36-
const DEFAULT_MAX_BLOCKS_PER_RESPONSE: u64 = 2000;
37-
3836
const STATE_MEMORY_ID: MemoryId = MemoryId::new(0);
3937
const BLOCK_LOG_INDEX_MEMORY_ID: MemoryId = MemoryId::new(1);
4038
const BLOCK_LOG_DATA_MEMORY_ID: MemoryId = MemoryId::new(2);
@@ -602,6 +600,33 @@ fn icrc1_balance_of(account: Account) -> Nat {
602600
get_balance(account).into()
603601
}
604602

603+
#[query]
604+
#[candid_method(query)]
605+
fn list_subaccounts(args: ListSubaccountsArgs) -> Vec<Subaccount> {
606+
let start_key = balance_key(Account {
607+
owner: args.owner,
608+
subaccount: args.start,
609+
});
610+
let end_key = balance_key(Account {
611+
owner: args.owner,
612+
subaccount: Some([u8::MAX; 32]),
613+
});
614+
let range = (
615+
if args.start.is_none() {
616+
Included(start_key)
617+
} else {
618+
Excluded(start_key)
619+
},
620+
Included(end_key),
621+
);
622+
with_account_data(|data| {
623+
data.range(range)
624+
.take(DEFAULT_MAX_BLOCKS_PER_RESPONSE as usize)
625+
.map(|((_, (_, subaccount)), _)| subaccount)
626+
.collect()
627+
})
628+
}
629+
605630
fn main() {}
606631

607632
#[cfg(test)]

rs/rosetta-api/icrc1/index-ng/tests/tests.rs

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ use candid::{Decode, Encode, Nat};
22
use ic_base_types::{CanisterId, PrincipalId};
33
use ic_icrc1_index_ng::{
44
GetAccountTransactionsArgs, GetAccountTransactionsResponse, GetAccountTransactionsResult,
5-
GetBlocksResponse, IndexArg, InitArg as IndexInitArg, TransactionWithId,
5+
GetBlocksResponse, IndexArg, InitArg as IndexInitArg, ListSubaccountsArgs, TransactionWithId,
6+
DEFAULT_MAX_BLOCKS_PER_RESPONSE,
67
};
78
use ic_icrc1_ledger::{InitArgs as LedgerInitArgs, LedgerArgument};
89
use ic_icrc1_test_utils::{valid_transactions_strategy, CallerTransferArg};
910
use ic_ledger_canister_core::archive::ArchiveOptions;
1011
use ic_state_machine_tests::StateMachine;
1112
use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue as Value;
12-
use icrc_ledger_types::icrc1::account::Account;
13+
use icrc_ledger_types::icrc1::account::{Account, Subaccount};
1314
use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferArg, TransferError};
1415
use icrc_ledger_types::icrc3::blocks::{BlockRange, GenericBlock, GetBlocksRequest};
1516
use icrc_ledger_types::icrc3::transactions::{Mint, Transaction, Transfer};
@@ -229,6 +230,30 @@ fn get_account_transactions(
229230
.expect("Failed to perform GetAccountTransactionsArgs")
230231
}
231232

233+
fn list_subaccounts(
234+
env: &StateMachine,
235+
index: CanisterId,
236+
principal: PrincipalId,
237+
start: Option<Subaccount>,
238+
) -> Vec<Subaccount> {
239+
Decode!(
240+
&env.execute_ingress_as(
241+
principal,
242+
index,
243+
"list_subaccounts",
244+
Encode!(&ListSubaccountsArgs {
245+
owner: principal.into(),
246+
start,
247+
})
248+
.unwrap()
249+
)
250+
.expect("failed to list_subaccounts")
251+
.bytes(),
252+
Vec<Subaccount>
253+
)
254+
.expect("failed to decode list_subaccounts response")
255+
}
256+
232257
// Assert that the index canister contains the same blocks as the ledger
233258
fn assert_ledger_index_parity(env: &StateMachine, ledger_id: CanisterId, index_id: CanisterId) {
234259
let ledger_blocks = icrc1_get_blocks(env, ledger_id);
@@ -617,3 +642,95 @@ fn test_icrc1_balance_of() {
617642
)
618643
.unwrap();
619644
}
645+
646+
#[test]
647+
fn test_list_subaccounts() {
648+
// For this test, we add minting operations for some principals:
649+
// - The principal 1 has one account with the last possible
650+
// subaccount.
651+
// - The principal 2 has a number of subaccounts equals to
652+
// two times the DEFAULT_MAX_BLOCKS_PER_RESPONSE. Therefore fetching
653+
// its subaccounts will trigger pagination.
654+
// - The principal 3 has one account with the first possible
655+
// subaccount.
656+
// - The principal 4 has one account with the default subaccount,
657+
// which should map to [0;32] in the index.
658+
659+
let account_1 = Account {
660+
owner: PrincipalId::new_user_test_id(1).into(),
661+
subaccount: Some([u8::MAX; 32]),
662+
};
663+
let accounts_2: Vec<_> = (0..(DEFAULT_MAX_BLOCKS_PER_RESPONSE * 2))
664+
.map(|i| account(2, i as u128))
665+
.collect();
666+
let account_3 = account(3, 0);
667+
let account_4 = Account {
668+
owner: PrincipalId::new_user_test_id(4).into(),
669+
subaccount: None,
670+
};
671+
672+
let mut initial_balances: Vec<_> = vec![
673+
(account_1, 10_000),
674+
(account_3, 10_000),
675+
(account_4, 40_000),
676+
];
677+
initial_balances.extend(accounts_2.iter().map(|account| (*account, 10_000)));
678+
679+
let env = &StateMachine::new();
680+
let ledger_id = install_ledger(env, initial_balances, default_archive_options());
681+
let index_id = install_index(env, ledger_id);
682+
683+
trigger_heartbeat(env);
684+
685+
// list account_1.owner subaccounts when no starting subaccount is specified
686+
assert_eq!(
687+
vec![*account_1.effective_subaccount()],
688+
list_subaccounts(env, index_id, PrincipalId(account_1.owner), None)
689+
);
690+
691+
// list account_3.owner subaccounts when no starting subaccount is specified
692+
assert_eq!(
693+
vec![*account_3.effective_subaccount()],
694+
list_subaccounts(env, index_id, PrincipalId(account_3.owner), None)
695+
);
696+
697+
// list account_3.owner subaccounts when an existing starting subaccount is specified but no subaccount is in that range
698+
assert!(list_subaccounts(
699+
env,
700+
index_id,
701+
PrincipalId(account_3.owner),
702+
Some(*account(3, 1).effective_subaccount())
703+
)
704+
.is_empty());
705+
706+
// list acccount_4.owner subaccounts should return the default subaccount
707+
// mapped to [0;32]
708+
assert_eq!(
709+
vec![[0; 32]],
710+
list_subaccounts(env, index_id, PrincipalId(account_4.owner), None)
711+
);
712+
713+
// account_2.owner should have two batches of subaccounts
714+
let principal_2 = accounts_2.get(0).unwrap().owner;
715+
let batch_1 = list_subaccounts(env, index_id, PrincipalId(principal_2), None);
716+
let expected_batch_1: Vec<_> = accounts_2
717+
.iter()
718+
.take(DEFAULT_MAX_BLOCKS_PER_RESPONSE as usize)
719+
.map(|account| *account.effective_subaccount())
720+
.collect();
721+
assert_eq!(expected_batch_1, batch_1);
722+
723+
let batch_2 = list_subaccounts(
724+
env,
725+
index_id,
726+
PrincipalId(principal_2),
727+
Some(*batch_1.last().unwrap()),
728+
);
729+
let expected_batch_2: Vec<_> = accounts_2
730+
.iter()
731+
.skip(DEFAULT_MAX_BLOCKS_PER_RESPONSE as usize)
732+
.take(DEFAULT_MAX_BLOCKS_PER_RESPONSE as usize)
733+
.map(|account| *account.effective_subaccount())
734+
.collect();
735+
assert_eq!(expected_batch_2, batch_2);
736+
}

0 commit comments

Comments
 (0)