Skip to content

Commit 608113a

Browse files
committed
feat(icp_index): FI-1219: Add SettledTransaction with TimeStamp field to ICP index
1 parent bb0ac2e commit 608113a

File tree

6 files changed

+101
-42
lines changed

6 files changed

+101
-42
lines changed

rs/rosetta-api/icp_ledger/index/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ DEPENDENCIES = [
2020
"@crate_index//:serde",
2121
"@crate_index//:ic-stable-structures",
2222
"@crate_index//:ic-metrics-encoder",
23+
"@crate_index//:serde_bytes",
2324
"@crate_index//:serde_json",
2425
]
2526

@@ -32,7 +33,6 @@ DEV_DEPENDENCIES = [
3233
"@crate_index//:assert_matches",
3334
"@crate_index//:candid_parser",
3435
"@crate_index//:proptest",
35-
"@crate_index//:serde_bytes",
3636
]
3737

3838
MACRO_DEPENDENCIES = [

rs/rosetta-api/icp_ledger/index/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ serde = { workspace = true }
2626
ic-canisters-http-types = { path = "../../../rust_canisters/http_types" }
2727
ic-metrics-encoder = "1.1"
2828
ic-canister-log = { path = "../../../rust_canisters/canister_log" }
29+
serde_bytes = { workspace = true }
2930
serde_json = { workspace = true }
3031
ic-icrc1-index-ng = {path ="../../icrc1/index-ng"}
3132

@@ -37,5 +38,4 @@ ic-state-machine-tests = { path = "../../../state_machine_tests" }
3738
ic-test-utilities-load-wasm = { path = "../../../test_utilities/load_wasm" }
3839
ic-icrc1 = { path = "../../icrc1" }
3940
ic-icp-index = { path = "./" }
40-
serde_bytes = { workspace = true }
4141

rs/rosetta-api/icp_ledger/index/index.did

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type GetAccountTransactionsArgs = record {
1616
type GetAccountIdentifierTransactionsError = record { message : text };
1717
type GetAccountIdentifierTransactionsResponse = record {
1818
balance : nat64;
19-
transactions : vec TransactionWithId;
19+
transactions : vec SettledTransactionWithId;
2020
oldest_tx_id : opt nat64;
2121
};
2222
type GetBlocksRequest = record { start : nat; length : nat };
@@ -53,13 +53,14 @@ type GetAccountIdentifierTransactionsResult = variant {
5353
type Status = record { num_blocks_synced : nat64 };
5454
type TimeStamp = record { timestamp_nanos : nat64 };
5555
type Tokens = record { e8s : nat64 };
56-
type Transaction = record {
56+
type SettledTransaction = record {
5757
memo : nat64;
5858
icrc1_memo : opt vec nat8;
5959
operation : Operation;
6060
created_at_time : opt TimeStamp;
61+
timestamp : TimeStamp;
6162
};
62-
type TransactionWithId = record { id : nat64; transaction : Transaction };
63+
type SettledTransactionWithId = record { id : nat64; transaction : SettledTransaction };
6364
service : (InitArg) -> {
6465
get_account_identifier_balance : (text) -> (nat64) query;
6566
get_account_identifier_transactions : (

rs/rosetta-api/icp_ledger/index/src/lib.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use candid::{CandidType, Deserialize, Principal};
22
use ic_ledger_core::block::EncodedBlock;
3-
use icp_ledger::{AccountIdentifier, BlockIndex, Transaction};
3+
use ic_ledger_core::timestamp::TimeStamp;
4+
use icp_ledger::{AccountIdentifier, Block, BlockIndex, Memo, Operation};
5+
use serde_bytes::ByteBuf;
6+
47
pub mod logs;
58

69
#[derive(CandidType, Debug, Deserialize)]
@@ -29,15 +32,39 @@ pub struct GetAccountIdentifierTransactionsArgs {
2932
}
3033

3134
#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
32-
pub struct TransactionWithId {
35+
pub struct SettledTransaction {
36+
pub operation: Operation,
37+
pub memo: Memo,
38+
/// The time this transaction was created on the client side.
39+
pub created_at_time: Option<TimeStamp>,
40+
#[serde(skip_serializing_if = "Option::is_none")]
41+
pub icrc1_memo: Option<ByteBuf>,
42+
/// The time the block with this transaction was created.
43+
pub timestamp: TimeStamp,
44+
}
45+
46+
impl From<Block> for SettledTransaction {
47+
fn from(block: Block) -> Self {
48+
SettledTransaction {
49+
operation: block.transaction.operation,
50+
memo: block.transaction.memo,
51+
created_at_time: block.transaction.created_at_time,
52+
icrc1_memo: block.transaction.icrc1_memo,
53+
timestamp: block.timestamp,
54+
}
55+
}
56+
}
57+
58+
#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
59+
pub struct SettledTransactionWithId {
3360
pub id: BlockIndex,
34-
pub transaction: Transaction,
61+
pub transaction: SettledTransaction,
3562
}
3663

3764
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
3865
pub struct GetAccountIdentifierTransactionsResponse {
3966
pub balance: u64,
40-
pub transactions: Vec<TransactionWithId>,
67+
pub transactions: Vec<SettledTransactionWithId>,
4168
// The txid of the oldest transaction the account_identifier has
4269
pub oldest_tx_id: Option<BlockIndex>,
4370
}

rs/rosetta-api/icp_ledger/index/src/main.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use ic_icp_index::logs::{P0, P1};
77
use ic_icp_index::{
88
GetAccountIdentifierTransactionsArgs, GetAccountIdentifierTransactionsResponse,
99
GetAccountIdentifierTransactionsResult, GetAccountTransactionsResult, InitArg, Log, LogEntry,
10-
Priority, Status, TransactionWithId,
10+
Priority, SettledTransaction, SettledTransactionWithId, Status,
1111
};
1212
use ic_icrc1_index_ng::GetAccountTransactionsArgs;
1313
use ic_ledger_core::block::{BlockType, EncodedBlock};
@@ -593,7 +593,7 @@ fn get_account_identifier_transactions(
593593
// TODO: deal with the user setting start to u64::MAX
594594
let start = arg.start.map_or(u64::MAX, |n| n);
595595
let key = account_identifier_block_ids_key(arg.account_identifier, start);
596-
let mut transactions = vec![];
596+
let mut settled_transactions = vec![];
597597
let indices = with_account_identifier_block_ids(|account_identifier_block_ids| {
598598
account_identifier_block_ids
599599
.range(key..)
@@ -613,21 +613,22 @@ fn get_account_identifier_transactions(
613613
));
614614
})
615615
});
616-
let transaction = decode_encoded_block(id, block.into())
616+
let settled_transaction = SettledTransaction::from(decode_encoded_block(id, block.into())
617617
.unwrap_or_else(|_| {
618618
ic_cdk::api::trap(&format!(
619-
"Block {} not found in the block log, account_identifier blocks map is corrupted!",id
620-
));
621-
})
622-
.transaction;
623-
let transaction_with_idx = TransactionWithId { id, transaction };
624-
transactions.push(transaction_with_idx);
619+
"Block {} not found in the block log, account_identifier blocks map is corrupted!",id))
620+
}));
621+
let transaction_with_idx = SettledTransactionWithId {
622+
id,
623+
transaction: settled_transaction,
624+
};
625+
settled_transactions.push(transaction_with_idx);
625626
}
626627
let oldest_tx_id = get_oldest_tx_id(arg.account_identifier);
627628
let balance = get_balance(arg.account_identifier);
628629
Ok(GetAccountIdentifierTransactionsResponse {
629630
balance,
630-
transactions,
631+
transactions: settled_transactions,
631632
oldest_tx_id,
632633
})
633634
}

