junobuild_shared/ledger/
icp.rs

1use crate::env::LEDGER;
2use crate::ledger::types::icp::{BlockIndexed, Blocks};
3use crate::ledger::utils::account_identifier_equal;
4use candid::{Func, Principal};
5use futures::future::join_all;
6use ic_cdk::api::call::{CallResult, RejectionCode};
7use ic_cdk::call;
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(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(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(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(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
200/// Queries the ledger for blocks since a specified index, including handling archived blocks.
201///
202/// # Arguments
203/// * `ledger_canister_id` - The principal of the ledger canister.
204/// * `start` - The starting block index.
205/// * `length` - The number of blocks to query.
206///
207/// # Returns
208/// A result containing the queried blocks or an error message.
209async fn blocks_since(
210    ledger_canister_id: Principal,
211    start: BlockIndex,
212    length: u64,
213) -> CallResult<Blocks> {
214    // Source: OpenChat
215    // https://github.com/open-ic/transaction-notifier/blob/cf8c2deaaa2e90aac9dc1e39ecc3e67e94451c08/canister/impl/src/lifecycle/heartbeat.rs
216
217    let response = query_blocks(ledger_canister_id, GetBlocksArgs { start, length }).await?;
218
219    let blocks: Blocks = response
220        .blocks
221        .into_iter()
222        .enumerate()
223        .map(|(index, block)| (start + (index as u64), block))
224        .collect();
225
226    if response.archived_blocks.is_empty() {
227        Ok(blocks)
228    } else {
229        type FromArchiveResult = CallResult<Blocks>;
230
231        async fn get_blocks_from_archive(range: ArchivedBlockRange) -> FromArchiveResult {
232            let args = GetBlocksArgs {
233                start: range.start,
234                length: range.length,
235            };
236            let func: Func = range.callback.into();
237
238            let response: CallResult<(GetBlocksResult,)> =
239                call(func.principal, &func.method, (args,)).await;
240
241            match response {
242                Err(e) => Err(e),
243                Ok((block_result,)) => match block_result {
244                    Err(_) => Err((
245                        RejectionCode::Unknown,
246                        "Block results cannot be decoded".to_string(),
247                    )),
248                    Ok(blocks_range) => Ok(blocks_range
249                        .blocks
250                        .into_iter()
251                        .enumerate()
252                        .map(|(index, block)| (range.start + (index as u64), block))
253                        .collect()),
254                },
255            }
256        }
257
258        // Adapt original code .archived_blocks.into_iter().sorted_by_key(|a| a.start)
259        let mut order_archived_blocks = response.archived_blocks;
260        order_archived_blocks.sort_by(|a, b| a.start.cmp(&b.start));
261
262        // Get the transactions from the archive canisters
263        let futures: Vec<_> = order_archived_blocks
264            .into_iter()
265            .map(get_blocks_from_archive)
266            .collect();
267
268        let archive_responses: Vec<FromArchiveResult> = join_all(futures).await;
269
270        let results = archive_responses
271            .into_iter()
272            .collect::<CallResult<Vec<Blocks>>>()?;
273
274        Ok(results.into_iter().flatten().chain(blocks).collect())
275    }
276}