verity_ic/remittance/
mod.rs

1//! The 'remittance' submodule contains logic for the remittance canister
2
3pub mod external_router;
4pub mod state;
5pub mod types;
6pub mod utils;
7
8use crate::{
9    crypto::{
10        self,
11        config::{Config, Environment},
12        ethereum::{recover_address_from_eth_signature, sign_message},
13        vec_u8_to_string,
14    },
15    owner, random,
16};
17use candid::Principal;
18use ic_cdk::caller;
19use state::*;
20use types::Subscriber;
21
22const REMITTANCE_EVENT: &str = "REMITTANCE";
23
24/// Initializes the remittance canister state
25pub fn init(env_opt: Option<Environment>) {
26    owner::init_owner();
27    random::init_ic_rand();
28
29    // Save the environment configuration
30    if let Some(env) = env_opt {
31        CONFIG.with(|s| {
32            let mut state = s.borrow_mut();
33            *state = Config::from(env);
34        })
35    }
36}
37
38/// Returns the owner of the contract
39pub fn owner() -> String {
40    owner::get_owner()
41}
42
43/// Returns the name of the contract
44pub fn name() -> String {
45    format!("remittance canister")
46}
47
48/// Subscribes to a DC canister using its canister_id
49pub async fn subscribe_to_dc(canister_id: Principal) {
50    let subscriber = Subscriber {
51        topic: REMITTANCE_EVENT.to_string(),
52    };
53    let _call_result: Result<(), _> = ic_cdk::call(canister_id, "subscribe", (subscriber,)).await;
54    // Update the list of subscribed canisters, avoiding duplicates
55    DC_CANISTERS.with(|dc_canister| {
56        let mut borrowed_canister = dc_canister.borrow_mut();
57        if !borrowed_canister.contains(&canister_id) {
58            borrowed_canister.push(canister_id)
59        }
60    });
61}
62
63/// Subscribes to a PCD canister using its canister_id
64pub async fn subscribe_to_pdc(pdc_canister_id: Principal) {
65    subscribe_to_dc(pdc_canister_id).await;
66    IS_PDC_CANISTER.with(|is_pdc_canister| {
67        is_pdc_canister.borrow_mut().insert(pdc_canister_id, true);
68    });
69}
70
71/// Validates and updates user and canister balances
72pub fn update_remittance(
73    new_remittances: Vec<types::DataModel>,
74    dc_canister: Principal,
75) -> Result<(), String> {
76    let is_pdc =
77        IS_PDC_CANISTER.with(|is_pdc_canister| is_pdc_canister.borrow().contains_key(&caller()));
78
79    // Validate input data, returning errors if any
80    if let Err(text) = utils::validate_remittance_data(is_pdc, &new_remittances, dc_canister) {
81        return Err(text);
82    }
83
84    // Process each remittance message
85    for new_remittance in new_remittances {
86        let _: Result<(), String> = match new_remittance.action.clone() {
87            types::Action::Adjust => {
88                utils::update_balance(&new_remittance, dc_canister);
89                Ok(())
90            }
91            types::Action::Deposit => {
92                utils::update_balance(&new_remittance, dc_canister);
93                // Increment canister's token balance on deposit
94                utils::update_canister_balance(
95                    new_remittance.token,
96                    new_remittance.chain,
97                    dc_canister,
98                    new_remittance.amount,
99                );
100                Ok(())
101            }
102            types::Action::Withdraw => {
103                utils::confirm_withdrawal(
104                    new_remittance.token.to_string(),
105                    new_remittance.chain.to_string(),
106                    new_remittance.account.to_string(),
107                    new_remittance.amount.abs() as u64,
108                    dc_canister,
109                );
110                // Deduct withdrawn amount from canister's pool
111                utils::update_canister_balance(
112                    new_remittance.token,
113                    new_remittance.chain,
114                    dc_canister,
115                    -new_remittance.amount,
116                );
117                Ok(())
118            }
119            types::Action::CancelWithdraw => {
120                utils::cancel_withdrawal(
121                    new_remittance.token.to_string(),
122                    new_remittance.chain.to_string(),
123                    new_remittance.account.to_string(),
124                    new_remittance.amount.abs() as u64,
125                    dc_canister,
126                );
127                Ok(())
128            }
129        };
130    }
131
132    Ok(())
133}
134
135/// Creates a remittance request
136pub async fn remit(
137    token: String,
138    chain: String,
139    account: String,
140    dc_canister: Principal,
141    amount: u64,
142    proof: String,
143) -> Result<types::RemittanceReply, Box<dyn std::error::Error>> {
144    // Verify the 'proof' is a valid signature of the amount
145    let _derived_address = recover_address_from_eth_signature(proof, format!("{amount}"))?;
146
147    // Ensure the signature matches the provided account
148    assert!(
149        _derived_address == account.to_lowercase(),
150        "INVALID_SIGNATURE"
151    );
152    // Ensure the remitted amount is greater than zero
153    assert!(amount > 0, "AMOUNT < 0");
154
155    // Generate key values
156    let chain: types::Chain = chain.try_into()?;
157    let token: types::Wallet = token.try_into()?;
158    let account: types::Wallet = account.try_into()?;
159
160    let hash_key = (
161        token.clone(),
162        chain.clone(),
163        account.clone(),
164        dc_canister.clone(),
165    );
166
167    // Check for withheld balance for the specified amount
168    let withheld_balance = utils::get_remitted_balance(
169        token.clone(),
170        chain.clone(),
171        account.clone(),
172        dc_canister.clone(),
173        amount,
174    );
175
176    let response: types::RemittanceReply;
177    // Return cached signature and nonce if amount exists in withheld map
178    if withheld_balance.balance == amount {
179        let message_hash = utils::hash_remittance_parameters(
180            withheld_balance.nonce,
181            amount,
182            &account.to_string(),
183            &chain.to_string(),
184            &dc_canister.to_string(),
185            &token.to_string(),
186        );
187
188        response = types::RemittanceReply {
189            hash: vec_u8_to_string(&message_hash),
190            signature: withheld_balance.signature.clone(),
191            nonce: withheld_balance.nonce,
192            amount,
193        };
194    } else {
195        let nonce = random::get_random_number();
196        let message_hash = utils::hash_remittance_parameters(
197            nonce,
198            amount,
199            &account.to_string(),
200            &chain.to_string(),
201            &dc_canister.to_string(),
202            &token.to_string(),
203        );
204        let balance = get_available_balance(
205            token.to_string(),
206            chain.to_string(),
207            account.to_string(),
208            dc_canister.clone(),
209        )?
210        .balance;
211
212        // Ensure sufficient funds for withdrawal
213        if amount > balance {
214            panic!("REMIT_AMOUNT:{amount} > AVAILABLE_BALANCE:{balance}");
215        }
216
217        // Generate a signature for the parameters
218        let config_store = CONFIG.with(|store| store.borrow().clone());
219        let signature_reply = sign_message(&message_hash, &config_store).await?;
220        let signature_string = format!("0x{}", signature_reply.signature_hex);
221
222        // Deduct remitted amount from main balance
223        REMITTANCE.with(|remittance| {
224            if let Some(existing_data) = remittance.borrow_mut().get_mut(&hash_key) {
225                existing_data.balance = existing_data.balance - amount;
226            }
227        });
228        // Track individual remitted amounts per (token, chain, recipient) combination
229        WITHHELD_AMOUNTS.with(|withheld_amount| {
230            withheld_amount
231                .borrow_mut()
232                .entry(hash_key.clone())
233                .or_insert(Vec::new())
234                .push(amount);
235        });
236        // Update withheld balance and generate a new signature
237        WITHHELD_REMITTANCE.with(|withheld| {
238            let mut withheld_remittance_store = withheld.borrow_mut();
239            withheld_remittance_store.insert(
240                (
241                    token.clone(),
242                    chain.clone(),
243                    account.clone(),
244                    dc_canister.clone(),
245                    amount,
246                ),
247                types::WithheldAccount {
248                    balance: amount,
249                    signature: signature_string.clone(),
250                    nonce,
251                },
252            );
253        });
254        // Create response object
255        response = types::RemittanceReply {
256            hash: format!("0x{}", vec_u8_to_string(&message_hash)),
257            signature: signature_string.clone(),
258            nonce,
259            amount,
260        };
261    }
262
263    Ok(response)
264}
265
266/// Retrieves the available balance for an account associated with a canister
267pub fn get_available_balance(
268    token: String,
269    chain: String,
270    account: String,
271    dc_canister: Principal,
272) -> Result<types::Account, Box<dyn std::error::Error>> {
273    let chain: types::Chain = chain.try_into()?;
274    let token: types::Wallet = token.try_into()?;
275    let account: types::Wallet = account.try_into()?;
276
277    // Get available balance for the specified key
278    let amount = utils::get_available_balance(token, chain, account, dc_canister);
279
280    Ok(amount)
281}
282
283/// Retrieves the canister balance for a specific token and chain
284pub fn get_canister_balance(
285    token: String,
286    chain: String,
287    dc_canister: Principal,
288) -> Result<types::Account, Box<dyn std::error::Error>> {
289    let chain: types::Chain = chain.try_into().unwrap();
290    let token: types::Wallet = token.try_into().unwrap();
291
292    // Get canister balance for the specified key
293    let amount = utils::get_canister_balance(token, chain, dc_canister);
294
295    Ok(amount)
296}
297
298/// Retrieves the withheld balance for an account on a specified canister and chain
299pub fn get_withheld_balance(
300    token: String,
301    chain: String,
302    account: String,
303    dc_canister: Principal,
304) -> Result<types::Account, Box<dyn std::error::Error>> {
305    let chain: types::Chain = chain.try_into()?;
306    let token: types::Wallet = token.try_into()?;
307    let account: types::Wallet = account.try_into()?;
308
309    let existing_key = (token.clone(), chain.clone(), account.clone(), dc_canister);
310
311    // sum up all the amounts in the withheld_amount value of this key
312    let sum = WITHHELD_AMOUNTS.with(|withheld_amount| {
313        let withheld_amount = withheld_amount.borrow();
314        let values = withheld_amount.get(&existing_key);
315
316        match values {
317            Some(vec) => vec.iter().sum::<u64>(),
318            None => 0,
319        }
320    });
321
322    Ok(types::Account { balance: sum })
323}
324
325/// Get the reciept of a successfull withdrawal
326pub fn get_reciept(
327    dc_canister: Principal,
328    nonce: u64,
329) -> Result<types::RemittanceReciept, Box<dyn std::error::Error>> {
330    let key = (dc_canister.clone(), nonce.clone());
331    Ok(REMITTANCE_RECIEPTS.with(|remittance_reciepts| {
332        remittance_reciepts
333            .borrow()
334            .get(&key)
335            .expect("RECIEPT_NOT_FOUND")
336            .clone()
337    }))
338}
339
340/// Get the public key associated with this particular canister
341pub async fn public_key() -> Result<crypto::ecdsa::PublicKeyReply, Box<dyn std::error::Error>> {
342    let config = CONFIG.with(|c| c.borrow().clone());
343
344    let request = crypto::ecdsa::ECDSAPublicKey {
345        canister_id: None,
346        derivation_path: vec![],
347        key_id: config.key.to_key_id(),
348    };
349
350    let (res,): (crypto::ecdsa::ECDSAPublicKeyReply,) = ic_cdk::call(
351        Principal::management_canister(),
352        "ecdsa_public_key",
353        (request,),
354    )
355    .await
356    .map_err(|e| format!("ECDSA_PUBLIC_KEY_FAILED: {}\t,Error_code:{:?}", e.1, e.0))?;
357
358    let address = crypto::ethereum::get_address_from_public_key(res.public_key.clone())?;
359
360    Ok(crypto::ecdsa::PublicKeyReply {
361        sec1_pk: hex::encode(res.public_key),
362        etherum_pk: address,
363    })
364}