verity_ic/remittance/
utils.rs

1use super::{
2    types::{Account, RemittanceReciept, WithheldAccount},
3    DC_CANISTERS,
4};
5use crate::crypto::string_to_vec_u8;
6use candid::Principal;
7use easy_hasher::easy_hasher;
8use eth_encode_packed::{
9    ethabi::{ethereum_types::U256, Address},
10    SolidityDataType,
11};
12use ic_cdk::{api::time, caller};
13
14/// Ensures the caller is a whitelisted DC canister, otherwise panics.
15/// This function checks if the caller's principal ID is in the list of allowed DC canisters.
16pub fn only_whitelisted_dc_canister() {
17    let caller_principal_id = caller();
18    if !DC_CANISTERS.with(|publisher| publisher.borrow().contains(&caller_principal_id)) {
19        panic!("NOT_ALLOWED");
20    }
21}
22
23/// Hashes the parameters needed for a remittance transaction.
24///
25/// # Parameters
26/// - `nonce`: A unique number to ensure the transaction is unique.
27/// - `amount`: The amount to be remitted.
28/// - `address`: The recipient's Ethereum address.
29/// - `chain_id`: The ID of the blockchain network.
30/// - `dc_canister_id`: The ID of the DC canister.
31/// - `token_address`: The address of the token being transferred.
32///
33/// # Returns
34/// A vector of bytes representing the hashed parameters.
35pub fn hash_remittance_parameters(
36    nonce: u64,
37    amount: u64,
38    address: &str,
39    chain_id: &str,
40    dc_canister_id: &str,
41    token_address: &str,
42) -> Vec<u8> {
43    // convert the address to bytes format
44    let address: [u8; 20] = string_to_vec_u8(address).try_into().unwrap();
45    let token_address: [u8; 20] = string_to_vec_u8(token_address).try_into().unwrap();
46
47    // pack the encoded bytes
48    let input = vec![
49        SolidityDataType::Number(U256::from(nonce)),
50        SolidityDataType::Number(U256::from(amount)),
51        SolidityDataType::Address(Address::from(address)),
52        SolidityDataType::String(chain_id),
53        SolidityDataType::String(dc_canister_id),
54        SolidityDataType::Address(Address::from(token_address)),
55    ];
56    let (_bytes, __) = eth_encode_packed::abi::encode_packed(&input);
57
58    easy_hasher::raw_keccak256(_bytes.clone()).to_vec()
59}
60
61/// Retrieves the remitted balance for a given account and amount.
62///
63/// # Parameters
64/// - `token`: The wallet token.
65/// - `chain`: The blockchain chain.
66/// - `account`: The user's wallet account.
67/// - `dc_canister`: The principal of the DC canister.
68/// - `amount`: The amount to check for remittance.
69///
70/// # Returns
71/// A `WithheldAccount` containing the remitted balance or zero if no remittance exists.
72pub fn get_remitted_balance(
73    token: super::types::Wallet,
74    chain: super::types::Chain,
75    account: super::types::Wallet,
76    dc_canister: Principal,
77    amount: u64,
78) -> WithheldAccount {
79    let withheld_amount = super::WITHHELD_REMITTANCE.with(|withheld| {
80        let existing_key = (token, chain, account.clone(), dc_canister, amount);
81        withheld
82            .borrow()
83            .get(&existing_key)
84            .cloned()
85            .unwrap_or_default()
86    });
87
88    withheld_amount
89}
90
91/// Retrieves the total available balance for a user.
92///
93/// # Parameters
94/// - `token`: The wallet token.
95/// - `chain`: The blockchain chain.
96/// - `account`: The user's wallet account.
97/// - `dc_canister`: The principal of the DC canister.
98///
99/// # Returns
100/// An `Account` containing the available balance.
101pub fn get_available_balance(
102    token: super::types::Wallet,
103    chain: super::types::Chain,
104    account: super::types::Wallet,
105    dc_canister: Principal,
106) -> Account {
107    let available_amount = super::REMITTANCE.with(|remittance| {
108        let existing_key = (token, chain, account, dc_canister);
109        remittance
110            .borrow()
111            .get(&existing_key)
112            .cloned()
113            .unwrap_or_default()
114    });
115
116    available_amount
117}
118
119/// Retrieves the total balance available to a specific canister.
120///
121/// # Parameters
122/// - `token`: The wallet token.
123/// - `chain`: The blockchain chain.
124/// - `dc_canister`: The principal of the DC canister.
125///
126/// # Returns
127/// An `Account` containing the canister's balance.
128pub fn get_canister_balance(
129    token: super::types::Wallet,
130    chain: super::types::Chain,
131    dc_canister: Principal,
132) -> Account {
133    let canister_balance = super::CANISTER_BALANCE.with(|cb| {
134        let existing_key = (token, chain, dc_canister);
135        cb.borrow().get(&existing_key).cloned().unwrap_or_default()
136    });
137
138    canister_balance
139}
140
141/// Updates the balance for a specific account in a DC canister within the remittance mapping.
142///
143/// # Parameters
144/// - `new_remittance`: The new remittance data model.
145/// - `dc_canister`: The principal of the DC canister.
146pub fn update_balance(new_remittance: &super::types::DataModel, dc_canister: Principal) {
147    super::REMITTANCE.with(|remittance| {
148        let mut remittance_store = remittance.borrow_mut();
149
150        let hash_key = (
151            new_remittance.token.clone(),
152            new_remittance.chain.clone(),
153            new_remittance.account.clone(),
154            dc_canister.clone(),
155        );
156
157        if let Some(existing_data) = remittance_store.get_mut(&hash_key) {
158            existing_data.balance =
159                ((existing_data.balance as i64) + (new_remittance.amount as i64)) as u64;
160        } else {
161            remittance_store.insert(
162                hash_key,
163                Account {
164                    balance: new_remittance.amount as u64,
165                },
166            );
167        }
168    });
169}
170
171/// Updates the canister's balance for a specific token.
172///
173/// # Parameters
174/// - `token`: The wallet token.
175/// - `chain`: The blockchain chain.
176/// - `dc_canister`: The principal of the DC canister.
177/// - `amount`: The amount to update the balance by.
178pub fn update_canister_balance(
179    token: super::types::Wallet,
180    chain: super::types::Chain,
181    dc_canister: Principal,
182    amount: i64,
183) {
184    super::CANISTER_BALANCE.with(|canister_balance| {
185        let mut canister_balance_store = canister_balance.borrow_mut();
186
187        let hash_key = (token.clone(), chain.clone(), dc_canister.clone());
188
189        if let Some(existing_data) = canister_balance_store.get_mut(&hash_key) {
190            existing_data.balance = ((existing_data.balance as i64) + (amount as i64)) as u64;
191        } else {
192            canister_balance_store.insert(
193                hash_key,
194                Account {
195                    balance: amount as u64,
196                },
197            );
198        }
199    });
200}
201
202/// Confirms a successful withdrawal by updating the necessary balances.
203///
204/// # Parameters
205/// - `token`: The token involved in the transaction.
206/// - `chain`: The blockchain chain.
207/// - `account`: The user's wallet account.
208/// - `amount_withdrawn`: The amount withdrawn.
209/// - `dc_canister`: The principal of the DC canister.
210///
211/// # Returns
212/// A boolean indicating the success of the withdrawal confirmation.
213pub fn confirm_withdrawal(
214    token: String,
215    chain: String,
216    account: String,
217    amount_withdrawn: u64,
218    dc_canister: Principal,
219) -> bool {
220    let chain: super::types::Chain = chain.try_into().unwrap();
221    let token: super::types::Wallet = token.try_into().unwrap();
222    let account: super::types::Wallet = account.try_into().unwrap();
223
224    let hash_key = (
225        token.clone(),
226        chain.clone(),
227        account.clone(),
228        dc_canister.clone(),
229    );
230
231    // go through the witheld amounts and remove this amount from it
232    super::WITHHELD_AMOUNTS.with(|witheld_amounts| {
233        let mut mut_witheld_amounts = witheld_amounts.borrow_mut();
234        let unwithdrawn_amounts = mut_witheld_amounts
235            .get(&hash_key)
236            .expect("WITHDRAWAL_CONFIRMATION_ERROR:AMOUNT_NOT_WITHELD")
237            .into_iter()
238            .filter(|&amount_to_withdraw| *amount_to_withdraw != amount_withdrawn)
239            .cloned()
240            .collect();
241        mut_witheld_amounts.insert(hash_key, unwithdrawn_amounts);
242    });
243
244    // go through the witheld balance store and remove this amount from it
245    let withdrawn_details = super::WITHHELD_REMITTANCE.with(|withheld_remittance| {
246        let key = (
247            token.clone(),
248            chain.clone(),
249            account.clone(),
250            dc_canister.clone(),
251            amount_withdrawn,
252        );
253        let withdrawn_balance = withheld_remittance.borrow().get(&key).unwrap().clone();
254        withheld_remittance.borrow_mut().remove(&key);
255
256        withdrawn_balance
257    });
258
259    // create a reciept entry here for a succcessfull withdrawal
260    super::REMITTANCE_RECIEPTS.with(|remittance_reciepts| {
261        remittance_reciepts.borrow_mut().insert(
262            (dc_canister, withdrawn_details.nonce),
263            RemittanceReciept {
264                token: token.to_string(),
265                chain: chain.to_string(),
266                amount: amount_withdrawn,
267                account: account.to_string(),
268                timestamp: time(),
269            },
270        );
271    });
272    return true;
273}
274
275/// Cancels a withdrawal request, returning the withheld amount to the available balance.
276///
277/// # Parameters
278/// - `token`: The token involved in the transaction.
279/// - `chain`: The blockchain chain.
280/// - `account`: The user's wallet account.
281/// - `amount_canceled`: The amount to cancel.
282/// - `dc_canister`: The principal of the DC canister.
283///
284/// # Returns
285/// A boolean indicating the success of the cancellation.
286pub fn cancel_withdrawal(
287    token: String,
288    chain: String,
289    account: String,
290    amount_canceled: u64,
291    dc_canister: Principal,
292) -> bool {
293    let chain: super::types::Chain = chain.try_into().unwrap();
294    let token: super::types::Wallet = token.try_into().unwrap();
295    let account: super::types::Wallet = account.try_into().unwrap();
296
297    let hash_key = (
298        token.clone(),
299        chain.clone(),
300        account.clone(),
301        dc_canister.clone(),
302    );
303
304    // go through the witheld amounts and remove this amount from it
305    super::WITHHELD_AMOUNTS.with(|witheld_amounts| {
306        let mut mut_witheld_amounts = witheld_amounts.borrow_mut();
307        let unwithdrawn_amounts = mut_witheld_amounts
308            .get(&hash_key)
309            .expect("CANCEL_WITHDRAW_ERROR:AMOUNT_NOT_WITHELD")
310            .into_iter()
311            .filter(|&amount_to_withdraw| *amount_to_withdraw != amount_canceled)
312            .cloned()
313            .collect();
314        mut_witheld_amounts.insert(hash_key.clone(), unwithdrawn_amounts);
315    });
316
317    // go through the witheld balance store and remove this amount from it
318    super::WITHHELD_REMITTANCE.with(|withheld_remittance| {
319        withheld_remittance.borrow_mut().remove(&(
320            token.clone(),
321            chain.clone(),
322            account.clone(),
323            dc_canister.clone(),
324            amount_canceled,
325        ));
326    });
327
328    // add the withheld total back to the available balance
329    super::REMITTANCE.with(|remittance| {
330        if let Some(existing_data) = remittance.borrow_mut().get_mut(&hash_key) {
331            existing_data.balance = existing_data.balance + amount_canceled;
332        }
333    });
334
335    return true;
336}
337
338/// Validates remittance data based on whether the caller is a PDC or not.
339///
340/// # Parameters
341/// - `is_pdc`: A boolean indicating if the caller is a PDC.
342/// - `new_remittances`: A vector of new remittance data models.
343/// - `dc_canister`: The principal of the DC canister.
344///
345/// # Returns
346/// A result indicating success or an error message.
347pub fn validate_remittance_data(
348    is_pdc: bool,
349    new_remittances: &Vec<super::types::DataModel>,
350    dc_canister: Principal,
351) -> Result<(), String> {
352    match is_pdc {
353        true => validate_pdc_remittance_data(new_remittances, dc_canister),
354        false => validate_dc_remittance_data(new_remittances, dc_canister),
355    }
356}
357
358/// Validates remittance data for processing by a PDC.
359///
360/// # Parameters
361/// - `new_remittances`: A vector of new remittance data models.
362/// - `dc_canister`: The principal of the DC canister.
363///
364/// # Returns
365/// A result indicating success or an error message.
366pub fn validate_pdc_remittance_data(
367    new_remittances: &Vec<super::types::DataModel>,
368    dc_canister: Principal,
369) -> Result<(), String> {
370    // validate that all adjust operations lead to a sum of zero
371    let adjust_operations: Vec<super::types::DataModel> = new_remittances
372        .into_iter()
373        .filter(|&single_remittance| single_remittance.action == super::types::Action::Adjust)
374        .cloned()
375        .collect();
376    // apply the same validation of dc canisters to the adjust operations of a pdc canister
377    if let Err(err_message) = validate_dc_remittance_data(&adjust_operations, dc_canister) {
378        return Err(err_message);
379    }
380
381    // validate that all operations that are not "adjust" operations are positive amounts
382    // other than adjusts we currently have no use for negative amounts operations
383    // this can be later changed
384    let non_adjust_operations_gt_0: Vec<&super::types::DataModel> = new_remittances
385        .into_iter()
386        .filter(|single_remittance| {
387            single_remittance.action != super::types::Action::Adjust && single_remittance.amount < 0
388        })
389        .collect();
390    if non_adjust_operations_gt_0.len() > 0 {
391        return Err("NON_ADJUST_AMOUNT_MUST_BE_GT_0".to_string());
392    }
393
394    Ok(())
395}
396
397/// Validates remittance data for processing by a DC canister.
398///
399/// # Parameters
400/// - `new_remittances`: A vector of new remittance data models.
401/// - `dc_canister`: The principal of the DC canister.
402///
403/// # Returns
404/// A result indicating success or an error message.
405pub fn validate_dc_remittance_data(
406    new_remittances: &Vec<super::types::DataModel>,
407    dc_canister: Principal,
408) -> Result<(), String> {
409    // validate that all operations are adjust and the resultant of amounts is zero
410    let amount_delta = new_remittances
411        .iter()
412        .fold(0, |acc, account| acc + account.amount);
413
414    if amount_delta != 0 {
415        return Err("SUM_ADJUST_AMOUNTS != 0".to_string());
416    }
417
418    // validate it is only adjust action provided
419    let is_action_valid = new_remittances
420        .iter()
421        .all(|item| item.action == super::types::Action::Adjust);
422
423    if !is_action_valid {
424        return Err("INVALID_ACTION_FOUND".to_string());
425    }
426
427    // check for all the negative deductions and confirm that the owners have at least that much balance
428    let mut sufficient_balance_error: Result<(), String> = Ok(());
429    new_remittances
430        .iter()
431        .filter(|&item| item.amount < 0)
432        .for_each(|item| {
433            let existing_balance = get_available_balance(
434                item.token.clone(),
435                item.chain.clone(),
436                item.account.clone(),
437                dc_canister.clone(),
438            );
439
440            if existing_balance.balance < (item.amount.abs() as u64) {
441                sufficient_balance_error = Err("INSUFFICIENT_USER_BALANCE".to_string());
442            }
443        });
444    if let Err(_) = sufficient_balance_error {
445        return sufficient_balance_error;
446    }
447    // check for all positive additions that the canister has enough balance to cover it
448    let mut insufficient_canister_balance: Result<(), String> = Ok(());
449    new_remittances
450        .iter()
451        .filter(|&item| item.amount > 0)
452        .for_each(|item| {
453            let existing_balance =
454                get_canister_balance(item.token.clone(), item.chain.clone(), dc_canister.clone());
455
456            if existing_balance.balance < (item.amount as u64) {
457                insufficient_canister_balance = Err("INSUFFICIENT_CANISTER_BALANCE".to_string());
458            }
459        });
460
461    insufficient_canister_balance
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn test_recover_address_from_eth_signature() {
470        let nonce: u64 = 10;
471        let amount: u64 = 100;
472        let address = "0x5c8e3a7c16fa5cdde9f74751d6b2395176f05c55".to_string();
473        let chain_id = "ethereum:5";
474        let dc_canister_id = "br5f7-7uaaa-aaaaa-qaaca-cai";
475        let token_address = "0x5c8e3a7c16fa5cdde9f74751d6b2395176f05c55";
476        let hashed_output = [
477            8, 134, 176, 185, 121, 53, 193, 199, 185, 42, 238, 73, 122, 96, 223, 42, 230, 175, 125,
478            59, 72, 6, 36, 6, 38, 59, 74, 94, 51, 57, 117, 88,
479        ]
480        .to_vec();
481
482        let recovered_address = hash_remittance_parameters(
483            nonce,
484            amount,
485            &address,
486            chain_id,
487            dc_canister_id,
488            token_address,
489        );
490        assert_eq!(recovered_address, hashed_output);
491    }
492}