rs/rosetta-api/icp_ledger/index/tests/tests.rs

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use candid::{Decode, Encode, Nat};
22
use ic_base_types::{CanisterId, PrincipalId};
33
use ic_icp_index::{
44
GetAccountIdentifierTransactionsArgs, GetAccountIdentifierTransactionsResponse,
5-
GetAccountIdentifierTransactionsResult, Status, TransactionWithId,
5+
GetAccountIdentifierTransactionsResult, SettledTransaction, SettledTransactionWithId, Status,
66
};
77
use ic_icrc1_index_ng::GetAccountTransactionsArgs;
88
use ic_ledger_canister_core::archive::ArchiveOptions;
@@ -13,7 +13,7 @@ use ic_state_machine_tests::StateMachine;
1313
use icp_ledger::{
1414
AccountIdentifier, GetBlocksArgs, QueryEncodedBlocksResponse, MAX_BLOCKS_PER_REQUEST,
1515
};
16-
use icp_ledger::{FeatureFlags, LedgerCanisterInitPayload, Memo, Operation, Transaction};
16+
use icp_ledger::{FeatureFlags, LedgerCanisterInitPayload, Memo, Operation};
1717
use icrc_ledger_types::icrc1::account::Account;
1818
use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferArg, TransferError};
1919
use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError};
@@ -323,6 +323,8 @@ fn get_account_identifier_transactions(
323323
accountidentifier_txs
324324
}
325325

