Skip to main content

coven_database/store/store_session/
store_authority.rs

1use crate::*;
2use crate::{RetainedReplayAuthority, RetainedReplayGenesisAuthority};
3use coven_protocol::store_commit::{
4    ResolvedStoreDeviceState, StoreAckRef, StoreDeviceRegistrationRef,
5};
6use rusqlite::OptionalExtension;
7
8use super::*;
9
10impl StoreSession<'_> {
11    fn membership_head_cursors(&mut self) -> Result<InitialStoreMembershipAuthority, DbError> {
12        InitialStoreMembershipAuthority::load_on(self.conn)
13    }
14
15    fn persist_membership_head_cursors(
16        &mut self,
17        head_refs: Vec<coven_protocol::membership::MembershipHeadRef>,
18    ) -> Result<(), DbError> {
19        let transaction = self.conn.unchecked_transaction().map_err(DbError::from)?;
20        InitialStoreMembershipAuthority { head_refs }.install_on(&transaction)?;
21        transaction.commit().map_err(DbError::from)
22    }
23
24    fn validated_store_owner(
25        &mut self,
26        expected_root: coven_protocol::store_commit::StoreRootRef,
27    ) -> Result<String, DbError> {
28        let records = crate::store::store_session::StoreRecords::new(self.conn, self.store_dir);
29        let (root, protocol_root) = self
30            .verified_store_authority
31            .root_authority_on(records)?
32            .ok_or(DbError::StoreRootHashMissing)?;
33        if root != expected_root {
34            return Err(DbError::Message(
35                "local Store root differs from the operation authority".to_string(),
36            ));
37        }
38        let owner = get_protocol_state_on(
39            self.conn,
40            coven_protocol::membership::OWNER_PUBKEY_STATE_KEY,
41        )?
42        .ok_or_else(|| DbError::Message("Store owner anchor is absent".to_string()))?;
43        if owner != protocol_root.descriptor.founder_pubkey {
44            return Err(DbError::Message(
45                "Store owner anchor differs from its signed root".to_string(),
46            ));
47        }
48        let baseline = self
49            .verified_store_authority
50            .retained_replay_baseline_on(records)?;
51        let owner_authority = match &baseline.authority {
52            RetainedReplayAuthority::Genesis(authority) => authority.clone(),
53            RetainedReplayAuthority::InstalledSnapshot(authority) => {
54                RetainedReplayGenesisAuthority {
55                    store_root: authority.store_root.clone(),
56                    founder_registration: authority.founder_registration.clone(),
57                }
58            }
59        };
60        if owner_authority.store_root != root {
61            return Err(DbError::Message(
62                "retained replay baseline belongs to another Store root".to_string(),
63            ));
64        }
65        let founder = self.verified_store_authority.activated_registration_on(
66            records,
67            &root,
68            &owner_authority.founder_registration,
69        )?;
70        let expected_genesis = ResolvedStoreDeviceState::founder(
71            &root,
72            owner_authority.founder_registration.clone(),
73            &protocol_root.descriptor.founder_pubkey,
74            protocol_root.descriptor.founder_grant.clone(),
75            &protocol_root.descriptor.founder_recovery,
76        )
77        .map_err(DbError::from)?;
78        let stored_genesis: ResolvedStoreDeviceState = serde_json::from_str(
79            &required_protocol_state_on(self.conn, STORE_DEVICE_GENESIS_STATE_KEY)?,
80        )
81        .map_err(|error| DbError::context("Store device genesis state", error))?;
82        if founder.author_pubkey != owner || stored_genesis != expected_genesis {
83            return Err(DbError::Message(
84                "Store device genesis differs from installed founder authority".to_string(),
85            ));
86        }
87        self.verified_store_authority
88            .remember_verified_owner_anchor(owner_authority)?;
89        Ok(owner)
90    }
91
92    fn install_store_owner_anchor(
93        &mut self,
94        anchor: crate::StoreOwnerAnchor,
95        membership: InitialStoreMembershipAuthority,
96    ) -> Result<(), DbError> {
97        if self.verified_store_authority.reuses_owner_anchor(&anchor)? {
98            return self.persist_membership_head_cursors(membership.head_refs);
99        }
100        let authority = anchor.authority().clone();
101        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
102        let root_value =
103            install_store_root_authority_on(&tx, &authority.store_root, &anchor.root().bytes)?;
104        install_store_founder_state_on(
105            &tx,
106            &authority.store_root,
107            &authority.founder_registration,
108            &anchor.founder().value,
109            &anchor.founder().bytes,
110            anchor.genesis(),
111        )?;
112        set_protocol_state_on(
113            &tx,
114            coven_protocol::membership::OWNER_PUBKEY_STATE_KEY,
115            anchor.owner(),
116        )?;
117        membership.install_on(&tx)?;
118        let baseline = crate::store::store_session::StoreTransaction::new(&tx, self.store_dir)
119            .ensure_founder_replay_baseline(
120                self.schema_version,
121                self.sync_routing_hash,
122                authority.clone(),
123            )?;
124        tx.commit().map_err(DbError::from)?;
125        self.verified_store_authority.commit_installed_owner_anchor(
126            authority,
127            root_value,
128            anchor.founder().value.clone(),
129            baseline,
130        );
131        Ok(())
132    }
133
134    fn local_store_founder_graph(&mut self) -> Result<Option<Box<DurableFounderGraph>>, DbError> {
135        load_local_store_founder_graph_on(self.conn)
136    }
137
138    fn stage_store_founder_graph(
139        &mut self,
140        graph: Box<DurableFounderGraph>,
141    ) -> Result<(), DbError> {
142        let conn = self.conn;
143        let tx = conn.unchecked_transaction().map_err(DbError::from)?;
144        if let Some(existing) = load_local_store_founder_graph_on(&tx)? {
145            existing.validate()?;
146            if founder_graph_identity(&existing) == founder_graph_identity(&graph) {
147                return Ok(());
148            }
149            return Err(DbError::Message(
150                "local Store founder graph already owns different exact objects".to_string(),
151            ));
152        }
153        use coven_protocol::provider::{ExactProbeProgress, ProviderProbeJournalRecord};
154        use coven_protocol::store_creation::{
155            StoreCreationAttempt, STORE_CREATION_ATTEMPT_STATE_KEY,
156        };
157
158        let attempt_json =
159            crate::required_protocol_state_on(&tx, STORE_CREATION_ATTEMPT_STATE_KEY)?;
160        let attempt: StoreCreationAttempt = serde_json::from_str(&attempt_json)
161            .map_err(|error| DbError::context("parse Store creation attempt", error))?;
162        let StoreCreationAttempt::FounderGraphReserved(graph_reservation) = attempt else {
163            return Err(DbError::Message(
164                "Store creation attempt has not reserved the complete founder graph".to_string(),
165            ));
166        };
167        let reservation = &graph_reservation.descriptor;
168        let descriptor = &graph.root.value.descriptor;
169        let founder = &reservation.membership.founder;
170        let authority = &founder.root.authority;
171        if authority.creation_id != descriptor.creation_id
172            || authority.founder_grant != descriptor.founder_grant
173            || authority.provider_admin_grant != descriptor.founder_provider_admin.grant_id
174            || authority.binding.store != descriptor.provider
175            || authority.binding.device != descriptor.founder_provider_admin.provider
176            || authority.founder_pubkey != descriptor.founder_pubkey
177            || authority.schema_version != descriptor.schema_version
178            || authority.sync_routing_hash != descriptor.sync_routing_hash
179            || founder.root.root_slot != descriptor.root_slot
180            || reservation.current_publication_slot != descriptor.current_publication_slot
181            || founder.registration_slot != descriptor.founder_registration
182            || &reservation.recovery_slot != descriptor.founder_recovery.first_slot()
183            || descriptor.founder_membership.first_slot() != &reservation.membership.first_slot
184        {
185            return Err(DbError::Message(
186                "signed Store descriptor differs from its durable creation attempt".to_string(),
187            ));
188        }
189        if graph.registration.value.acknowledgements != graph_reservation.acknowledgements
190            || graph.initial_ack.value.last_sync != authority.founder_timestamp
191            || graph.initial_ack.value.successor.next_slot != graph_reservation.next_ack_slot
192            || graph.membership.entry.value.created_at != authority.founder_timestamp
193            || graph.membership.head.value.body.successor.next_slot
194                != graph_reservation.membership.next_head_slot
195        {
196            return Err(DbError::Message(
197                "signed founder graph differs from its durable slot reservation".to_string(),
198            ));
199        }
200
201        let exact_key = format!(
202            "provider_probe/{}",
203            hex::encode(authority.probes.exact_slots().as_bytes())
204        );
205        let exact_json = crate::required_protocol_state_on(&tx, &exact_key)?;
206        let exact: ProviderProbeJournalRecord = serde_json::from_str(&exact_json)
207            .map_err(|error| DbError::context("parse provider probe journal", error))?;
208        let ProviderProbeJournalRecord::Exact(exact) = exact else {
209            return Err(DbError::Message(
210                "Store creation exact probe id names another probe kind".to_string(),
211            ));
212        };
213        let ExactProbeProgress::ReceiptReady { receipt } = exact.progress else {
214            return Err(DbError::Message(
215                "Store creation exact probe has no terminal receipt".to_string(),
216            ));
217        };
218        if receipt != descriptor.founder_provider_admin.capability.exact_slots {
219            return Err(DbError::Message(
220                "signed Store descriptor differs from its terminal exact probe".to_string(),
221            ));
222        }
223        for key in [exact_key, STORE_CREATION_ATTEMPT_STATE_KEY.to_string()] {
224            let deleted = crate::delete_protocol_state_on(&tx, &key)?;
225            if deleted != 1 {
226                return Err(DbError::Message(
227                    "Store creation journal disappeared during typed consumption".to_string(),
228                ));
229            }
230        }
231        tx.execute(
232            "INSERT INTO local_store_protocol_root \
233         (singleton, store_root_hash, store_protocol_root_bytes, prepared_object) \
234         VALUES (1, ?1, ?2, ?3)",
235            rusqlite::params![
236                graph.root.value.object_hash().to_string(),
237                graph.root.bytes,
238                serde_json::to_string(&graph.root.prepared).map_err(|error| {
239                    DbError::context("serialize prepared Store root", error)
240                })?,
241            ],
242        )
243        .map_err(DbError::from)?;
244        tx.execute(
245            "INSERT INTO local_store_device_registration \
246         (singleton, device_id, registration_hash, registration_bytes, prepared_object, \
247          initial_ack_ref, initial_ack_bytes, initial_ack_prepared, state) \
248         VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
249            rusqlite::params![
250                graph.registration.value.device_id.to_string(),
251                graph.registration.value.registration_hash().to_string(),
252                graph.registration.bytes,
253                serde_json::to_string(&graph.registration.prepared).map_err(|error| {
254                    DbError::context("serialize prepared founder registration", error)
255                })?,
256                serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
257                    DbError::context("serialize founder initial ack ref", error)
258                })?,
259                graph.initial_ack.bytes,
260                serde_json::to_string(&graph.initial_ack.prepared).map_err(|error| {
261                    DbError::context("serialize founder initial ack object", error)
262                })?,
263                serde_json::to_string(&LocalDeviceRegistrationState::Prepared).map_err(
264                    |error| DbError::context("serialize registration journal state", error)
265                )?,
266            ],
267        )
268        .map_err(DbError::from)?;
269        tx.execute(
270            "INSERT INTO local_store_founder_graph \
271         (singleton, membership_graph) VALUES (1, ?1)",
272            rusqlite::params![
273                serde_json::to_string(&DurableFounderMembershipJournal::from_graph(
274                    &graph.membership,
275                ))
276                .map_err(|error| DbError::context("serialize founder membership graph", error))?,
277            ],
278        )
279        .map_err(DbError::from)?;
280        tx.commit().map_err(DbError::from)
281    }
282
283    fn complete_store_founder_graph(
284        &mut self,
285        expected_root: coven_protocol::store_commit::StoreRootRef,
286        expected_registration: StoreDeviceRegistrationRef,
287        expected_initial_ack: StoreAckRef,
288        expected_membership: FounderMembershipRefs,
289        current_publication: crate::ObservedStorePublication,
290    ) -> Result<(), DbError> {
291        let schema_version = self.schema_version;
292        let routing_hash = self.sync_routing_hash;
293        let verified_authority = &mut *self.verified_store_authority;
294        let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
295        let store_dir = self.store_dir;
296        let graph = load_local_store_founder_graph_on(&tx)?
297            .ok_or_else(|| DbError::Message("local Store founder graph is absent".to_string()))?;
298        let root = coven_protocol::store_commit::StoreRootRef {
299            store_root_id: graph.root.value.descriptor.store_root_id(),
300            store_root_hash: graph.root.value.object_hash(),
301            object: graph.root.prepared.reference().clone(),
302        };
303        let registration = StoreDeviceRegistrationRef::from_registration(
304            &graph.registration.value,
305            graph.registration.prepared.reference().clone(),
306        );
307        if root != expected_root
308            || registration != expected_registration
309            || graph.initial_ack_ref != expected_initial_ack
310            || graph.membership.entry_ref != expected_membership.entry
311            || graph.membership.head_ref != expected_membership.head
312        {
313            return Err(DbError::Message(
314                "verified founder graph differs from its durable exact references".to_string(),
315            ));
316        }
317        let founder_authority =
318            coven_protocol::store_commit::StoreDeviceRegistrationActivation::Founder {
319                root: root.clone(),
320            };
321        let device_genesis = ResolvedStoreDeviceState::founder(
322            &root,
323            registration.clone(),
324            &graph.root.value.descriptor.founder_pubkey,
325            graph.root.value.descriptor.founder_grant.clone(),
326            &graph.root.value.descriptor.founder_recovery,
327        )
328        .map_err(DbError::from)?;
329        let device_genesis_json = serde_json::to_string(&device_genesis)
330            .map_err(|error| DbError::context("serialize Store device genesis state", error))?;
331        let device_id = registration.device_id.to_string();
332        let registration_hash = registration.registration_hash.to_string();
333        match &graph.registration_state {
334            LocalDeviceRegistrationState::Prepared
335            | LocalDeviceRegistrationState::RegistrationPublished
336            | LocalDeviceRegistrationState::RegistrationActivated { .. } => {
337                return Err(DbError::Message(
338                    "founder registration and initial acknowledgement are not exact-created"
339                        .to_string(),
340                ));
341            }
342            LocalDeviceRegistrationState::Created => {}
343            LocalDeviceRegistrationState::Activated { authority } => {
344                if authority != &founder_authority {
345                    return Err(DbError::Message(
346                        "founder registration journal carries another activation authority"
347                            .to_string(),
348                    ));
349                }
350                let store_transaction =
351                    crate::store::store_session::StoreTransaction::new(&tx, store_dir);
352                let installed = store_transaction.root_authority(verified_authority)?;
353                let installed_registration = store_transaction.activated_registration(
354                    verified_authority,
355                    &root,
356                    &registration,
357                )?;
358                let stored: Option<(String, String, String)> = tx
359                    .query_row(
360                        "SELECT registration_object, activation_authority, registration_hash \
361                         FROM store_device_registration_activations WHERE device_id = ?1",
362                        [&device_id],
363                        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
364                    )
365                    .optional()
366                    .map_err(DbError::from)?;
367                let ack: Option<String> = tx
368                    .query_row(
369                        "SELECT ack_ref FROM published_store_acks WHERE singleton = 1",
370                        [],
371                        |row| row.get(0),
372                    )
373                    .optional()
374                    .map_err(DbError::from)?;
375                let stored_device_genesis =
376                    crate::get_protocol_state_on(&tx, STORE_DEVICE_GENESIS_STATE_KEY)?;
377                if installed
378                    .as_ref()
379                    .map(|(reference, value)| (reference, value))
380                    != Some((&root, &graph.root.value))
381                    || installed_registration != graph.registration.value
382                    || stored
383                        != Some((
384                            serde_json::to_string(&registration).map_err(|error| {
385                                DbError::context("serialize founder registration ref", error)
386                            })?,
387                            serde_json::to_string(&founder_authority).map_err(|error| {
388                                DbError::context("serialize founder authority", error)
389                            })?,
390                            registration.registration_hash.to_string(),
391                        ))
392                    || ack
393                        != Some(
394                            serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
395                                DbError::context("serialize founder ack ref", error)
396                            })?,
397                        )
398                    || stored_device_genesis.as_deref() != Some(&device_genesis_json)
399                {
400                    return Err(DbError::Message(
401                        "activated founder journal differs from installed exact authority"
402                            .to_string(),
403                    ));
404                }
405                let publication =
406                    super::observed_store_publication::load_store_current_publication_on(&tx)?;
407                if publication.record() != current_publication.record()
408                    || publication.observed_version() != Some(current_publication.version())
409                {
410                    return Err(DbError::Message(
411                        "activated founder journal differs from its Store publication record"
412                            .to_string(),
413                    ));
414                }
415                let owner_authority = RetainedReplayGenesisAuthority {
416                    store_root: root.clone(),
417                    founder_registration: registration.clone(),
418                };
419                let baseline_matches = {
420                    let baseline =
421                        crate::store::store_session::StoreTransaction::new(&tx, store_dir)
422                            .retained_replay_baseline(verified_authority)?;
423                    baseline.schema_version == schema_version
424                        && baseline.routing_hash == routing_hash
425                        && baseline.authority
426                            == RetainedReplayAuthority::Genesis(owner_authority.clone())
427                };
428                if !baseline_matches {
429                    return Err(DbError::Message(
430                        "activated founder state differs from its generation-zero replay baseline"
431                            .to_string(),
432                    ));
433                }
434                verified_authority.remember_verified_owner_anchor(owner_authority)?;
435                return Ok(());
436            }
437        }
438        let root_value = install_store_root_authority_on(&tx, &root, &graph.root.bytes)?;
439        current_publication
440            .record()
441            .verify_genesis(
442                root.store_root_hash,
443                &graph.root.value.descriptor.founder_pubkey,
444            )
445            .map_err(DbError::from)?;
446        super::observed_store_publication::install_genesis_store_publication_on(
447            &tx,
448            current_publication.record(),
449            current_publication.version(),
450        )?;
451        let activation = serde_json::to_string(
452            &coven_protocol::store_commit::StoreDeviceRegistrationActivation::Founder {
453                root: root.clone(),
454            },
455        )
456        .map_err(|error| DbError::context("serialize founder registration activation", error))?;
457        let journal_state = serde_json::to_string(&LocalDeviceRegistrationState::Activated {
458            authority: founder_authority,
459        })
460        .map_err(|error| DbError::context("serialize founder registration journal", error))?;
461        let updated = tx
462            .execute(
463                "UPDATE local_store_device_registration SET state = ?1 \
464                 WHERE singleton = 1 AND device_id = ?2 AND registration_hash = ?3 \
465                   AND initial_ack_ref = ?4 AND state = ?5",
466                rusqlite::params![
467                    journal_state,
468                    &device_id,
469                    &registration_hash,
470                    serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
471                        DbError::context("serialize founder initial ack ref", error)
472                    })?,
473                    serde_json::to_string(&LocalDeviceRegistrationState::Created).map_err(
474                        |error| DbError::context("serialize created journal state", error)
475                    )?,
476                ],
477            )
478            .map_err(DbError::from)?;
479        if updated != 1 {
480            return Err(DbError::Message(
481                "founder registration journal did not activate".to_string(),
482            ));
483        }
484        tx.execute(
485            "INSERT INTO store_device_registration_activations \
486             (device_id, registration_hash, author_pubkey, device_signing_pubkey, \
487              registration_bytes, registration_object, activation_authority) \
488             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
489            rusqlite::params![
490                &device_id,
491                &registration_hash,
492                graph.registration.value.author_pubkey,
493                graph.registration.value.device_signing_pubkey,
494                graph.registration.bytes,
495                serde_json::to_string(&registration).map_err(|error| {
496                    DbError::context("serialize founder registration ref", error)
497                })?,
498                activation,
499            ],
500        )
501        .map_err(DbError::from)?;
502        tx.execute(
503            "INSERT INTO published_store_acks \
504             (singleton, ack_ref, successor_slot) VALUES (1, ?1, ?2)",
505            rusqlite::params![
506                serde_json::to_string(&graph.initial_ack_ref).map_err(|error| {
507                    DbError::context("serialize founder initial ack ref", error)
508                })?,
509                serde_json::to_string(&graph.initial_ack.value.successor.next_slot)
510                    .map_err(|error| DbError::context("serialize founder ack successor", error))?,
511            ],
512        )
513        .map_err(DbError::from)?;
514        for (key, value) in [
515            (LOCAL_DEVICE_ID_STATE_KEY, device_id),
516            (STORE_DEVICE_GENESIS_STATE_KEY, device_genesis_json),
517        ] {
518            crate::set_protocol_state_on(&tx, key, &value)?;
519        }
520        crate::set_protocol_state_on(
521            &tx,
522            coven_protocol::membership::OWNER_PUBKEY_STATE_KEY,
523            &graph.root.value.descriptor.founder_pubkey,
524        )?;
525        crate::InitialStoreMembershipAuthority {
526            head_refs: vec![graph.membership.head_ref.clone()],
527        }
528        .install_on(&tx)?;
529        let baseline = crate::store::store_session::StoreTransaction::new(&tx, store_dir)
530            .install_generation_zero_replay_baseline(
531                schema_version,
532                routing_hash,
533                RetainedReplayGenesisAuthority {
534                    store_root: root.clone(),
535                    founder_registration: registration.clone(),
536                },
537            )?;
538        tx.commit().map_err(DbError::from)?;
539        verified_authority.commit_installed_owner_anchor(
540            RetainedReplayGenesisAuthority {
541                store_root: root,
542                founder_registration: registration,
543            },
544            root_value,
545            graph.registration.value,
546            baseline,
547        );
548        Ok(())
549    }
550}
551
552impl StoreDatabase {
553    pub async fn membership_head_cursors(
554        &self,
555    ) -> Result<crate::InitialStoreMembershipAuthority, DbError> {
556        self.call_store(|session| session.membership_head_cursors())
557            .await
558    }
559
560    pub async fn persist_membership_head_cursors(
561        &self,
562        head_refs: Vec<coven_protocol::membership::MembershipHeadRef>,
563    ) -> Result<(), DbError> {
564        self.call_store(move |session| session.persist_membership_head_cursors(head_refs))
565            .await
566    }
567
568    pub async fn local_store_root_ref(
569        &self,
570    ) -> Result<Option<coven_protocol::store_commit::StoreRootRef>, DbError> {
571        self.call_store(|session| {
572            session
573                .root_authority()
574                .map(|authority| authority.map(|(reference, _)| reference))
575        })
576        .await
577    }
578
579    pub async fn validated_store_owner(
580        &self,
581        expected_root: &coven_protocol::store_commit::StoreRootRef,
582    ) -> Result<String, DbError> {
583        let expected_root = expected_root.clone();
584        self.call_store(move |session| session.validated_store_owner(expected_root))
585            .await
586    }
587
588    pub async fn install_store_owner_anchor(
589        &self,
590        anchor: crate::StoreOwnerAnchor,
591        membership: InitialStoreMembershipAuthority,
592    ) -> Result<(), DbError> {
593        self.call_store(move |session| session.install_store_owner_anchor(anchor, membership))
594            .await
595    }
596
597    pub async fn local_store_founder_graph(
598        &self,
599    ) -> Result<Option<Box<DurableFounderGraph>>, DbError> {
600        self.call_store(|session| session.local_store_founder_graph())
601            .await
602    }
603
604    pub async fn stage_store_founder_graph(
605        &self,
606        graph: Box<DurableFounderGraph>,
607    ) -> Result<(), DbError> {
608        graph.validate()?;
609        self.call_store(move |session| session.stage_store_founder_graph(graph))
610            .await
611    }
612
613    pub async fn complete_store_founder_graph(
614        &self,
615        expected_root: coven_protocol::store_commit::StoreRootRef,
616        expected_registration: StoreDeviceRegistrationRef,
617        expected_initial_ack: StoreAckRef,
618        expected_membership: FounderMembershipRefs,
619        current_publication: crate::ObservedStorePublication,
620    ) -> Result<(), DbError> {
621        self.call_store(move |session| {
622            session.complete_store_founder_graph(
623                expected_root,
624                expected_registration,
625                expected_initial_ack,
626                expected_membership,
627                current_publication,
628            )
629        })
630        .await
631    }
632}