Skip to main content

junobuild_shared/ledger/
icp.rs

1use crate::env::ICP_LEDGER;
2use crate::ic::DecodeCandid;
3use crate::ledger::types::icp::{BlockIndexed, Blocks};
4use crate::ledger::utils::account_identifier_equal;
5use candid::{Func, Principal};
6use futures::future::join_all;
7use ic_cdk::call::{Call, CallResult};
8use ic_ledger_types::{
9    query_blocks, transfer, AccountIdentifier, ArchivedBlockRange, BlockIndex, GetBlocksArgs,
10    GetBlocksResult, Memo, Operation, Subaccount, Tokens, Transaction, TransferArgs,
11    TransferResult, DEFAULT_SUBACCOUNT,
12};
13
14// We do not use subaccount, yet.
15pub const SUB_ACCOUNT: Subaccount = DEFAULT_SUBACCOUNT;
16
17/// Converts a principal and subaccount into an account identifier.
18///
19/// # Arguments
20/// * `principal` - A reference to the principal to be converted.
21/// * `sub_account` - A reference to the subaccount.
22///
23/// # Returns
24/// An `AccountIdentifier` derived from the given principal and subaccount.
25pub fn principal_to_account_identifier(
26    principal: &Principal,
27    sub_account: &Subaccount,
28) -> AccountIdentifier {
29    AccountIdentifier::new(principal, sub_account)
30}
31
32/// Transfers tokens to a specified account.
33///
34/// # Arguments
35/// * `to` - The principal of the destination account.
36/// * `to_sub_account` - The subaccount of the destination account.
37/// * `memo` - A memo for the transaction.
38/// * `amount` - The amount of tokens to transfer.
39/// * `fee` - The transaction fee.
40///
41/// # Returns
42/// A result containing the transfer result or an error message.
43pub async fn transfer_payment(
44    to: &Principal,
45    to_sub_account: &Subaccount,
46    memo: Memo,
47    amount: Tokens,
48    fee: Tokens,
49) -> CallResult<TransferResult> {
50    let account_identifier: AccountIdentifier = principal_to_account_identifier(to, to_sub_account);
51
52    let args = TransferArgs {
53        memo,
54        amount,
55        fee,
56        from_subaccount: Some(SUB_ACCOUNT),
57        to: account_identifier,
58        created_at_time: None,
59    };
60
61    transfer_token(args).await
62}
63
64/// Initiates a transfer of ICP tokens using the provided arguments and "old" ICP account identifier.
65///
66/// # Arguments
67/// * `args` - A `TransferArgs` struct containing the details of the ICP transfer.
68///
69/// # Returns
70/// A `CallResult<TransferResult>` indicating either the success or failure of the ICP token transfer.
71pub async fn transfer_token(args: TransferArgs) -> CallResult<TransferResult> {
72    let ledger = Principal::from_text(ICP_LEDGER).unwrap();
73
74    transfer(ledger, &args).await
75}
76
77/// Finds a payment transaction based on specified criteria.
78///
79/// # Arguments
80/// * `from` - The account identifier of the sender.
81/// * `to` - The account identifier of the receiver.
82/// * `amount` - The amount of tokens transferred.
83/// * `block_index` - The starting block index to search from.
84///
85/// # Returns
86/// An option containing the found block indexed or None if not found.
87pub async fn find_payment(
88    from: AccountIdentifier,
89    to: AccountIdentifier,
90    amount: Tokens,
91    block_index: BlockIndex,
92) -> Option<BlockIndexed> {
93    let ledger = Principal::from_text(ICP_LEDGER).unwrap();
94
95    // We can use a length of block of 1 to find the block we are interested in
96    // https://forum.dfinity.org/t/ledger-query-blocks-how-to/16996/4
97    let response = blocks_since(ledger, block_index, 1).await.unwrap();
98
99    fn payment_check(
100        transaction: &Transaction,
101        expected_from: AccountIdentifier,
102        expected_to: AccountIdentifier,
103        expected_amount: Tokens,
104    ) -> bool {
105        match &transaction.operation {
106            None => (),
107            Some(operation) => match operation {
108                Operation::Transfer {
109                    from, to, amount, ..
110                } => {
111                    return account_identifier_equal(expected_from, *from)
112                        && account_identifier_equal(expected_to, *to)
113                        && expected_amount.e8s() == amount.e8s();
114                }
115                Operation::Mint { .. } => (),
116                Operation::Burn { .. } => (),
117                Operation::Approve { .. } => (),
118                Operation::TransferFrom { .. } => (),
119            },
120        }
121
122        false
123    }
124
125    let block = response
126        .iter()
127        .find(|(_, block)| payment_check(&block.transaction, from, to, amount));
128
129    block.cloned()
130}
131
132/// Queries the ledger for the current chain length.
133///
134/// # Arguments
135/// * `block_index` - The block index from which to start the query.
136///
137/// # Returns
138/// A result containing the chain length or an error message.
139pub async fn chain_length(block_index: BlockIndex) -> CallResult<u64> {
140    let ledger = Principal::from_text(ICP_LEDGER).unwrap();
141    let response = query_blocks(
142        ledger,
143        &GetBlocksArgs {
144            start: block_index,
145            length: 1,
146        },
147    )
148    .await?;
149    Ok(response.chain_length)
150}
151
152/// Finds blocks containing transfers for specified account identifiers.
153///
154/// # Arguments
155/// * `block_index` - The starting block index for the query.
156/// * `length` - The number of blocks to query.
157/// * `account_identifiers` - A list of account identifiers to match transactions.
158///
159/// # Returns
160/// A collection of blocks matching the criteria.
161pub async fn find_blocks_transfer(
162    block_index: BlockIndex,
163    length: u64,
164    account_identifiers: Vec<AccountIdentifier>,
165) -> Blocks {
166    let ledger = Principal::from_text(ICP_LEDGER).unwrap();
167
168    // Source: OpenChat
169    // https://github.com/open-ic/transaction-notifier/blob/cf8c2deaaa2e90aac9dc1e39ecc3e67e94451c08/canister/impl/src/lifecycle/heartbeat.rs#L73
170    let response = blocks_since(ledger, block_index, length).await.unwrap();
171
172    fn valid_mission_control(
173        transaction: &Transaction,
174        account_identifiers: &[AccountIdentifier],
175    ) -> bool {
176        match &transaction.operation {
177            None => (),
178            Some(operation) => match operation {
179                Operation::Transfer { from, to, .. } => {
180                    return account_identifiers.iter().any(|&account_identifier| {
181                        account_identifier == *to || account_identifier == *from
182                    });
183                }
184                Operation::Mint { .. } => (),
185                Operation::Burn { .. } => (),
186                Operation::Approve { .. } => (),
187                Operation::TransferFrom { .. } => (),
188            },
189        }
190
191        false
192    }
193
194    response
195        .into_iter()
196        .filter(|(_, block)| valid_mission_control(&block.transaction, &account_identifiers))
197        .collect()
198}
199
200type QueryBlocksResult = Result<Blocks, String>;
201
202/// Queries the ledger for blocks since a specified index, including handling archived blocks.
203///
204/// # Arguments
205/// * `ledger_canister_id` - The principal of the ledger canister.
206/// * `start` - The starting block index.
207/// * `length` - The number of blocks to query.
208///
209/// # Returns
210/// A result containing the queried blocks or an error message.
211async fn blocks_since(
212    ledger_canister_id: Principal,
213    start: BlockIndex,
214    length: u64,
215) -> QueryBlocksResult {
216    // Source: OpenChat
217    // https://github.com/open-ic/transaction-notifier/blob/cf8c2deaaa2e90aac9dc1e39ecc3e67e94451c08/canister/impl/src/lifecycle/heartbeat.rs
218
219    let response = query_blocks(ledger_canister_id, &GetBlocksArgs { start, length })
220        .await
221        .map_err(|e| {
222            format!(
223                "Query blocks {} with length {} failed: {:?}",
224                start, length, e
225            )
226        })?;
227
228    let blocks: Blocks = response
229        .blocks
230        .into_iter()
231        .enumerate()
232        .map(|(index, block)| (start + (index as u64), block))
233        .collect();
234
235    if response.archived_blocks.is_empty() {
236        Ok(blocks)
237    } else {
238        async fn get_blocks_from_archive(range: ArchivedBlockRange) -> Result<Blocks, String> {
239            let args = GetBlocksArgs {
240                start: range.start,
241                length: range.length,
242            };
243            let func: Func = range.callback.into();
244
245            let block_result = Call::bounded_wait(func.principal, &func.method)
246                .with_arg(args)
247                .await
248                .decode_candid::<GetBlocksResult>()?;
249
250            match block_result {
251                Err(e) => Err(format!("Query blocks from archive failed: {:?}", e)),
252                Ok(blocks_range) => Ok(blocks_range
253                    .blocks
254                    .into_iter()
255                    .enumerate()
256                    .map(|(index, block)| (range.start + (index as u64), block))
257                    .collect()),
258            }
259        }
260
261        // Adapt original code .archived_blocks.into_iter().sorted_by_key(|a| a.start)
262        let mut order_archived_blocks = response.archived_blocks;
263        order_archived_blocks.sort_by(|a, b| a.start.cmp(&b.start));
264
265        // Get the transactions from the archive canisters
266        let futures: Vec<_> = order_archived_blocks
267            .into_iter()
268            .map(get_blocks_from_archive)
269            .collect();
270
271        let archive_responses: Vec<QueryBlocksResult> = join_all(futures).await;
272
273        let results = archive_responses
274            .into_iter()
275            .collect::<Result<Vec<Blocks>, String>>()?;
276
277        Ok(results.into_iter().flatten().chain(blocks).collect())
278    }
279}