1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
use crate::utils::qr::read_qrs;
use crate::utils::qr::Error as PinReadError;
use crate::utils::qr::PinReadModeSettings;
use crate::Wallet;
use bech32::FromBase32;
use chain_impl_mockchain::{block::BlockDate, fragment::FragmentId};
use jcli_lib::key::read_bech32;
pub use jormungandr_automation::jormungandr::RestSettings;
use std::path::Path;
use thiserror::Error;
use valgrind::ProposalExtension;
use valgrind::SettingsExtensions;
use valgrind::ValgrindClient;
use vit_servicing_station_lib::db::models::proposals::FullProposalInfo;
use wallet::Settings;
use wallet_core::Choice;

unsafe impl Send for Wallet {}
use jormungandr_automation::testing::vit::VoteCastCounterError;
use jormungandr_lib::interfaces::VotePlanId;
use std::convert::TryInto;

/// Responsible for controlling more than one wallet at one time. Useful for load scenario or wallets
/// which handles many users
pub struct MultiController {
    pub(super) backend: ValgrindClient,
    pub(super) wallets: Vec<Wallet>,
    pub(super) settings: Settings,
}

impl MultiController {
    /// Creates object based on qr codes files
    ///
    /// # Errors
    ///
    /// On backend connectivity issues or parsing qr problems
    ///
    /// # Panics
    ///
    /// On internal error when exposing secret key
    pub fn recover_from_qrs<P: AsRef<Path>>(
        wallet_backend_address: &str,
        qrs: &[P],
        pin_mode: &PinReadModeSettings,
        backend_settings: RestSettings,
    ) -> Result<Self, MultiControllerError> {
        let mut backend =
            ValgrindClient::new(wallet_backend_address.to_string(), backend_settings)?;
        let settings = backend.settings()?.into_wallet_settings();

        backend.enable_logs();
        let wallets = read_qrs(qrs, pin_mode, true)
            .into_iter()
            .map(|secret| Wallet::recover(secret.leak_secret().as_ref()).unwrap())
            .collect();

        Ok(Self {
            backend,
            wallets,
            settings,
        })
    }

    /// Creates object based on secret files
    ///
    /// # Errors
    ///
    /// On backend connectivity issues or parsing qr problems
    ///
    /// # Panics
    ///
    /// On single wallet recover error
    ///
    pub fn recover_from_sks<P: AsRef<Path>>(
        proxy_address: &str,
        private_keys: &[P],
        backend_settings: RestSettings,
    ) -> Result<Self, MultiControllerError> {
        let backend = ValgrindClient::new(proxy_address.to_string(), backend_settings)?;
        let settings = backend.settings()?.into_wallet_settings();
        let wallets = private_keys
            .iter()
            .map(|x| {
                let (_, data, _) = read_bech32(Some(&x.as_ref().to_path_buf())).unwrap();
                let key_bytes = Vec::<u8>::from_base32(data.as_slice()).unwrap();
                let data: [u8; 64] = key_bytes.try_into().unwrap();
                Wallet::recover(&data).unwrap()
            })
            .collect();

        Ok(Self {
            backend,
            wallets,
            settings,
        })
    }

    /// Gets proposals from vit-servicing-station based on voting group
    ///
    /// # Errors
    ///
    /// On connection issues
    ///
    pub fn proposals(&self, group: &str) -> Result<Vec<FullProposalInfo>, MultiControllerError> {
        self.backend.proposals(group).map_err(Into::into)
    }

    /// Get inner backend client which can perform some custom REST API operations over the node
    /// or servicing-station
    pub(crate) fn backend(&self) -> &ValgrindClient {
        &self.backend
    }

    /// Update wallet state for entire wallets collection based on current state in blockchain
    ///
    /// # Errors
    ///
    /// On connection issues
    ///
    pub fn update_wallets_state(&mut self) -> Result<(), MultiControllerError> {
        let backend = self.backend().clone();
        let count = self.wallets.len();
        for (idx, wallet) in self.wallets.iter_mut().enumerate() {
            let account_state = backend.account_state(wallet.id())?;
            println!("{}/{} Updating account state", idx + 1, count);
            wallet.set_state((*account_state.value()).into(), account_state.counters())?;
        }
        Ok(())
    }

    /// Update wallet states based on current state in blockchain
    ///
    /// # Errors
    ///
    /// On connection issues
    ///
    pub fn update_wallet_state(&mut self, wallet_index: usize) -> Result<(), MultiControllerError> {
        let backend = self.backend().clone();
        let wallet = self
            .wallets
            .get_mut(wallet_index)
            .ok_or(MultiControllerError::NotEnoughWallets)?;
        let account_state = backend.account_state(wallet.id())?;
        wallet
            .set_state((*account_state.value()).into(), account_state.counters())
            .map_err(Into::into)
    }

    /// Update wallet states based on current state in blockchain if condition is satisfied. For example
    /// only if wallet spending counter is 0 which means it is in initial state
    ///
    /// # Errors
    ///
    /// On connection issues
    ///
    pub fn update_wallet_state_if(
        &mut self,
        wallet_index: usize,
        predicate: &dyn Fn(&Wallet) -> bool,
    ) -> Result<(), MultiControllerError> {
        let wallet = self
            .wallets
            .get_mut(wallet_index)
            .ok_or(MultiControllerError::NotEnoughWallets)?;
        if predicate(wallet) {
            self.update_wallet_state(wallet_index)?;
        }
        Ok(())
    }

