use canton_proto::com::daml::ledger::api::v2 as pb;
#[derive(Clone, Debug)]
pub struct Submit {
pub(crate) act_as: Vec<String>,
pub(crate) commands: Vec<pb::Command>,
pub(crate) command_id: Option<String>,
pub(crate) user_id: Option<String>,
pub(crate) read_as: Vec<String>,
pub(crate) workflow_id: Option<String>,
pub(crate) synchronizer_id: Option<String>,
pub(crate) deduplication: Option<pb::commands::DeduplicationPeriod>,
pub(crate) transaction_shape: crate::request::TransactionShape,
pub(crate) submission_id: Option<String>,
pub(crate) disclosed_contracts: Vec<pb::DisclosedContract>,
pub(crate) package_id_selection_preference: Vec<String>,
pub(crate) min_ledger_time_abs: Option<prost_types::Timestamp>,
pub(crate) min_ledger_time_rel: Option<std::time::Duration>,
pub(crate) prefetch_contract_keys: Vec<pb::PrefetchContractKey>,
pub(crate) taps_max_passes: Option<u32>,
}
impl Submit {
pub fn new(act_as: impl Into<String>) -> Self {
Self::new_multi(vec![act_as.into()])
}
#[must_use]
pub fn new_multi(act_as: Vec<String>) -> Self {
Self {
act_as,
commands: Vec::new(),
command_id: None,
user_id: None,
read_as: Vec::new(),
workflow_id: None,
synchronizer_id: None,
deduplication: None,
transaction_shape: crate::request::TransactionShape::default(),
submission_id: None,
disclosed_contracts: Vec::new(),
package_id_selection_preference: Vec::new(),
min_ledger_time_abs: None,
min_ledger_time_rel: None,
prefetch_contract_keys: Vec::new(),
taps_max_passes: None,
}
}
#[must_use]
pub fn add_command(mut self, command: pb::Command) -> Self {
self.commands.push(command);
self
}
#[must_use]
pub fn with_command_id(mut self, command_id: impl Into<String>) -> Self {
self.command_id = Some(command_id.into());
self
}
#[must_use]
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
#[must_use]
pub fn with_read_as(mut self, read_as: Vec<String>) -> Self {
self.read_as = read_as;
self
}
#[must_use]
pub fn with_workflow_id(mut self, workflow_id: impl Into<String>) -> Self {
self.workflow_id = Some(workflow_id.into());
self
}
#[must_use]
pub fn with_synchronizer_id(mut self, synchronizer_id: impl Into<String>) -> Self {
self.synchronizer_id = Some(synchronizer_id.into());
self
}
#[must_use]
pub fn with_deduplication_duration(mut self, duration: std::time::Duration) -> Self {
self.deduplication = Some(pb::commands::DeduplicationPeriod::DeduplicationDuration(
prost_types::Duration {
seconds: i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
nanos: i32::try_from(duration.subsec_nanos()).unwrap_or(0),
},
));
self
}
#[must_use]
pub fn with_deduplication_offset(mut self, offset: i64) -> Self {
self.deduplication = Some(pb::commands::DeduplicationPeriod::DeduplicationOffset(
offset,
));
self
}
#[must_use]
pub fn with_transaction_shape(mut self, shape: crate::request::TransactionShape) -> Self {
self.transaction_shape = shape;
self
}
#[must_use]
pub fn with_submission_id(mut self, submission_id: impl Into<String>) -> Self {
self.submission_id = Some(submission_id.into());
self
}
#[must_use]
pub fn add_disclosed_contract(mut self, contract: pb::DisclosedContract) -> Self {
self.disclosed_contracts.push(contract);
self
}
#[must_use]
pub fn with_package_id_selection_preference(mut self, package_ids: Vec<String>) -> Self {
self.package_id_selection_preference = package_ids;
self
}
#[must_use]
pub fn with_min_ledger_time_abs(mut self, time: prost_types::Timestamp) -> Self {
self.min_ledger_time_abs = Some(time);
self
}
#[must_use]
pub fn with_min_ledger_time_rel(mut self, duration: std::time::Duration) -> Self {
self.min_ledger_time_rel = Some(duration);
self
}
#[must_use]
pub fn with_prefetch_contract_keys(mut self, keys: Vec<pb::PrefetchContractKey>) -> Self {
self.prefetch_contract_keys = keys;
self
}
#[must_use]
pub fn with_taps_max_passes(mut self, passes: u32) -> Self {
self.taps_max_passes = Some(passes);
self
}
pub(crate) fn into_commands(self) -> (String, pb::Commands) {
let command_id = self
.command_id
.unwrap_or_else(|| format!("sdk-{}", uuid::Uuid::new_v4()));
let commands = pb::Commands {
command_id: command_id.clone(),
act_as: self.act_as,
read_as: self.read_as,
user_id: self.user_id.unwrap_or_default(),
workflow_id: self.workflow_id.unwrap_or_default(),
synchronizer_id: self.synchronizer_id.unwrap_or_default(),
commands: self.commands,
deduplication_period: self.deduplication,
submission_id: self.submission_id.unwrap_or_default(),
disclosed_contracts: self.disclosed_contracts,
package_id_selection_preference: self.package_id_selection_preference,
min_ledger_time_abs: self.min_ledger_time_abs,
min_ledger_time_rel: self.min_ledger_time_rel.map(|d| prost_types::Duration {
seconds: i64::try_from(d.as_secs()).unwrap_or(i64::MAX),
nanos: i32::try_from(d.subsec_nanos()).unwrap_or(0),
}),
prefetch_contract_keys: self.prefetch_contract_keys,
taps_max_passes: self.taps_max_passes,
};
(command_id, commands)
}
}
#[must_use]
pub fn create(template_id: pb::Identifier, arguments: pb::Record) -> pb::Command {
pb::Command {
command: Some(pb::command::Command::Create(pb::CreateCommand {
template_id: Some(template_id),
create_arguments: Some(arguments),
})),
}
}
#[must_use]
pub fn exercise(
template_id: pb::Identifier,
contract_id: impl Into<String>,
choice: impl Into<String>,
argument: pb::Value,
) -> pb::Command {
pb::Command {
command: Some(pb::command::Command::Exercise(pb::ExerciseCommand {
template_id: Some(template_id),
contract_id: contract_id.into(),
choice: choice.into(),
choice_argument: Some(argument),
})),
}
}
#[must_use]
pub fn identifier(
package_id: impl Into<String>,
module_name: impl Into<String>,
entity_name: impl Into<String>,
) -> pb::Identifier {
pb::Identifier {
package_id: package_id.into(),
module_name: module_name.into(),
entity_name: entity_name.into(),
}
}
#[must_use]
pub fn record(fields: Vec<(&str, pb::Value)>) -> pb::Record {
pb::Record {
record_id: None,
fields: fields
.into_iter()
.map(|(label, value)| pb::RecordField {
label: label.to_string(),
value: Some(value),
})
.collect(),
}
}
pub mod value {
use canton_proto::com::daml::ledger::api::v2 as pb;
fn wrap(sum: pb::value::Sum) -> pb::Value {
pb::Value { sum: Some(sum) }
}
#[must_use]
pub fn party(party: impl Into<String>) -> pb::Value {
wrap(pb::value::Sum::Party(party.into()))
}
#[must_use]
pub fn text(text: impl Into<String>) -> pb::Value {
wrap(pb::value::Sum::Text(text.into()))
}
#[must_use]
pub fn record(record: pb::Record) -> pb::Value {
wrap(pb::value::Sum::Record(record))
}
#[must_use]
pub fn text_map(entries: Vec<(&str, pb::Value)>) -> pb::Value {
wrap(pb::value::Sum::TextMap(pb::TextMap {
entries: entries
.into_iter()
.map(|(key, value)| pb::text_map::Entry {
key: key.to_string(),
value: Some(value),
})
.collect(),
}))
}
#[must_use]
pub fn empty_text_map() -> pb::Value {
text_map(Vec::new())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn create_builds_a_create_command_with_template_and_args() {
let command = create(
identifier("pkg-1", "Licensing.AppInstall", "AppInstallRequest"),
record(vec![("owner", value::party("alice"))]),
);
let Some(pb::command::Command::Create(create_cmd)) = command.command else {
panic!("expected a create command");
};
let template = create_cmd.template_id.unwrap();
assert_eq!(template.package_id, "pkg-1");
assert_eq!(template.module_name, "Licensing.AppInstall");
assert_eq!(template.entity_name, "AppInstallRequest");
let args = create_cmd.create_arguments.unwrap();
assert_eq!(args.fields.len(), 1);
assert_eq!(args.fields[0].label, "owner");
assert!(matches!(
args.fields[0].value.as_ref().unwrap().sum,
Some(pb::value::Sum::Party(_))
));
}
#[test]
fn exercise_builds_an_exercise_command() {
let command = exercise(
identifier("pkg-1", "M", "T"),
"cid-1",
"Accept",
value::record(record(vec![])),
);
let Some(pb::command::Command::Exercise(ex)) = command.command else {
panic!("expected an exercise command");
};
assert_eq!(ex.contract_id, "cid-1");
assert_eq!(ex.choice, "Accept");
assert_eq!(ex.template_id.unwrap().entity_name, "T");
assert!(ex.choice_argument.is_some());
}
#[test]
fn submit_collects_multiple_commands_in_order() {
let submit = Submit::new("alice")
.add_command(create(identifier("p", "M", "A"), record(vec![])))
.add_command(create(identifier("p", "M", "B"), record(vec![])));
assert_eq!(submit.commands.len(), 2);
}
#[test]
fn submit_builder_collects_parties_commands_and_id() {
let submit = Submit::new("alice")
.with_command_id("cmd-42")
.add_command(create(identifier("p", "M", "E"), record(vec![])));
assert_eq!(submit.act_as, vec!["alice".to_string()]);
assert_eq!(submit.command_id.as_deref(), Some("cmd-42"));
assert_eq!(submit.commands.len(), 1);
}
#[test]
fn into_commands_wires_every_field_and_generates_an_id() {
let disclosed = pb::DisclosedContract {
contract_id: "cid-1".to_string(),
..Default::default()
};
let (command_id, commands) = Submit::new("alice")
.with_user_id("user-1")
.with_read_as(vec!["bob".to_string()])
.with_workflow_id("wf-1")
.with_synchronizer_id("sync-1")
.with_deduplication_duration(std::time::Duration::from_secs(30))
.with_submission_id("sub-1")
.add_disclosed_contract(disclosed)
.with_package_id_selection_preference(vec!["pkg-1".to_string()])
.with_min_ledger_time_rel(std::time::Duration::from_millis(1500))
.with_prefetch_contract_keys(vec![pb::PrefetchContractKey::default()])
.with_taps_max_passes(3)
.add_command(create(identifier("p", "M", "E"), record(vec![])))
.into_commands();
assert!(command_id.starts_with("sdk-"), "generated uuid id");
assert_eq!(commands.command_id, command_id);
assert_eq!(commands.act_as, vec!["alice".to_string()]);
assert_eq!(commands.read_as, vec!["bob".to_string()]);
assert_eq!(commands.user_id, "user-1");
assert_eq!(commands.workflow_id, "wf-1");
assert_eq!(commands.synchronizer_id, "sync-1");
assert_eq!(commands.commands.len(), 1);
assert_eq!(commands.submission_id, "sub-1");
assert_eq!(commands.disclosed_contracts.len(), 1);
assert_eq!(commands.disclosed_contracts[0].contract_id, "cid-1");
assert_eq!(
commands.package_id_selection_preference,
vec!["pkg-1".to_string()]
);
let Some(rel) = commands.min_ledger_time_rel else {
panic!("expected a relative min ledger time");
};
assert_eq!((rel.seconds, rel.nanos), (1, 500_000_000));
assert_eq!(commands.prefetch_contract_keys.len(), 1);
assert_eq!(commands.taps_max_passes, Some(3));
let Some(pb::commands::DeduplicationPeriod::DeduplicationDuration(d)) =
commands.deduplication_period
else {
panic!("expected a deduplication duration");
};
assert_eq!(d.seconds, 30);
}
#[test]
fn into_commands_wires_an_absolute_min_ledger_time() {
let (_, commands) = Submit::new("alice")
.with_min_ledger_time_abs(prost_types::Timestamp {
seconds: 1_700_000_000,
nanos: 0,
})
.into_commands();
let Some(abs) = commands.min_ledger_time_abs else {
panic!("expected an absolute min ledger time");
};
assert_eq!(abs.seconds, 1_700_000_000);
}
#[test]
fn new_multi_carries_every_acting_party() {
let (_, commands) = Submit::new_multi(vec!["a".to_string(), "b".to_string()])
.add_command(create(identifier("p", "M", "E"), record(vec![])))
.into_commands();
assert_eq!(commands.act_as, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn into_commands_preserves_an_explicit_id_and_offset_dedup() {
let (command_id, commands) = Submit::new("alice")
.with_command_id("cmd-7")
.with_deduplication_offset(42)
.into_commands();
assert_eq!(command_id, "cmd-7");
assert_eq!(commands.command_id, "cmd-7");
assert!(matches!(
commands.deduplication_period,
Some(pb::commands::DeduplicationPeriod::DeduplicationOffset(42))
));
assert!(commands.user_id.is_empty());
assert!(commands.workflow_id.is_empty());
}
#[test]
fn empty_text_map_is_a_textmap_with_no_entries() {
let Some(pb::value::Sum::TextMap(map)) = value::empty_text_map().sum else {
panic!("expected a text map value");
};
assert!(map.entries.is_empty());
}
#[test]
fn text_map_preserves_entries() {
let Some(pb::value::Sum::TextMap(map)) = value::text_map(vec![("k", value::text("v"))]).sum
else {
panic!("expected a text map value");
};
assert_eq!(map.entries.len(), 1);
assert_eq!(map.entries[0].key, "k");
}
}