verity_ic/remittance/external_router/
mod.rs

1use crate::remittance::types::{DataModel, Event, RemittanceSubscriber, Subscriber};
2use candid::{CandidType, Principal};
3use ic_cdk::{api::call::RejectionCode, id};
4use serde::Deserialize;
5use serde_json::Value;
6use std::{cell::RefCell, collections::BTreeMap};
7
8thread_local! {
9    pub static REMITTANCE_CANISTER: RefCell<Option<RemittanceSubscriber>> = RefCell::default();
10}
11
12pub mod permissions;
13pub type SubscriberStore = BTreeMap<Principal, Subscriber>;
14#[derive(Clone, Debug, CandidType, Deserialize)]
15pub struct Account {
16    pub balance: u64,
17}
18
19/// Set a value for the remittance canister registered
20pub fn set_remittance_canister(remittance_principal: Principal) {
21    // Store the remittance canister's principal and mark it as not subscribed
22    REMITTANCE_CANISTER.with(|rc| {
23        let _ = rc.borrow_mut().insert(RemittanceSubscriber {
24            canister_principal: remittance_principal,
25            subscribed: false,
26        });
27    })
28}
29
30/// Get the remittance canister registered with the DC canister
31pub fn get_remittance_canister() -> RemittanceSubscriber {
32    // Ensure at least one remittance canister is subscribed to this DC
33    REMITTANCE_CANISTER
34        .with(|rc| rc.borrow().clone())
35        .expect("REMITTANCE_CANISTER_NOT_INITIALIZED")
36}
37
38/// Subscribe to the DC canister
39///
40/// This function is called externally by the remittance canister
41/// that wants to receive data from this external router (DC or PDC) canister. The remittance
42/// canister will call this function that exists in the external router canister.
43pub fn subscribe() {
44    // Verify if this remittance canister is whitelisted
45    // Set the subscribed value to true if it matches, otherwise panic
46    let subscriber_principal_id = ic_cdk::caller();
47    let whitelisted_remittance_canister = REMITTANCE_CANISTER
48        .with(|rc| rc.borrow().clone())
49        .expect("REMITTANCE_CANISTER_NOT_INITIALIZED");
50
51    if whitelisted_remittance_canister.canister_principal != subscriber_principal_id {
52        panic!("REMITTANCE_CANISTER_NOT_WHITELISTED");
53    }
54
55    REMITTANCE_CANISTER.with(|rc| {
56        let _ = rc.borrow_mut().insert(RemittanceSubscriber {
57            canister_principal: subscriber_principal_id,
58            subscribed: true,
59        });
60    });
61}
62
63/// Checks if the provided canister principal is already whitelisted by this DC canister
64pub fn is_subscribed(canister_principal: Principal) -> bool {
65    let whitelisted_remittance_canister = REMITTANCE_CANISTER
66        .with(|rc| rc.borrow().clone())
67        .expect("REMITTANCE_CANISTER_NOT_INITIALIZED");
68
69    whitelisted_remittance_canister.canister_principal == canister_principal
70        && whitelisted_remittance_canister.subscribed
71}
72
73/// Publish an array of DC state changes to the remittance canister
74pub async fn publish_dc_json_to_remittance(json_data: String) -> Result<(), String> {
75    // The input string should be an array of events in the same format
76    // used by the ccamp to fetch events from logstore. This is used
77    // for events that were missed by the poller or need manual provisioning.
78    // Schema example:
79    // [{
80    //     "event_name": "BalanceAdjusted",
81    //     "canister_id": "bkyz2-fmaaa-aaaaa-qaaaq-cai",
82    //     "account": "0x9C81E8F60a9B8743678F1b6Ae893Cc72c6Bc6840",
83    //     "amount": 100000,
84    //     "chain": "ethereum:5",
85    //     "token": "0xB24a30A3971e4d9bf771BDc81435c25EA69A445c"
86    // }]
87
88    // Parse the string of data into serde_json::Value.
89    let json_event: Value =
90        serde_json::from_str(&json_data[..]).expect("JSON_DESERIALIZATION_FAILED");
91    // Ensure the top-level JSON is an array
92    let update_succesful = if let Value::Array(events) = json_event {
93        let mut parsed_events: Vec<DataModel> = Vec::new();
94
95        for event in events {
96            // Parse the JSON object into an 'Event' struct
97            let json_event: Event = serde_json::from_value(event).unwrap();
98            // Convert each "event" object into a data model and send it to the remittance canister
99            let parsed_event: DataModel = json_event.into();
100            // Send this info to the remittance canister to modify the balances
101            parsed_events.push(parsed_event);
102        }
103
104        let dc_canister = id();
105        let response = broadcast_to_remittance(&parsed_events, dc_canister)
106            .or(Err("PUBLISH_FAILED".to_string()));
107
108        response
109    } else {
110        Err("ERROR_PARSING_EVENT_INTO_DATAMODEL".to_string())
111    };
112
113    update_succesful
114}
115
116/// Publish an array of PDC state changes to the remittance canister
117pub async fn publish_pdc_json_to_remittance(json_data: String) -> Result<(), String> {
118    // The input string should be an array of events in the same format
119    // used by the ccamp to fetch events from logstore. This is used
120    // for events that were missed by the poller or need manual provisioning.
121    // Schema example:
122    // [{
123    //     "event_name": "FundsDeposited",
124    //     "canister_id": "bkyz2-fmaaa-aaaaa-qaaaq-cai",
125    //     "account": "0x9C81E8F60a9B8743678F1b6Ae893Cc72c6Bc6840",
126    //     "amount": 100000,
127    //     "chain": "ethereum:5",
128    //     "token": "0xB24a30A3971e4d9bf771BDc81435c25EA69A445c"
129    // }]
130
131    // Parse the string of data into serde_json::Value.
132    let json_event: Value =
133        serde_json::from_str(&json_data[..]).expect("JSON_DESERIALIZATION_FAILED");
134    // Ensure the top-level JSON is an array
135    let update_succesful = if let Value::Array(events) = json_event {
136        for event in events {
137            // Parse the JSON object into an 'Event' struct
138            let json_event: Event = serde_json::from_value(event).unwrap();
139            // Parse the canister_id string into a principal
140            let dc_canister: Principal = (&json_event.canister_id[..]).try_into().unwrap();
141            // Convert each "event" object into a data model and send it to the remittance canister
142            let parsed_event: DataModel = json_event.into();
143            // Send this info to the remittance canister to modify the balances
144            let _ = broadcast_to_remittance(&vec![parsed_event], dc_canister);
145        }
146        Ok(())
147    } else {
148        Err("ERROR_PARSING_EVENT_INTO_DATAMODEL".to_string())
149    };
150
151    update_succesful
152}
153
154// Use this method to publish data to the remittance model
155// When new data is available, publish it to the remittance model
156pub fn broadcast_to_remittance(
157    events: &Vec<DataModel>,
158    dc_canister: Principal,
159) -> Result<(), RejectionCode> {
160    let whitelisted_remittance_canister = get_remittance_canister();
161    if !whitelisted_remittance_canister.subscribed {
162        panic!("REMITTANCE_CANISTER_NOT_INITIALIZED");
163    }
164
165    let remittance_response: Result<(), RejectionCode> = ic_cdk::notify(
166        whitelisted_remittance_canister.canister_principal,
167        "update_remittance",
168        (&events, dc_canister),
169    );
170
171    remittance_response
172}