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