Skip to main content

daml_grpc/
executor.rs

1use async_trait::async_trait;
2
3use crate::data::command::{DamlCommand, DamlCreateCommand, DamlExerciseCommand};
4use crate::data::event::{DamlCreatedEvent, DamlEvent};
5use crate::data::filter::{DamlEventFormat, DamlTransactionFormat, DamlTransactionShape};
6use crate::data::value::DamlValue;
7use crate::data::{DamlCommandsDeduplicationPeriod, DamlError, DamlMinLedgerTime, DamlResult, DamlTransaction};
8use crate::service::DamlCommandService;
9use crate::util::Required;
10use crate::{DamlCommandFactory, DamlGrpcClient};
11
12/// Construct a [`DamlSimpleExecutor`].
13pub struct DamlSimpleExecutorBuilder<'a> {
14    ledger_client: &'a DamlGrpcClient,
15    act_as: Option<Vec<String>>,
16    read_as: Option<Vec<String>>,
17    workflow_id: Option<&'a str>,
18    user_id: Option<&'a str>,
19    deduplication_period: Option<DamlCommandsDeduplicationPeriod>,
20    min_ledger_time: Option<DamlMinLedgerTime>,
21    auth_token: Option<&'a str>,
22}
23
24impl<'a> DamlSimpleExecutorBuilder<'a> {
25    pub const fn new(ledger_client: &'a DamlGrpcClient) -> Self {
26        Self {
27            ledger_client,
28            act_as: None,
29            read_as: None,
30            workflow_id: None,
31            user_id: None,
32            deduplication_period: None,
33            min_ledger_time: None,
34            auth_token: None,
35        }
36    }
37
38    pub fn workflow_id(self, workflow_id: &'a str) -> Self {
39        Self {
40            workflow_id: Some(workflow_id),
41            ..self
42        }
43    }
44
45    pub fn act_as(self, act_as: impl Into<String>) -> Self {
46        Self {
47            act_as: Some(vec![act_as.into()]),
48            ..self
49        }
50    }
51
52    pub fn act_as_all(self, act_as_all: Vec<String>) -> Self {
53        Self {
54            act_as: Some(act_as_all),
55            ..self
56        }
57    }
58
59    pub fn read_as(self, read_as: impl Into<String>) -> Self {
60        Self {
61            read_as: Some(vec![read_as.into()]),
62            ..self
63        }
64    }
65
66    pub fn read_as_all(self, read_as_all: Vec<String>) -> Self {
67        Self {
68            read_as: Some(read_as_all),
69            ..self
70        }
71    }
72
73    /// v2 wire name (v1's `application_id`). Sets the submission's
74    /// `user_id`, which the participant matches against the JWT's
75    /// `sub` claim when the auth layer is user-based.
76    pub fn user_id(self, user_id: &'a str) -> Self {
77        Self {
78            user_id: Some(user_id),
79            ..self
80        }
81    }
82
83    pub fn deduplication_period(self, deduplication_period: DamlCommandsDeduplicationPeriod) -> Self {
84        Self {
85            deduplication_period: Some(deduplication_period),
86            ..self
87        }
88    }
89
90    pub fn min_ledger_time(self, min_ledger_time: DamlMinLedgerTime) -> Self {
91        Self {
92            min_ledger_time: Some(min_ledger_time),
93            ..self
94        }
95    }
96
97    /// Override any JWT token enabled in the `DamlGrpcClient`.
98    pub fn auth_token(self, auth_token: &'a str) -> Self {
99        Self {
100            auth_token: Some(auth_token),
101            ..self
102        }
103    }
104
105    pub fn build(self) -> DamlResult<DamlSimpleExecutor<'a>> {
106        if self.has_parties() {
107            Ok(DamlSimpleExecutor::new(
108                self.ledger_client,
109                self.act_as.unwrap_or_default(),
110                self.read_as.unwrap_or_default(),
111                self.workflow_id.unwrap_or("default-workflow"),
112                self.user_id.unwrap_or("default-user"),
113                self.deduplication_period,
114                self.min_ledger_time,
115                self.auth_token,
116            ))
117        } else {
118            Err(DamlError::InsufficientParties)
119        }
120    }
121
122    fn has_parties(&self) -> bool {
123        match (self.act_as.as_deref(), self.read_as.as_deref()) {
124            (None, None) => false,
125            (Some(act_as), None) => !act_as.is_empty(),
126            (None, Some(read_as)) => !read_as.is_empty(),
127            (Some(act_as), Some(read_as)) => !act_as.is_empty() || !read_as.is_empty(),
128        }
129    }
130}
131
132/// An async failable Daml command executor.
133///
134/// v2 removed the dedicated `TransactionTree` response shape: the
135/// tree-shaped (ledger-effects) view is now selectable on the same
136/// `SubmitAndWaitForTransaction` RPC via a `TransactionFormat`
137/// whose `transaction_shape = LedgerEffects`. The executor exposes
138/// that selection through [`Self::execute_for_transaction_with_effects`]
139/// which returns the same `DamlTransaction` populated with both
140/// `Created` and `Exercised` events.
141#[async_trait]
142pub trait CommandExecutor {
143    async fn execute_for_transaction(&self, command: DamlCommand) -> DamlResult<DamlTransaction>;
144    async fn execute_for_transaction_with_effects(&self, command: DamlCommand) -> DamlResult<DamlTransaction>;
145    async fn execute_create(&self, create_command: DamlCreateCommand) -> DamlResult<DamlCreatedEvent>;
146    async fn execute_exercise(&self, exercise_command: DamlExerciseCommand) -> DamlResult<DamlValue>;
147}
148
149/// A simple async Daml command executor.
150pub struct DamlSimpleExecutor<'a> {
151    ledger_client: &'a DamlGrpcClient,
152    command_factory: DamlCommandFactory,
153    auth_token: Option<&'a str>,
154}
155
156impl<'a> DamlSimpleExecutor<'a> {
157    #[allow(clippy::too_many_arguments)]
158    pub fn new(
159        ledger_client: &'a DamlGrpcClient,
160        act_as: Vec<String>,
161        read_as: Vec<String>,
162        workflow_id: &str,
163        user_id: &str,
164        deduplication_period: Option<DamlCommandsDeduplicationPeriod>,
165        min_ledger_time: Option<DamlMinLedgerTime>,
166        auth_token: Option<&'a str>,
167    ) -> Self {
168        let command_factory =
169            DamlCommandFactory::new(workflow_id, user_id, act_as, read_as, deduplication_period, min_ledger_time);
170        Self {
171            ledger_client,
172            command_factory,
173            auth_token,
174        }
175    }
176
177    pub fn act_as(&self) -> &[String] {
178        self.command_factory.act_as()
179    }
180
181    pub fn read_as(&self) -> &[String] {
182        self.command_factory.read_as()
183    }
184
185    async fn submit_and_wait_for_transaction(&self, command: DamlCommand) -> DamlResult<DamlTransaction> {
186        let commands = self.command_factory.make_command(command);
187        // Default `None` transaction-format selects ACS-delta shape
188        // with per-party wildcard filters — fine for "give me what I
189        // just submitted" use.
190        self.client().submit_and_wait_for_transaction(commands, None).await
191    }
192
193    /// Submit and wait, returning a [`DamlTransaction`] populated
194    /// with both `Created` and `Exercised` events (the `LedgerEffects`
195    /// shape; v1's `TransactionTree`).
196    async fn submit_and_wait_for_transaction_with_effects(&self, command: DamlCommand) -> DamlResult<DamlTransaction> {
197        let commands = self.command_factory.make_command(command);
198        // Build a transaction-format scoped to the submitter's parties
199        // with the LedgerEffects shape and verbose output.
200        let mut filters_by_party = std::collections::HashMap::new();
201        let wildcard = crate::data::filter::DamlFilters::default();
202        for party in self.act_as() {
203            filters_by_party.insert(party.clone(), wildcard.clone());
204        }
205        for party in self.read_as() {
206            filters_by_party.insert(party.clone(), wildcard.clone());
207        }
208        let event_format = DamlEventFormat {
209            filters_by_party,
210            filters_for_any_party: None,
211            verbose: true,
212        };
213        let format = DamlTransactionFormat {
214            event_format,
215            transaction_shape: DamlTransactionShape::LedgerEffects,
216        };
217        self.client().submit_and_wait_for_transaction(commands, Some(format)).await
218    }
219
220    fn client(&self) -> DamlCommandService<'_> {
221        match self.auth_token {
222            Some(token) => self.ledger_client.command_service().with_token(token),
223            None => self.ledger_client.command_service(),
224        }
225    }
226}
227
228#[async_trait]
229#[allow(clippy::needless_lifetimes)]
230impl CommandExecutor for DamlSimpleExecutor<'_> {
231    async fn execute_for_transaction(&self, command: DamlCommand) -> DamlResult<DamlTransaction> {
232        self.submit_and_wait_for_transaction(command).await
233    }
234
235    async fn execute_for_transaction_with_effects(&self, command: DamlCommand) -> DamlResult<DamlTransaction> {
236        self.submit_and_wait_for_transaction_with_effects(command).await
237    }
238
239    async fn execute_create(&self, create_command: DamlCreateCommand) -> Result<DamlCreatedEvent, DamlError> {
240        let mut tx = self.submit_and_wait_for_transaction(DamlCommand::Create(create_command)).await?;
241        if tx.events.is_empty() {
242            return Err(DamlError::Other("execute_create: transaction had no events".to_owned()));
243        }
244        tx.events.swap_remove(0).try_created()
245    }
246
247    /// Submit an exercise command and return the result of the first
248    /// `Exercised` event whose `exercise_result` is populated.
249    ///
250    /// Returns [`DamlError::MissingRequiredField`] if the transaction
251    /// contains no `Exercised` event (e.g. a mis-routed `Create`
252    /// command) or if every `Exercised` event has an empty result. In
253    /// practice the participant populates `exercise_result` for every
254    /// choice — even non-consuming, unit-returning ones (as
255    /// `Some(DamlValue::Unit)`) — so this path only fires for
256    /// malformed responses.
257    async fn execute_exercise(&self, exercise_command: DamlExerciseCommand) -> Result<DamlValue, DamlError> {
258        let tx = self.submit_and_wait_for_transaction_with_effects(DamlCommand::Exercise(exercise_command)).await?;
259        tx.events
260            .into_iter()
261            .find_map(|e| match e {
262                DamlEvent::Exercised(ex) => ex.exercise_result.clone(),
263                _ => None,
264            })
265            .req()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[tokio::test]
274    async fn test_act_as() -> DamlResult<()> {
275        let client = DamlGrpcClient::dummy_for_testing();
276        let executor = DamlSimpleExecutorBuilder::new(&client).act_as("Alice").build()?;
277        assert_eq!(&["Alice"], executor.act_as());
278        assert_eq!(0, executor.read_as().len());
279        Ok(())
280    }
281
282    #[tokio::test]
283    async fn test_read_as() -> DamlResult<()> {
284        let client = DamlGrpcClient::dummy_for_testing();
285        let executor = DamlSimpleExecutorBuilder::new(&client).read_as("Alice").build()?;
286        assert_eq!(&["Alice"], executor.read_as());
287        assert_eq!(0, executor.act_as().len());
288        Ok(())
289    }
290
291    #[tokio::test]
292    async fn test_act_as_and_read_as() -> DamlResult<()> {
293        let client = DamlGrpcClient::dummy_for_testing();
294        let executor = DamlSimpleExecutorBuilder::new(&client).act_as("Alice").read_as("Bob").build()?;
295        assert_eq!(&["Alice"], executor.act_as());
296        assert_eq!(&["Bob"], executor.read_as());
297        Ok(())
298    }
299
300    #[tokio::test]
301    async fn test_act_as_all() -> DamlResult<()> {
302        let client = DamlGrpcClient::dummy_for_testing();
303        let executor =
304            DamlSimpleExecutorBuilder::new(&client).act_as_all(vec!["Alice".into(), "Bob".into()]).build()?;
305        assert_eq!(&["Alice", "Bob"], executor.act_as());
306        assert_eq!(0, executor.read_as().len());
307        Ok(())
308    }
309
310    #[tokio::test]
311    async fn test_read_as_all() -> DamlResult<()> {
312        let client = DamlGrpcClient::dummy_for_testing();
313        let executor =
314            DamlSimpleExecutorBuilder::new(&client).read_as_all(vec!["Alice".into(), "Bob".into()]).build()?;
315        assert_eq!(&["Alice", "Bob"], executor.read_as());
316        assert_eq!(0, executor.act_as().len());
317        Ok(())
318    }
319
320    #[tokio::test]
321    async fn test_act_as_all_and_read_as_all() -> DamlResult<()> {
322        let client = DamlGrpcClient::dummy_for_testing();
323        let executor = DamlSimpleExecutorBuilder::new(&client)
324            .act_as_all(vec!["Alice".into(), "Bob".into()])
325            .read_as_all(vec!["John".into(), "Jill".into()])
326            .build()?;
327        assert_eq!(&["Alice", "Bob"], executor.act_as());
328        assert_eq!(&["John", "Jill"], executor.read_as());
329        Ok(())
330    }
331
332    #[tokio::test]
333    async fn test_no_actors_should_fail() -> DamlResult<()> {
334        let client = DamlGrpcClient::dummy_for_testing();
335        let executor = DamlSimpleExecutorBuilder::new(&client).build();
336        match executor {
337            Err(DamlError::InsufficientParties) => (),
338            _ => panic!("expected DamlError::InsufficientParties"),
339        }
340        Ok(())
341    }
342}