polymesh_api_tester/
tester.rs

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