326+
const SYNC_STEP_SECONDS: Duration = Duration::from_secs(60);
327+
326328
// Helper function that calls tick on env until either
327329
// the index canister has synced all the blocks up to the
328330
// last one in the ledger or enough attempts passed and therefore
@@ -332,7 +334,7 @@ fn wait_until_sync_is_completed(env: &StateMachine, index_id: CanisterId, ledger
332334
let mut num_blocks_synced = u64::MAX;
333335
let mut chain_length = u64::MAX;
334336
for _i in 0..MAX_ATTEMPTS {
335-
env.advance_time(Duration::from_secs(60));
337+
env.advance_time(SYNC_STEP_SECONDS);
336338
env.tick();
337339
num_blocks_synced = status(env, index_id).num_blocks_synced;
338340
chain_length = icp_get_blocks(env, ledger_id).len() as u64;
@@ -344,22 +346,22 @@ fn wait_until_sync_is_completed(env: &StateMachine, index_id: CanisterId, ledger
344346
}
345347

346348
#[track_caller]
347-
fn assert_tx_eq(tx1: &Transaction, tx2: &Transaction) {
349+
fn assert_tx_eq(tx1: &SettledTransaction, tx2: &SettledTransaction) {
348350
assert_eq!(tx1.operation, tx2.operation);
349351
assert_eq!(tx1.memo, tx2.memo);
350-
assert_eq!(tx1.operation, tx2.operation);
351-
assert_eq!(tx1.operation, tx2.operation);
352+
assert_eq!(tx1.icrc1_memo, tx2.icrc1_memo);
353+
assert_eq!(tx1.timestamp, tx2.timestamp);
352354
}
353355

354356
// checks that two txs are equal minus the fields set by the ledger (e.g. timestamp)
355357
#[track_caller]
356-
fn assert_tx_with_id_eq(tx1: &TransactionWithId, tx2: &TransactionWithId) {
358+
fn assert_tx_with_id_eq(tx1: &SettledTransactionWithId, tx2: &SettledTransactionWithId) {
357359
assert_eq!(tx1.id, tx2.id, "id");
358360
assert_tx_eq(&tx1.transaction, &tx2.transaction);
359361
}
360362

361363
#[track_caller]
362-
fn assert_txs_with_id_eq(txs1: Vec<TransactionWithId>, txs2: Vec<TransactionWithId>) {
364+
fn assert_txs_with_id_eq(txs1: Vec<SettledTransactionWithId>, txs2: Vec<SettledTransactionWithId>) {
363365
assert_eq!(
364366
txs1.len(),
365367
txs2.len(),
@@ -442,6 +444,18 @@ fn test_archive_indexing() {
442444
assert_ledger_index_parity(env, ledger_id, index_id);
443445
}
444446

447+
fn expected_block_timestamp(phase: u32, start_time: SystemTime) -> TimeStamp {
448+
TimeStamp::from(
449+
start_time
450+
.checked_add(
451+
SYNC_STEP_SECONDS
452+
.checked_mul(phase)
453+
.expect("checked_mul should not overflow"),
454+
)
455+
.expect("checked_add should not overflow"),
456+
)
457+
}
458+
445459
#[test]
446460
fn test_get_account_identifier_transactions() {
447461
let mut initial_balances = HashMap::new();
@@ -454,22 +468,26 @@ fn test_get_account_identifier_transactions() {
454468
let index_id = install_index(env, ledger_id);
455469

456470
// List of the transactions that the test is going to add. This exists to make
457-
// the test easier to read
458-
let tx0 = TransactionWithId {
471+
// the test easier to read. The transactions are executed in separate phases, where the block
472+
// timestamp is a function of the phase.
473+
let mut phase = 0u32;
474+
let tx0 = SettledTransactionWithId {
459475
id: 0u64,
460-
transaction: Transaction {
476+
transaction: SettledTransaction {
461477
operation: Operation::Mint {
462478
to: account(1, 0).into(),
463479
amount: Tokens::from_e8s(1_000_000_000_000_u64),
464480
},
465481
memo: Memo(0),
466482
created_at_time: None,
467483
icrc1_memo: None,
484+
timestamp: expected_block_timestamp(phase, env.time()),
468485
},
469486
};
470-
let tx1 = TransactionWithId {
487+
phase = 1;
488+
let tx1 = SettledTransactionWithId {
471489
id: 1u64,
472-
transaction: Transaction {
490+
transaction: SettledTransaction {
473491
operation: Operation::Transfer {
474492
to: account(2, 0).into(),
475493
from: account(1, 0).into(),
@@ -480,11 +498,13 @@ fn test_get_account_identifier_transactions() {
480498
memo: Memo(0),
481499
created_at_time: None,
482500
icrc1_memo: None,
501+
timestamp: expected_block_timestamp(phase, env.time()),
483502
},
484503
};
485-
let tx2 = TransactionWithId {
504+
phase = 2;
505+
let tx2 = SettledTransactionWithId {
486506
id: 2u64,
487-
transaction: Transaction {
507+
transaction: SettledTransaction {
488508
operation: Operation::Transfer {
489509
to: account(2, 0).into(),
490510
from: account(1, 0).into(),
@@ -495,11 +515,12 @@ fn test_get_account_identifier_transactions() {
495515
memo: Memo(0),
496516
created_at_time: None,
497517
icrc1_memo: None,
518+
timestamp: expected_block_timestamp(phase, env.time()),
498519
},
499520
};
500-
let tx3 = TransactionWithId {
521+
let tx3 = SettledTransactionWithId {
501522
id: 3u64,
502-
transaction: Transaction {
523+
transaction: SettledTransaction {
503524
operation: Operation::Transfer {
504525
to: account(1, 1).into(),
505526
from: account(2, 0).into(),
@@ -510,17 +531,19 @@ fn test_get_account_identifier_transactions() {
510531
memo: Memo(0),
511532
created_at_time: None,
512533
icrc1_memo: None,
534+
timestamp: expected_block_timestamp(phase, env.time()),
513535
},
514536
};
537+
phase = 3;
515538
let expires_at = env
516539
.time()
517540
.duration_since(SystemTime::UNIX_EPOCH)
518541
.unwrap()
519542
.as_nanos() as u64
520543
+ Duration::from_secs(3600).as_nanos() as u64;
521-
let tx4 = TransactionWithId {
544+
let tx4 = SettledTransactionWithId {
522545
id: 4u64,
523-
transaction: Transaction {
546+
transaction: SettledTransaction {
524547
operation: Operation::Approve {
525548
from: account(1, 0).into(),
526549
spender: account(4, 4).into(),
@@ -532,6 +555,7 @@ fn test_get_account_identifier_transactions() {
532555
memo: Memo(0),
533556
created_at_time: None,
534557
icrc1_memo: None,
558+
timestamp: expected_block_timestamp(phase, env.time()),
535559
},
536560
};
537561

@@ -643,16 +667,17 @@ fn test_get_account_transactions_start_length() {
643667
);
644668
}
645669
let expected_txs: Vec<_> = (0..10)
646-
.map(|i| TransactionWithId {
670+
.map(|i| SettledTransactionWithId {
647671
id: i,
648-
transaction: Transaction {
672+
transaction: SettledTransaction {
649673
operation: Operation::Mint {
650674
to: account(1, 0).into(),
651675
amount: Tokens::from_e8s(i * 10_000),
652676
},
653677
memo: Memo(0),
654678
created_at_time: None,
655679
icrc1_memo: None,
680+
timestamp: TimeStamp::from(env.time()),
656681
},
657682
})
658683
.collect();
@@ -731,7 +756,7 @@ fn test_get_account_identifier_transactions_pagination() {
731756
}
732757

733758
let mut last_seen_txid = start;
734-
for TransactionWithId { id, transaction } in &res.transactions {
759+
for SettledTransactionWithId { id, transaction } in &res.transactions {
735760
// transactions ids must be unique and in descending order
736761
if let Some(last_seen_txid) = last_seen_txid {
737762
assert!(*id < last_seen_txid);
@@ -740,14 +765,19 @@ fn test_get_account_identifier_transactions_pagination() {
740765

741766
// check the transaction itself
742767
assert_tx_eq(
743-
&Transaction {
768+
&SettledTransaction {
744769
operation: Operation::Mint {
745770
to: account(1, 0).into(),
746771
amount: Tokens::from_e8s(*id * 10_000),
747772
},
748773
memo: Memo(0),
749774
created_at_time: None,
750775
icrc1_memo: None,
776+
timestamp: TimeStamp::from(
777+
env.time()
778+
.checked_sub(SYNC_STEP_SECONDS)
779+
.expect("should not underflow"),
780+
),
751781
},
752782
transaction,
753783
);

0 commit comments

Comments
 (0)