polymesh_api_tester/
tester.rs

1use std::collections::HashMap;
2
3#[cfg(feature = "polymesh_v7")]
4use polymesh_api::polymesh::types::polymesh_primitives::secondary_key::ExtrinsicPermissions;
5use polymesh_api::{
6  client::{AccountId, AssetId, IdentityId},
7  polymesh::types::polymesh_primitives::{
8    secondary_key::{KeyRecord, Permissions, SecondaryKey},
9    subset::SubsetRestriction,
10    ticker::Ticker,
11  },
12  Api,
13};
14use polymesh_api_client_extras::*;
15
16use crate::*;
17
18async fn get_sudo_signer(api: &Api, signer: &DbAccountSigner) -> Option<DbAccountSigner> {
19  api.query().sudo().key().await.ok()?.and_then(|key| {
20    if key == signer.account() {
21      Some(signer.clone())
22    } else {
23      None
24    }
25  })
26}
27
28pub struct PolymeshTester {
29  pub api: Api,
30  seed: String,
31  init_polyx: u128,
32  db: Db,
33  pub cdd: DbAccountSigner,
34  pub sudo: Option<DbAccountSigner>,
35  users: HashMap<String, User>,
36}
37
38impl PolymeshTester {
39  pub async fn new() -> Result<Self> {
40    let api = client_api().await?;
41    // Generate a seed based on current timestamp.
42    // We use a 'seed' to allow running tests in parallel.
43    let ts = std::time::SystemTime::now()
44      .duration_since(std::time::UNIX_EPOCH)
45      .expect("now later then epoch")
46      .as_nanos();
47    let url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "accounts.db".into());
48    let db = Db::open(api.clone(), &url).await?;
49    let cdd = DbAccountSigner::alice(db.clone());
50    let sudo = get_sudo_signer(&api, &cdd).await;
51    Ok(Self {
52      api,
53      init_polyx: 10_000 * ONE_POLYX,
54      seed: format!("{}", ts),
55      cdd,
56      sudo,
57      users: Default::default(),
58      db,
59    })
60  }
61
62  /// Set how much POLYX to fund each user.
63  pub fn set_init_polyx(&mut self, val: u128) {
64    self.init_polyx = val * ONE_POLYX;
65  }
66
67  pub fn has_sudo(&self) -> bool {
68    self.sudo.is_some()
69  }
70
71  pub fn get_seed(&self) -> &str {
72    &self.seed
73  }
74
75  pub fn dev_user(&self, name: &str) -> Result<DbAccountSigner> {
76    DbAccountSigner::from_string(self.db.clone(), &format!("//{}", name))
77  }
78
79  fn set_user_did(&mut self, name: &str, did: IdentityId) {
80    if let Some(user) = self.users.get_mut(name) {
81      user.did = Some(did);
82    }
83  }
84
85  fn update_user(&mut self, name: &str, user: &User) {
86    self.users.insert(name.to_string(), user.clone());
87  }
88
89  pub fn new_signer_idx(&self, name: &str, idx: usize) -> Result<AccountSigner> {
90    AccountSigner::from_string(&format!("//{}_{}_{}", self.seed, name, idx))
91  }
92
93  fn get_user(&mut self, name: &str) -> Result<User> {
94    use std::collections::hash_map::Entry;
95    match self.users.entry(name.to_string()) {
96      Entry::Occupied(entry) => Ok(entry.get().clone()),
97      Entry::Vacant(entry) => {
98        let signer = AccountSigner::from_string(&format!("//{}_{}", self.seed, entry.key()))?;
99        let user = User::new(&self.api, signer);
100        Ok(entry.insert(user).clone())
101      }
102    }
103  }
104
105  /// Get the user if they exist, or create a new one.  Make sure the user
106  /// has an identity.
107  pub async fn user(&mut self, name: &str) -> Result<User> {
108    let mut user = self.get_user(name)?;
109    if user.did.is_none() {
110      let did = self.register_and_fund(user.account()).await?;
111      user.did = Some(did);
112      self.set_user_did(name, did);
113    }
114    Ok(user)
115  }
116
117  async fn load_dids(&mut self, users: &mut [User]) -> Result<()> {
118    for user in users {
119      // Skip users that have an identity.
120      if user.did.is_some() {
121        continue;
122      }
123      // Try getting the user's identity from the chain.
124      user.did = self.get_did(user.account()).await?;
125    }
126    Ok(())
127  }
128
129  /// Get the users if they exist, or create them.  Make sure the users
130  /// have identities.
131  pub async fn users(&mut self, names: &[&str]) -> Result<Vec<User>> {
132    let names = names.iter().map(|name| (*name, 0)).collect::<Vec<_>>();
133    self.users_with_secondary_keys(names.as_slice()).await
134  }
135
136  /// Get the users if they exist, or create them.  Make sure the users
137  /// have identities.
138  pub async fn users_with_secondary_keys(&mut self, names: &[(&str, usize)]) -> Result<Vec<User>> {
139    let mut users = Vec::new();
140    let mut accounts = Vec::new();
141    for (name, sk) in names {
142      // Get or create user.
143      let mut user = self.get_user(name)?;
144      if user.secondary_keys.len() < *sk {
145        for idx in 0..*sk {
146          let sk = self.new_signer_idx(name, idx)?;
147          accounts.push(sk.account());
148          user.secondary_keys.push(sk);
149        }
150        self.update_user(name, &user);
151      }
152      accounts.push(user.account());
153      users.push(user);
154    }
155    self.load_dids(users.as_mut_slice()).await?;
156    // Calls for registering users and funding them.
157    let mut calls = Vec::new();
158    let has_sudo = self.has_sudo();
159    // Add calls to register users missing identities.
160    let mut need_dids = Vec::new();
161    let mut has_secondary_keys = false;
162    for (idx, user) in users.iter().enumerate() {
163      if user.did.is_some() {
164        continue;
165      }
166      need_dids.push(idx);
167      if user.secondary_keys.len() > 0 {
168        has_secondary_keys = true;
169      }
170      let secondary_keys = user
171        .secondary_keys
172        .iter()
173        .map(|key| SecondaryKey {
174          key: key.account(),
175          permissions: Permissions {
176            asset: SubsetRestriction::Whole,
177            #[cfg(feature = "polymesh_v6")]
178            extrinsic: SubsetRestriction::Whole,
179            #[cfg(feature = "polymesh_v7")]
180            extrinsic: ExtrinsicPermissions::Whole,
181            portfolio: SubsetRestriction::Whole,
182          },
183        })
184        .collect();
185      // User needs an identity.
186      calls.push(
187        self
188          .api
189          .call()
190          .identity()
191          .cdd_register_did_with_cdd(user.account(), secondary_keys, None)?
192          .into(),
193      );
194    }
195    // Add calls to fund the users.
196    let mut sudos = Vec::new();
197    for account in accounts {
198      if has_sudo {
199        // We have sudo, just set their balance.
200        sudos.push(
201          self
202            .api
203            .call()
204            .balances()
205            .set_balance(account.into(), self.init_polyx, 0)?
206            .into(),
207        );
208      } else {
209        // Transfer some funds to the user.
210        calls.push(
211          self
212            .api
213            .call()
214            .balances()
215            .transfer(account.into(), self.init_polyx)?
216            .into(),
217        );
218      }
219    }
220    let signer = self.sudo.as_mut().unwrap_or_else(|| &mut self.cdd);
221    // Execute batch.
222    let mut res = self
223      .api
224      .call()
225      .utility()
226      .batch(calls)?
227      .submit_and_watch(signer)
228      .await?;
229    let sudo_res = if sudos.len() > 0 {
230      Some(
231        self
232          .api
233          .call()
234          .sudo()
235          .sudo(self.api.call().utility().batch(sudos)?.into())?
236          .submit_and_watch(signer)
237          .await?,
238      )
239    } else {
240      None
241    };
242    let mut auths = HashMap::new();
243    if has_secondary_keys {
244      if let Some(events) = res.events().await? {
245        for rec in &events.0 {
246          match &rec.event {
247            RuntimeEvent::Identity(IdentityEvent::AuthorizationAdded(
248              _,
249              _,
250              Some(sk),
251              auth,
252              _,
253              _,
254            )) => {
255              auths.insert(*sk, *auth);
256            }
257            _ => (),
258          }
259        }
260      }
261    }
262    // Get new identities from batch events.
263    let ids = get_created_ids(&mut res).await?;
264    let mut joins = Vec::new();
265    for idx in need_dids {
266      let (name, _) = names[idx];
267      match &ids[idx] {
268        CreatedIds::IdentityCreated(did) => {
269          let user = &mut users[idx];
270          user.did = Some(*did);
271          for sk in &mut user.secondary_keys {
272            if let Some(auth) = auths.remove(&sk.account()) {
273              joins.push((auth, sk.clone()));
274            }
275          }
276          self.set_user_did(name, *did);
277        }
278        id => {
279          log::warn!("Unexpected id: {id:?}");
280        }
281      }
282    }
283    // Wait for both batches to finalize.
284    if let Some(mut res) = sudo_res {
285      res.wait_finalized().await?;
286    }
287    res.wait_finalized().await?;
288
289    // Join Secondary keys to their identity.
290    if joins.len() > 0 {
291      let mut results = Vec::new();
292      for (auth, mut sk) in joins {
293        results.push(
294          self
295            .api
296            .call()
297            .identity()
298            .join_identity_as_key(auth)?
299            .submit_and_watch(&mut sk)
300            .await?,
301        );
302      }
303      // Wait for joins to execute.
304      for mut res in results {
305        res.ok().await?;
306      }
307    }
308    Ok(users)
309  }
310
311  pub async fn key_records(&self, account: AccountId) -> Result<Option<KeyRecord<AccountId>>> {
312    Ok(self.api.query().identity().key_records(account).await?)
313  }
314
315  pub async fn get_did(&self, account: AccountId) -> Result<Option<IdentityId>> {
316    let did = match self.key_records(account).await? {
317      Some(KeyRecord::PrimaryKey(did)) => Some(did),
318      #[cfg(feature = "polymesh_v6")]
319      Some(KeyRecord::SecondaryKey(did, _)) => Some(did),
320      #[cfg(feature = "polymesh_v7")]
321      Some(KeyRecord::SecondaryKey(did)) => Some(did),
322      _ => None,
323    };
324    Ok(did)
325  }
326
327  pub async fn register_and_fund(&mut self, account: AccountId) -> Result<IdentityId> {
328    let did = match self.get_did(account).await? {
329      Some(did) => did,
330      None => {
331        // `account` is not linked to an identity.
332        // Create a new identity with `account` as the primary key.
333        let mut res = self
334          .api
335          .call()
336          .utility()
337          .batch(vec![
338            self
339              .api
340              .call()
341              .identity()
342              .cdd_register_did_with_cdd(account, vec![], None)?
343              .into(),
344            self
345              .api
346              .call()
347              .balances()
348              .transfer(account.into(), self.init_polyx)?
349              .into(),
350          ])?
351          .execute(&mut self.cdd)
352          .await?;
353        get_identity_id(&mut res).await?.unwrap()
354      }
355    };
356    Ok(did)
357  }
358
359  pub fn gen_ticker(&self) -> Ticker {
360    use rand::{thread_rng, Rng};
361    let mut data = [0u8; 6];
362    thread_rng().fill(&mut data[..]);
363    let name = hex::encode_upper(data);
364    Ticker(name.as_bytes().try_into().unwrap())
365  }
366
367  #[cfg(feature = "polymesh_v7")]
368  pub fn gen_asset_id(&self) -> AssetId {
369    use rand::{thread_rng, Rng};
370    let mut data = [0u8; 8];
371    thread_rng().fill(&mut data[..]);
372    let fake_name = hex::encode_upper(data);
373    AssetId(fake_name.as_bytes().try_into().unwrap())
374  }
375}