cala_server/graphql/
schema.rs

1use async_graphql::{dataloader::*, types::connection::*, *};
2use cala_ledger::{balance::AccountBalance, primitives::*, tx_template::NewParamDefinition};
3
4use crate::{app::CalaApp, extension::*};
5
6use super::{
7    account::*, account_set::*, balance::*, journal::*, loader::*, primitives::*, transaction::*,
8    tx_template::*, velocity::*,
9};
10
11#[derive(Default)]
12pub struct CoreQuery<E: QueryExtensionMarker> {
13    _phantom: std::marker::PhantomData<E>,
14}
15
16#[Object(name = "Query")]
17impl<E: QueryExtensionMarker> CoreQuery<E> {
18    #[graphql(flatten)]
19    async fn extension(&self) -> E {
20        E::default()
21    }
22
23    async fn account(&self, ctx: &Context<'_>, id: UUID) -> async_graphql::Result<Option<Account>> {
24        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
25        Ok(loader.load_one(AccountId::from(id)).await?)
26    }
27
28    async fn account_by_external_id(
29        &self,
30        ctx: &Context<'_>,
31        external_id: String,
32    ) -> async_graphql::Result<Option<Account>> {
33        let app = ctx.data_unchecked::<CalaApp>();
34        match app
35            .ledger()
36            .accounts()
37            .find_by_external_id(external_id)
38            .await
39        {
40            Ok(account) => Ok(Some(account.into())),
41            Err(cala_ledger::account::error::AccountError::CouldNotFindByExternalId(_)) => Ok(None),
42            Err(err) => Err(err.into()),
43        }
44    }
45
46    async fn account_by_code(
47        &self,
48        ctx: &Context<'_>,
49        code: String,
50    ) -> async_graphql::Result<Option<Account>> {
51        let app = ctx.data_unchecked::<CalaApp>();
52        match app.ledger().accounts().find_by_code(code).await {
53            Ok(account) => Ok(Some(account.into())),
54            Err(cala_ledger::account::error::AccountError::CouldNotFindByCode(_)) => Ok(None),
55            Err(err) => Err(err.into()),
56        }
57    }
58
59    async fn accounts(
60        &self,
61        ctx: &Context<'_>,
62        first: i32,
63        after: Option<String>,
64    ) -> Result<Connection<AccountsByNameCursor, Account, EmptyFields, EmptyFields>> {
65        let app = ctx.data_unchecked::<CalaApp>();
66        query(
67            after,
68            None,
69            Some(first),
70            None,
71            |after, _, first, _| async move {
72                let first = first.expect("First always exists");
73                let result = app
74                    .ledger()
75                    .accounts()
76                    .list(cala_ledger::es_entity::PaginatedQueryArgs { first, after })
77                    .await?;
78                let mut connection = Connection::new(false, result.has_next_page);
79                connection
80                    .edges
81                    .extend(result.entities.into_iter().map(|entity| {
82                        let cursor = AccountsByNameCursor::from(&entity);
83                        Edge::new(cursor, Account::from(entity))
84                    }));
85                Ok::<_, async_graphql::Error>(connection)
86            },
87        )
88        .await
89    }
90
91    async fn account_set(
92        &self,
93        ctx: &Context<'_>,
94        id: UUID,
95    ) -> async_graphql::Result<Option<AccountSet>> {
96        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
97        Ok(loader.load_one(AccountSetId::from(id)).await?)
98    }
99
100    async fn journal(&self, ctx: &Context<'_>, id: UUID) -> async_graphql::Result<Option<Journal>> {
101        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
102        Ok(loader.load_one(JournalId::from(id)).await?)
103    }
104
105    async fn balance(
106        &self,
107        ctx: &Context<'_>,
108        journal_id: UUID,
109        account_id: UUID,
110        currency: CurrencyCode,
111    ) -> async_graphql::Result<Option<Balance>> {
112        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
113        let journal_id = JournalId::from(journal_id);
114        let account_id = AccountId::from(account_id);
115        let currency = Currency::from(currency);
116        let balance: Option<AccountBalance> =
117            loader.load_one((journal_id, account_id, currency)).await?;
118        Ok(balance.map(Balance::from))
119    }
120
121    async fn transaction(
122        &self,
123        ctx: &Context<'_>,
124        id: UUID,
125    ) -> async_graphql::Result<Option<Transaction>> {
126        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
127        Ok(loader.load_one(TransactionId::from(id)).await?)
128    }
129
130    async fn transaction_by_external_id(
131        &self,
132        ctx: &Context<'_>,
133        external_id: String,
134    ) -> async_graphql::Result<Option<Transaction>> {
135        let app = ctx.data_unchecked::<CalaApp>();
136        match app
137            .ledger()
138            .transactions()
139            .find_by_external_id(external_id)
140            .await
141        {
142            Ok(transaction) => Ok(Some(transaction.into())),
143            Err(cala_ledger::transaction::error::TransactionError::CouldNotFindByExternalId(_)) => {
144                Ok(None)
145            }
146            Err(err) => Err(err.into()),
147        }
148    }
149
150    async fn tx_template(
151        &self,
152        ctx: &Context<'_>,
153        id: UUID,
154    ) -> async_graphql::Result<Option<TxTemplate>> {
155        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
156        Ok(loader.load_one(TxTemplateId::from(id)).await?)
157    }
158
159    async fn tx_template_by_code(
160        &self,
161        ctx: &Context<'_>,
162        code: String,
163    ) -> async_graphql::Result<Option<TxTemplate>> {
164        let app = ctx.data_unchecked::<CalaApp>();
165        match app.ledger().tx_templates().find_by_code(code).await {
166            Ok(tx_template) => Ok(Some(tx_template.into())),
167            Err(cala_ledger::tx_template::error::TxTemplateError::CouldNotFindByCode(_)) => {
168                Ok(None)
169            }
170            Err(err) => Err(err.into()),
171        }
172    }
173
174    async fn velocity_limit(
175        &self,
176        ctx: &Context<'_>,
177        id: UUID,
178    ) -> async_graphql::Result<Option<VelocityLimit>> {
179        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
180        Ok(loader.load_one(VelocityLimitId::from(id)).await?)
181    }
182
183    async fn velocity_control(
184        &self,
185        ctx: &Context<'_>,
186        id: UUID,
187    ) -> async_graphql::Result<Option<VelocityControl>> {
188        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
189        Ok(loader.load_one(VelocityControlId::from(id)).await?)
190    }
191}
192
193#[derive(Default)]
194pub struct CoreMutation<E: MutationExtensionMarker> {
195    _phantom: std::marker::PhantomData<E>,
196}
197
198#[Object(name = "Mutation")]
199impl<E: MutationExtensionMarker> CoreMutation<E> {
200    #[graphql(flatten)]
201    async fn extension(&self) -> E {
202        E::default()
203    }
204
205    async fn account_create(
206        &self,
207        ctx: &Context<'_>,
208        input: AccountCreateInput,
209    ) -> Result<AccountCreatePayload> {
210        let app = ctx.data_unchecked::<CalaApp>();
211        let mut op = ctx
212            .data_unchecked::<DbOp>()
213            .try_lock()
214            .expect("Lock held concurrently");
215        let mut builder = cala_ledger::account::NewAccount::builder();
216        builder
217            .id(input.account_id)
218            .name(input.name)
219            .code(input.code)
220            .normal_balance_type(input.normal_balance_type)
221            .status(input.status);
222
223        if let Some(external_id) = input.external_id {
224            builder.external_id(external_id);
225        }
226        if let Some(description) = input.description {
227            builder.description(description);
228        }
229        if let Some(metadata) = input.metadata {
230            builder.metadata(metadata)?;
231        }
232        let account = app
233            .ledger()
234            .accounts()
235            .create_in_op(&mut *op, builder.build()?)
236            .await?;
237
238        if let Some(account_set_ids) = input.account_set_ids {
239            for id in account_set_ids {
240                app.ledger()
241                    .account_sets()
242                    .add_member_in_op(&mut *op, AccountSetId::from(id), account.id())
243                    .await?;
244            }
245        }
246
247        Ok(account.into())
248    }
249
250    async fn account_update(
251        &self,
252        ctx: &Context<'_>,
253        id: UUID,
254        input: AccountUpdateInput,
255    ) -> Result<AccountUpdatePayload> {
256        let app = ctx.data_unchecked::<CalaApp>();
257        let mut op = ctx
258            .data_unchecked::<DbOp>()
259            .try_lock()
260            .expect("Lock held concurrently");
261
262        let mut builder = cala_ledger::account::AccountUpdate::default();
263        if let Some(name) = input.name {
264            builder.name(name);
265        }
266        if let Some(code) = input.code {
267            builder.code(code);
268        }
269        if let Some(normal_balance_type) = input.normal_balance_type {
270            builder.normal_balance_type(normal_balance_type);
271        }
272        if let Some(status) = input.status {
273            builder.status(status);
274        }
275        if let Some(external_id) = input.external_id {
276            builder.external_id(external_id);
277        }
278        if let Some(description) = input.description {
279            builder.description(description);
280        }
281        if let Some(metadata) = input.metadata {
282            builder.metadata(metadata)?;
283        }
284
285        let mut account = app.ledger().accounts().find(AccountId::from(id)).await?;
286        if account.update(builder).did_execute() {
287            app.ledger()
288                .accounts()
289                .persist_in_op(&mut *op, &mut account)
290                .await?;
291        }
292
293        Ok(account.into())
294    }
295
296    async fn account_set_create(
297        &self,
298        ctx: &Context<'_>,
299        input: AccountSetCreateInput,
300    ) -> Result<AccountSetCreatePayload> {
301        let app = ctx.data_unchecked::<CalaApp>();
302        let mut op = ctx
303            .data_unchecked::<DbOp>()
304            .try_lock()
305            .expect("Lock held concurrently");
306        let mut builder = cala_ledger::account_set::NewAccountSet::builder();
307        builder
308            .id(input.account_set_id)
309            .journal_id(input.journal_id)
310            .name(input.name)
311            .normal_balance_type(input.normal_balance_type);
312
313        if let Some(description) = input.description {
314            builder.description(description);
315        }
316        if let Some(metadata) = input.metadata {
317            builder.metadata(metadata)?;
318        }
319        let account_set = app
320            .ledger()
321            .account_sets()
322            .create_in_op(&mut *op, builder.build()?)
323            .await?;
324
325        Ok(account_set.into())
326    }
327
328    async fn account_set_update(
329        &self,
330        ctx: &Context<'_>,
331        id: UUID,
332        input: AccountSetUpdateInput,
333    ) -> Result<AccountSetUpdatePayload> {
334        let app = ctx.data_unchecked::<CalaApp>();
335        let mut op = ctx
336            .data_unchecked::<DbOp>()
337            .try_lock()
338            .expect("Lock held concurrently");
339        let mut builder = cala_ledger::account_set::AccountSetUpdate::default();
340        if let Some(name) = input.name {
341            builder.name(name);
342        }
343        if let Some(desc) = input.description {
344            builder.description(desc);
345        }
346        if let Some(normal_balance_type) = input.normal_balance_type {
347            builder.normal_balance_type(normal_balance_type);
348        }
349
350        if let Some(metadata) = input.metadata {
351            builder.metadata(metadata)?;
352        }
353
354        let mut account_set = app
355            .ledger()
356            .account_sets()
357            .find(AccountSetId::from(id))
358            .await?;
359        if account_set.update(builder).did_execute() {
360            app.ledger()
361                .account_sets()
362                .persist_in_op(&mut *op, &mut account_set)
363                .await?;
364        }
365
366        Ok(account_set.into())
367    }
368
369    async fn add_to_account_set(
370        &self,
371        ctx: &Context<'_>,
372        input: AddToAccountSetInput,
373    ) -> Result<AddToAccountSetPayload> {
374        let app = ctx.data_unchecked::<CalaApp>();
375        let mut op = ctx
376            .data_unchecked::<DbOp>()
377            .try_lock()
378            .expect("Lock held concurrently");
379
380        let account_set = app
381            .ledger()
382            .account_sets()
383            .add_member_in_op(&mut *op, AccountSetId::from(input.account_set_id), input)
384            .await?;
385
386        Ok(account_set.into())
387    }
388
389    async fn remove_from_account_set(
390        &self,
391        ctx: &Context<'_>,
392        input: RemoveFromAccountSetInput,
393    ) -> Result<RemoveFromAccountSetPayload> {
394        let app = ctx.data_unchecked::<CalaApp>();
395        let mut op = ctx
396            .data_unchecked::<DbOp>()
397            .try_lock()
398            .expect("Lock held concurrently");
399
400        let account_set = app
401            .ledger()
402            .account_sets()
403            .remove_member_in_op(&mut *op, AccountSetId::from(input.account_set_id), input)
404            .await?;
405
406        Ok(account_set.into())
407    }
408
409    async fn journal_create(
410        &self,
411        ctx: &Context<'_>,
412        input: JournalCreateInput,
413    ) -> Result<JournalCreatePayload> {
414        let app = ctx.data_unchecked::<CalaApp>();
415        let mut op = ctx
416            .data_unchecked::<DbOp>()
417            .try_lock()
418            .expect("Lock held concurrently");
419        let mut builder = cala_ledger::journal::NewJournal::builder();
420        builder
421            .id(input.journal_id)
422            .name(input.name)
423            .status(input.status);
424        if let Some(description) = input.description {
425            builder.description(description);
426        }
427        let journal = app
428            .ledger()
429            .journals()
430            .create_in_op(&mut *op, builder.build()?)
431            .await?;
432
433        Ok(journal.into())
434    }
435
436    async fn journal_update(
437        &self,
438        ctx: &Context<'_>,
439        id: UUID,
440        input: JournalUpdateInput,
441    ) -> Result<JournalUpdatePayload> {
442        let app = ctx.data_unchecked::<CalaApp>();
443        let mut op = ctx
444            .data_unchecked::<DbOp>()
445            .try_lock()
446            .expect("Lock held concurrently");
447
448        let mut builder = cala_ledger::journal::JournalUpdate::default();
449        if let Some(name) = input.name {
450            builder.name(name);
451        }
452        if let Some(status) = input.status {
453            builder.status(status);
454        }
455        if let Some(description) = input.description {
456            builder.description(description);
457        }
458
459        let mut journal = app.ledger().journals().find(JournalId::from(id)).await?;
460        if journal.update(builder).did_execute() {
461            app.ledger()
462                .journals()
463                .persist_in_op(&mut *op, &mut journal)
464                .await?;
465        }
466
467        Ok(journal.into())
468    }
469
470    async fn tx_template_create(
471        &self,
472        ctx: &Context<'_>,
473        input: TxTemplateCreateInput,
474    ) -> Result<TxTemplateCreatePayload> {
475        let app = ctx.data_unchecked::<CalaApp>();
476        let mut op = ctx
477            .data_unchecked::<DbOp>()
478            .try_lock()
479            .expect("Lock held concurrently");
480        let mut new_tx_template_transaction_builder =
481            cala_ledger::tx_template::NewTxTemplateTransaction::builder();
482        let TxTemplateTransactionInput {
483            effective,
484            journal_id,
485            correlation_id,
486            external_id,
487            description,
488            metadata,
489        } = input.transaction;
490        new_tx_template_transaction_builder
491            .effective(effective)
492            .journal_id(journal_id);
493        if let Some(correlation_id) = correlation_id {
494            new_tx_template_transaction_builder.correlation_id(correlation_id);
495        };
496        if let Some(external_id) = external_id {
497            new_tx_template_transaction_builder.external_id(external_id);
498        };
499        if let Some(description) = description {
500            new_tx_template_transaction_builder.description(description);
501        };
502        if let Some(metadata) = metadata {
503            new_tx_template_transaction_builder.metadata(metadata);
504        }
505        let new_transaction = new_tx_template_transaction_builder.build()?;
506
507        let mut new_params = Vec::new();
508        if let Some(params) = input.params {
509            for param in params {
510                let mut param_builder = NewParamDefinition::builder();
511                param_builder.name(param.name).r#type(param.r#type.into());
512                if let Some(default) = param.default {
513                    param_builder.default_expr(default);
514                }
515                if let Some(desc) = param.description {
516                    param_builder.description(desc);
517                }
518                let new_param = param_builder.build()?;
519                new_params.push(new_param);
520            }
521        }
522
523        let mut new_entries = Vec::new();
524        for entry in input.entries {
525            let TxTemplateEntryInput {
526                entry_type,
527                account_id,
528                layer,
529                direction,
530                units,
531                currency,
532                description,
533            } = entry;
534            let mut new_entry_input_builder =
535                cala_ledger::tx_template::NewTxTemplateEntry::builder();
536            new_entry_input_builder
537                .entry_type(entry_type)
538                .account_id(account_id)
539                .layer(layer)
540                .direction(direction)
541                .units(units)
542                .currency(currency);
543            if let Some(desc) = description {
544                new_entry_input_builder.description(desc);
545            }
546            let new_entry_input = new_entry_input_builder.build()?;
547            new_entries.push(new_entry_input);
548        }
549
550        let mut new_tx_template_builder = cala_ledger::tx_template::NewTxTemplate::builder();
551        new_tx_template_builder
552            .id(input.tx_template_id)
553            .code(input.code)
554            .transaction(new_transaction)
555            .params(new_params)
556            .entries(new_entries);
557        if let Some(desc) = input.description {
558            new_tx_template_builder.description(desc);
559        }
560        if let Some(metadata) = input.metadata {
561            new_tx_template_builder.metadata(metadata)?;
562        }
563        let new_tx_template = new_tx_template_builder.build()?;
564
565        let tx_template = app
566            .ledger()
567            .tx_templates()
568            .create_in_op(&mut *op, new_tx_template)
569            .await?;
570
571        Ok(tx_template.into())
572    }
573
574    async fn transaction_post(
575        &self,
576        ctx: &Context<'_>,
577        input: TransactionInput,
578    ) -> Result<TransactionPostPayload> {
579        let app = ctx.data_unchecked::<CalaApp>();
580        let mut op = ctx
581            .data_unchecked::<DbOp>()
582            .try_lock()
583            .expect("Lock held concurrently");
584        let params = input.params.map(cala_ledger::tx_template::Params::from);
585        let transaction = app
586            .ledger()
587            .post_transaction_in_op(
588                &mut *op,
589                input.transaction_id.into(),
590                &input.tx_template_code,
591                params.unwrap_or_default(),
592            )
593            .await?;
594        Ok(transaction.into())
595    }
596
597    async fn velocity_limit_create(
598        &self,
599        ctx: &Context<'_>,
600        input: VelocityLimitCreateInput,
601    ) -> Result<VelocityLimitCreatePayload> {
602        let app = ctx.data_unchecked::<CalaApp>();
603        let mut op = ctx
604            .data_unchecked::<DbOp>()
605            .try_lock()
606            .expect("Lock held concurrently");
607
608        let mut new_velocity_limit_builder = cala_ledger::velocity::NewVelocityLimit::builder();
609        new_velocity_limit_builder
610            .id(input.velocity_limit_id)
611            .name(input.name)
612            .description(input.description);
613
614        if let Some(condition) = input.condition {
615            new_velocity_limit_builder.condition(condition);
616        }
617
618        if let Some(currency) = input.currency {
619            new_velocity_limit_builder.currency(currency);
620        }
621
622        let mut new_window = Vec::new();
623        for partition_key_input in input.window {
624            let mut new_partition_key_builder = cala_ledger::velocity::NewPartitionKey::builder();
625            let partition_key = new_partition_key_builder
626                .alias(partition_key_input.alias)
627                .value(partition_key_input.value)
628                .build()?;
629
630            new_window.push(partition_key);
631        }
632        new_velocity_limit_builder.window(new_window);
633
634        let mut new_limit_builder = cala_ledger::velocity::NewLimit::builder();
635
636        if let Some(timestamp_source) = input.limit.timestamp_source {
637            new_limit_builder.timestamp_source(timestamp_source);
638        }
639
640        let mut new_balance_limits = Vec::new();
641        for balance_limit_input in input.limit.balance {
642            let mut new_balance_limit_builder = cala_ledger::velocity::NewBalanceLimit::builder();
643            new_balance_limit_builder
644                .limit_type(balance_limit_input.limit_type)
645                .layer(balance_limit_input.layer)
646                .amount(balance_limit_input.amount)
647                .enforcement_direction(balance_limit_input.normal_balance_type);
648
649            if let Some(start) = balance_limit_input.start {
650                new_balance_limit_builder.start(start);
651            }
652
653            if let Some(end) = balance_limit_input.end {
654                new_balance_limit_builder.end(end);
655            }
656
657            let new_balance_limit = new_balance_limit_builder.build()?;
658            new_balance_limits.push(new_balance_limit);
659        }
660        new_limit_builder.balance(new_balance_limits);
661        let new_limit = new_limit_builder.build()?;
662
663        new_velocity_limit_builder.limit(new_limit);
664
665        if let Some(params) = input.params {
666            let mut new_params = Vec::new();
667            for param in params {
668                let mut param_builder = NewParamDefinition::builder();
669                param_builder.name(param.name).r#type(param.r#type.into());
670                if let Some(default) = param.default {
671                    param_builder.default_expr(default);
672                }
673                if let Some(description) = param.description {
674                    param_builder.description(description);
675                }
676                let new_param = param_builder.build()?;
677                new_params.push(new_param);
678            }
679            new_velocity_limit_builder.params(new_params);
680        }
681
682        let new_velocity_limit = new_velocity_limit_builder.build()?;
683
684        let velocity_limit = app
685            .ledger()
686            .velocities()
687            .create_limit_in_op(&mut *op, new_velocity_limit)
688            .await?;
689
690        Ok(velocity_limit.into())
691    }
692
693    async fn velocity_control_create(
694        &self,
695        ctx: &Context<'_>,
696        input: VelocityControlCreateInput,
697    ) -> Result<VelocityControlCreatePayload> {
698        let app = ctx.data_unchecked::<CalaApp>();
699        let mut op = ctx
700            .data_unchecked::<DbOp>()
701            .try_lock()
702            .expect("Lock held concurrently");
703
704        let mut new_velocity_control_builder = cala_ledger::velocity::NewVelocityControl::builder();
705        new_velocity_control_builder
706            .id(input.velocity_control_id)
707            .name(input.name)
708            .description(input.description);
709
710        if let Some(condition) = input.condition {
711            new_velocity_control_builder.condition(condition);
712        }
713
714        let mut new_velocity_enforcement_builder =
715            cala_ledger::velocity::NewVelocityEnforcement::builder();
716        new_velocity_enforcement_builder.action(input.enforcement.velocity_enforcement_action);
717        let new_velocity_enforcement = new_velocity_enforcement_builder.build()?;
718
719        new_velocity_control_builder.enforcement(new_velocity_enforcement);
720
721        let new_velocity_control = new_velocity_control_builder.build()?;
722        let velocity_control = app
723            .ledger()
724            .velocities()
725            .create_control_in_op(&mut *op, new_velocity_control)
726            .await?;
727
728        Ok(velocity_control.into())
729    }
730
731    async fn velocity_control_add_limit(
732        &self,
733        ctx: &Context<'_>,
734        input: VelocityControlAddLimitInput,
735    ) -> Result<VelocityControlAddLimitPayload> {
736        let app = ctx.data_unchecked::<CalaApp>();
737        let mut op = ctx
738            .data_unchecked::<DbOp>()
739            .try_lock()
740            .expect("Lock held concurrently");
741
742        let velocity_limit = app
743            .ledger()
744            .velocities()
745            .add_limit_to_control_in_op(
746                &mut *op,
747                input.velocity_control_id.into(),
748                input.velocity_limit_id.into(),
749            )
750            .await?;
751
752        Ok(velocity_limit.into())
753    }
754
755    async fn velocity_control_attach(
756        &self,
757        ctx: &Context<'_>,
758        input: VelocityControlAttachInput,
759    ) -> Result<VelocityControlAttachPayload> {
760        let app = ctx.data_unchecked::<CalaApp>();
761        let mut op = ctx
762            .data_unchecked::<DbOp>()
763            .try_lock()
764            .expect("Lock held concurrently");
765        let params = cala_ledger::tx_template::Params::from(input.params);
766
767        let velocity_control = app
768            .ledger()
769            .velocities()
770            .attach_control_to_account_in_op(
771                &mut *op,
772                input.velocity_control_id.into(),
773                input.account_id.into(),
774                params,
775            )
776            .await?;
777
778        Ok(velocity_control.into())
779    }
780}