    /// Sends vote transaction on behalf of wallet with index `wallet_index` on proposal with
    /// given choice. Sets expiry slot equal to `valid_until`.
    /// # Errors
    ///
    /// On connection issues
    ///
    pub fn vote(
        &mut self,
        wallet_index: usize,
        proposal: &FullProposalInfo,
        choice: Choice,
        valid_until: BlockDate,
    ) -> Result<FragmentId, MultiControllerError> {
        let wallet = self
            .wallets
            .get_mut(wallet_index)
            .ok_or(MultiControllerError::NotEnoughWallets)?;
        let tx = wallet.vote(
            self.settings.clone(),
            &proposal.clone().into_wallet_proposal(),
            choice,
            &valid_until,
        )?;
        self.backend()
            .send_fragment(tx.to_vec())
            .map_err(Into::into)
    }

    /// Sends bunch of vote transactions on behalf of wallet with index `wallet_index`
    /// with map of proposals and respectful choices. Sets expiry slot equal to `valid_until`.
    /// Method can use V0 or V1 api based on preference. V1 enable to send all transactions in a batch
    /// as single call whether legacy V0 is able to send them as one vote per call
    ///
    /// # Errors
    ///
    /// On connection issues
    ///
    /// # Panics
    ///
    /// On connection problem
    ///
    pub fn votes_batch(
        &mut self,
        wallet_index: usize,
        use_v1: bool,
        votes_data: Vec<(&FullProposalInfo, Choice)>,
        valid_until: &BlockDate,
    ) -> Result<Vec<FragmentId>, MultiControllerError> {
        let wallet = self
            .wallets
            .get_mut(wallet_index)
            .ok_or(MultiControllerError::NotEnoughWallets)?;
        let account_state = self.backend.account_state(wallet.id())?;

        let mut counters = account_state.counters();
        let settings = self.settings.clone();
        let txs = votes_data
            .into_iter()
            .map(|(p, c)| {
                wallet
                    .set_state((*account_state.value()).into(), counters)
                    .unwrap();
                let tx = wallet
                    .vote(
                        settings.clone(),
                        &p.clone().into_wallet_proposal(),
                        c,
                        valid_until,
                    )
                    .unwrap()
                    .to_vec();
                counters[0] += 1;
                tx
            })
            .rev()
            .collect();

        self.backend()
            .send_fragments_at_once(txs, use_v1)
            .map_err(Into::into)
    }

    /// Confirms all transactions for all wallets. Confirms means loose interest in tracking their statuses.
    /// This method should be call when we are sure our transactions all in final states (in block or failed).
    pub fn confirm_all_transactions(&mut self) {
        for wallet in &mut self.wallets {
            wallet.confirm_all_transactions();
        }
    }

    /// Confirms all transactions for wallet. Confirms means loose interest in tracking their statuses.
    /// This method should be call when we are sure our transactions all in final states (in block or failed).
    pub fn confirm_transaction(&mut self, fragment_id: FragmentId) {
        for wallet in &mut self.wallets {
            wallet.confirm_transaction(fragment_id);
        }
    }

    /// Wallet counts
    #[must_use]
    pub fn wallet_count(&self) -> usize {
        self.wallets.len()
    }
}

impl From<MultiController> for Vec<Wallet> {
    fn from(controller: MultiController) -> Self {
        controller.wallets
    }
}

/// Errors for `MultiController`
#[derive(Debug, Error)]
pub enum MultiControllerError {
    /// Wallet related errors
    #[error("wallet error")]
    Wallet(#[from] crate::wallet::Error),
    /// Backend related errors
    #[error("wallet error")]
    Backend(#[from] valgrind::Error),
    /// Wallet Controller related errors
    #[error("controller error")]
    Controller(#[from] crate::ControllerError),
    /// Read pin errors
    #[error("pin read error")]
    PinRead(#[from] PinReadError),
    /// Wallet time boundaries errors
    #[error("wallet time error")]
    WalletTime(#[from] wallet::time::Error),
    /// Not enough proposals
    #[error("not enough proposals")]
    NotEnoughProposals,
    /// Internal wallet core errors
    #[error(transparent)]
    WalletCore(#[from] wallet_core::Error),
    /// Not enough proposals
    #[error("not enough wallets")]
    NotEnoughWallets,
    /// Not enough votes
    #[error("not enough votes to cast")]
    NoMoreVotesToVote,
    /// Randomizing choices failed
    #[error("cannot choose next random choice")]
    RandomChoiceFailed,
    /// Missing proposal
    #[error("missing proposal with id: {0}")]
    MissingProposal(usize),
    /// Missing proposal
    #[error(transparent)]
    VotesCastRegister(#[from] VoteCastCounterError),
    /// Too many proposals
    #[error("invalid proposals length for voteplan with id: ({0}) by design it should be more than 128 proposals in single vote plan")]
    InvalidProposalsLen(VotePlanId),
}