1mod choice;
52mod commands;
53mod primitives;
54mod template;
55mod value;
56
57pub use choice::Choice;
58pub use commands::{create_command, exercise_by_key_command, exercise_command};
59pub use primitives::{
60 ContractId, Date, GenMap, Int64, NestedOpt, Numeric, NumericParseError, Party, PartyParseError,
61 TextMap, Timestamp, Unit,
62};
63pub use template::{Contract, Interface, Template, WithKey};
64pub use value::{FromValue, ToValue, ValueError, from_record};
65
66#[doc(hidden)]
73pub use value::{
74 AbsentField, enum_constructor, enum_value, find_field, optional_field, record, record_fields,
75 required_field, unexpected_constructor, unit_value, variant_parts, variant_value,
76};
77
78pub use canton_proto::com::daml::ledger::api::v2::{Record, Value};
82
83pub use serde;
87
88#[cfg(test)]
89#[allow(
90 clippy::unwrap_used,
91 clippy::expect_used,
92 clippy::assertions_on_constants,
93 non_camel_case_types
94)]
95mod tests {
96 use super::*;
97
98 #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
108 struct AppInstall {
109 #[serde(rename = "provider")]
110 provider: Party,
111 #[serde(rename = "amount")]
112 amount: Numeric,
113 #[serde(rename = "tags")]
114 tags: Vec<String>,
115 #[serde(rename = "noteText")]
120 note: Option<String>,
121 }
122
123 impl ToValue for AppInstall {
124 fn to_value(&self) -> Value {
125 record(vec![
126 ("provider", ToValue::to_value(&self.provider)),
127 ("amount", ToValue::to_value(&self.amount)),
128 ("tags", ToValue::to_value(&self.tags)),
129 ("noteText", ToValue::to_value(&self.note)),
130 ])
131 }
132 }
133
134 impl FromValue for AppInstall {
135 fn from_value(value: &Value) -> Result<Self, ValueError> {
136 Ok(Self {
137 provider: FromValue::from_value(required_field(value, 0, "provider")?)
138 .map_err(|e| e.at("provider"))?,
139 amount: FromValue::from_value(required_field(value, 1, "amount")?)
140 .map_err(|e| e.at("amount"))?,
141 tags: FromValue::from_value(required_field(value, 2, "tags")?)
142 .map_err(|e| e.at("tags"))?,
143 note: optional_field(value, 3, "noteText").map_err(|e| e.at("noteText"))?,
144 })
145 }
146 }
147
148 struct AppInstall_Accept;
149 struct AppInstalled;
150
151 impl Choice<AppInstall> for AppInstall_Accept {
152 type Return = ContractId<AppInstalled>;
153 const NAME: &'static str = "Accept";
154 const CONSUMING: bool = true;
155 }
156
157 impl ToValue for AppInstall_Accept {
159 fn to_value(&self) -> Value {
160 record(vec![])
161 }
162 }
163
164 impl Contract for AppInstall {
165 const PACKAGE_ID: &'static str = "deadbeef";
166 const PACKAGE_NAME: &'static str = "app-install";
167 const MODULE_NAME: &'static str = "Licensing.AppInstall";
168 const ENTITY_NAME: &'static str = "AppInstall";
169 }
170
171 impl Template for AppInstall {
172 fn to_record(&self) -> Record {
173 record_fields(vec![
174 ("provider", ToValue::to_value(&self.provider)),
175 ("amount", ToValue::to_value(&self.amount)),
176 ("tags", ToValue::to_value(&self.tags)),
177 ("noteText", ToValue::to_value(&self.note)),
178 ])
179 }
180 }
181
182 impl WithKey for AppInstall {
183 type Key = Party;
184 }
185
186 struct Holding;
188 struct HoldingView;
189 struct Holding_Transfer;
190
191 impl Contract for Holding {
192 const PACKAGE_ID: &'static str = "cafef00d";
193 const PACKAGE_NAME: &'static str = "splice-api-token-holding";
194 const MODULE_NAME: &'static str = "Splice.Api.Token.HoldingV1";
195 const ENTITY_NAME: &'static str = "Holding";
196 }
197 impl Interface for Holding {
198 type View = HoldingView;
199 }
200 impl FromValue for HoldingView {
201 fn from_value(_value: &Value) -> Result<Self, ValueError> {
202 Ok(HoldingView)
203 }
204 }
205 impl Choice<Holding> for Holding_Transfer {
206 type Return = ();
207 const NAME: &'static str = "Transfer";
208 const CONSUMING: bool = true;
209 }
210 impl ToValue for Holding_Transfer {
211 fn to_value(&self) -> Value {
212 record(vec![])
213 }
214 }
215
216 #[test]
223 fn a_nested_decode_failure_carries_the_field_path() {
224 let wrong = record(vec![
226 ("provider", ToValue::to_value(&Party::new("alice"))),
227 ("amount", ToValue::to_value(&Party::new("not-a-number"))),
228 ("tags", ToValue::to_value(&Vec::<String>::new())),
229 ("noteText", ToValue::to_value(&Option::<String>::None)),
230 ]);
231
232 let err = <AppInstall as FromValue>::from_value(&wrong).expect_err("amount is not a Party");
233 assert_eq!(err.path(), ["amount"], "the failing field must be named");
234 assert!(err.to_string().contains("amount"), "{err}");
235 }
236
237 #[test]
243 fn a_mismatch_names_the_kind_and_never_the_payload() {
244 let secret = "alice::1220deadbeef";
245 let payload = record(vec![
246 ("owner", crate::Party::new(secret).to_value()),
247 (
248 "memo",
249 "wired 4200 to the numbered account".to_string().to_value(),
250 ),
251 ]);
252
253 let err = <String as FromValue>::from_value(&payload).expect_err("a Record is not a Text");
255 let message = err.to_string();
256
257 assert!(message.contains("expected Text"), "{message}");
258 assert!(
259 message.contains("Record"),
260 "should name the kind found: {message}"
261 );
262 assert!(!message.contains(secret), "leaked a party id: {message}");
263 assert!(
264 !message.contains("4200"),
265 "leaked a payload field: {message}"
266 );
267 assert!(!message.contains("memo"), "leaked a field label: {message}");
268
269 let sdk: canton_core::Error = err.into();
272 assert!(!sdk.to_string().contains(secret), "{sdk}");
273 }
274
275 #[test]
276 fn record_round_trips_through_value() {
277 let app = AppInstall {
278 provider: Party::new("alice::1"),
279 amount: Numeric("1.50".to_string()),
280 tags: vec!["defi".to_string(), "wallet".to_string()],
281 note: Some("hi".to_string()),
282 };
283 let back = AppInstall::from_value(&app.to_value()).unwrap();
284 assert_eq!(back, app);
285
286 let empty = record(vec![]);
288 assert!(AppInstall::from_value(&empty).is_err());
289 }
290
291 #[test]
292 fn primitives_round_trip_through_value() {
293 let party = Party::new("alice::1");
294 assert_eq!(Party::from_value(&party.to_value()).unwrap(), party);
295
296 let numeric = Numeric("1.50".to_string());
297 assert_eq!(Numeric::from_value(&numeric.to_value()).unwrap(), numeric);
298
299 assert_eq!(i64::from_value(&42_i64.to_value()).unwrap(), 42);
300 assert!(bool::from_value(&true.to_value()).unwrap());
301 }
302
303 #[test]
304 fn containers_round_trip_through_value() {
305 let list = vec!["a".to_string(), "b".to_string()];
306 assert_eq!(Vec::<String>::from_value(&list.to_value()).unwrap(), list);
307
308 let some: Option<String> = Some("x".to_string());
309 assert_eq!(
310 Option::<String>::from_value(&some.to_value()).unwrap(),
311 some
312 );
313 let none: Option<String> = None;
314 assert_eq!(
315 Option::<String>::from_value(&none.to_value()).unwrap(),
316 none
317 );
318
319 let mut map = TextMap::new();
320 map.insert("k".to_string(), 7_i64);
321 assert_eq!(TextMap::<i64>::from_value(&map.to_value()).unwrap(), map);
322 }
323
324 #[test]
325 fn contract_id_is_typed_and_round_trips() {
326 let cid: ContractId<AppInstalled> = ContractId::new("00abc");
327 let back: ContractId<AppInstalled> = ContractId::from_value(&cid.to_value()).unwrap();
328 assert_eq!(back, cid);
329 assert_eq!(back.as_str(), "00abc");
330 }
331
332 #[test]
333 fn choice_metadata_is_available() {
334 assert_eq!(<AppInstall_Accept as Choice<AppInstall>>::NAME, "Accept");
335 assert!(<AppInstall_Accept as Choice<AppInstall>>::CONSUMING);
336 }
337
338 #[test]
339 fn wrong_shape_is_an_error() {
340 let text = "not a party".to_string().to_value();
342 assert!(Party::from_value(&text).is_err());
343 }
344
345 #[test]
346 fn newtypes_use_lf_json_encoding() {
347 use serde_json::json;
348
349 assert_eq!(
351 serde_json::to_value(Party::new("alice::1")).unwrap(),
352 json!("alice::1")
353 );
354 assert_eq!(
355 serde_json::to_value(Numeric("1.50".to_string())).unwrap(),
356 json!("1.50")
357 );
358 let cid: ContractId<()> = ContractId::new("00abc");
359 assert_eq!(serde_json::to_value(cid).unwrap(), json!("00abc"));
360
361 assert_eq!(serde_json::to_value(Date(0)).unwrap(), json!("1970-01-01"));
363 assert_eq!(serde_json::to_value(Date(1)).unwrap(), json!("1970-01-02"));
364
365 for ts in [Timestamp(0), Timestamp(1_700_000_000_000_000)] {
367 let text = serde_json::to_string(&ts).unwrap();
368 assert_eq!(serde_json::from_str::<Timestamp>(&text).unwrap(), ts);
369 }
370 for date in [Date(0), Date(20_000), Date(-100)] {
371 let text = serde_json::to_string(&date).unwrap();
372 assert_eq!(serde_json::from_str::<Date>(&text).unwrap(), date);
373 }
374 }
375
376 #[test]
377 fn int64_uses_the_lf_json_string_form() {
378 use serde_json::json;
379
380 assert_eq!(serde_json::to_value(Int64(5)).unwrap(), json!("5"));
382 assert_eq!(serde_json::from_str::<Int64>("\"5\"").unwrap(), Int64(5));
384 assert_eq!(serde_json::from_str::<Int64>("5").unwrap(), Int64(5));
385 let big = Int64(9_007_199_254_740_993);
387 assert_eq!(
388 serde_json::from_str::<Int64>(&serde_json::to_string(&big).unwrap()).unwrap(),
389 big
390 );
391 assert_eq!(Int64::from_value(&Int64(7).to_value()).unwrap(), Int64(7));
393 }
394
395 #[test]
396 fn numeric_accepts_string_and_number_json() {
397 use serde_json::json;
398
399 assert_eq!(
401 serde_json::to_value(Numeric("1.50".to_string())).unwrap(),
402 json!("1.50")
403 );
404 assert_eq!(
406 serde_json::from_str::<Numeric>("\"1.5\"").unwrap(),
407 Numeric("1.5".to_string())
408 );
409 assert_eq!(
410 serde_json::from_str::<Numeric>("2").unwrap(),
411 Numeric("2".to_string())
412 );
413 }
414
415 #[test]
416 fn nested_optional_uses_the_lf_json_list_form() {
417 use serde_json::json;
418
419 let none: Option<NestedOpt<String>> = None;
422 let some_none: Option<NestedOpt<String>> = Some(NestedOpt(None));
423 let some_some: Option<NestedOpt<String>> = Some(NestedOpt(Some("x".to_string())));
424
425 assert_eq!(serde_json::to_value(&none).unwrap(), json!(null));
426 assert_eq!(serde_json::to_value(&some_none).unwrap(), json!([]));
427 assert_eq!(serde_json::to_value(&some_some).unwrap(), json!(["x"]));
428
429 for value in [none, some_none, some_some] {
432 let text = serde_json::to_string(&value).unwrap();
433 assert_eq!(
434 serde_json::from_str::<Option<NestedOpt<String>>>(&text).unwrap(),
435 value
436 );
437 }
438
439 let triple: Option<NestedOpt<NestedOpt<String>>> = Some(NestedOpt(Some(NestedOpt(None))));
441 assert_eq!(serde_json::to_value(&triple).unwrap(), json!([[]]));
442 }
443
444 #[test]
445 fn nested_optional_grpc_matches_option() {
446 let some_none: Option<NestedOpt<String>> = Some(NestedOpt(None));
449 let back: Option<NestedOpt<String>> = FromValue::from_value(&some_none.to_value()).unwrap();
450 assert_eq!(back, some_none);
451
452 let none: Option<NestedOpt<String>> = None;
453 assert_ne!(none.to_value(), some_none.to_value());
454 }
455
456 #[test]
457 fn unit_is_the_empty_object_in_json() {
458 use serde_json::json;
459
460 assert_eq!(serde_json::to_value(Unit).unwrap(), json!({}));
462 assert_eq!(serde_json::from_str::<Unit>("{}").unwrap(), Unit);
463 assert_eq!(serde_json::from_str::<Unit>("null").unwrap(), Unit);
465 assert_eq!(Unit::from_value(&Unit.to_value()).unwrap(), Unit);
467 }
468
469 #[test]
470 fn command_builders_produce_ledger_api_commands() {
471 use canton_proto::com::daml::ledger::api::v2::command::Command as Cmd;
472
473 let app = AppInstall {
474 provider: Party::new("alice::1"),
475 amount: Numeric("1.00".to_string()),
476 tags: vec![],
477 note: None,
478 };
479
480 let create = create_command(&app);
483 match create.command {
484 Some(Cmd::Create(c)) => {
485 let id = c.template_id.unwrap();
486 assert_eq!(id.package_id, "#app-install");
487 assert_eq!(id.module_name, "Licensing.AppInstall");
488 assert_eq!(id.entity_name, "AppInstall");
489 assert!(c.create_arguments.is_some(), "payload becomes create args");
490 }
491 _ => panic!("expected a Create command"),
492 }
493
494 let cid: ContractId<AppInstall> = ContractId::new("00cid");
496 let exercise = exercise_command(&cid, &AppInstall_Accept);
497 match exercise.command {
498 Some(Cmd::Exercise(e)) => {
499 assert_eq!(e.contract_id, "00cid");
500 assert_eq!(e.choice, "Accept");
501 assert_eq!(e.template_id.unwrap().entity_name, "AppInstall");
502 assert!(e.choice_argument.is_some());
503 }
504 _ => panic!("expected an Exercise command"),
505 }
506
507 let key = Party::new("alice::1");
509 let by_key = exercise_by_key_command::<AppInstall, _>(&key, &AppInstall_Accept);
510 match by_key.command {
511 Some(Cmd::ExerciseByKey(e)) => {
512 assert_eq!(e.choice, "Accept");
513 assert_eq!(e.template_id.unwrap().entity_name, "AppInstall");
514 assert!(
515 e.contract_key.is_some(),
516 "key becomes the contract_key value"
517 );
518 }
519 _ => panic!("expected an ExerciseByKey command"),
520 }
521 }
522
523 #[test]
524 fn exercise_on_interface_builds_a_command() {
525 use canton_proto::com::daml::ledger::api::v2::command::Command as Cmd;
526
527 let cid: ContractId<Holding> = ContractId::new("00hold");
530 let cmd = exercise_command(&cid, &Holding_Transfer);
531 match cmd.command {
532 Some(Cmd::Exercise(e)) => {
533 assert_eq!(e.contract_id, "00hold");
534 assert_eq!(e.choice, "Transfer");
535 let id = e.template_id.unwrap();
536 assert_eq!(id.entity_name, "Holding");
537 assert_eq!(id.package_id, "#splice-api-token-holding");
538 }
539 _ => panic!("expected an Exercise command on the interface"),
540 }
541 }
542
543 fn wire_record(labelled: bool, take: usize) -> Value {
549 use canton_proto::com::daml::ledger::api::v2 as pb;
550 let sample = AppInstall {
551 provider: Party::new("p::1"),
552 amount: Numeric("1.5".to_string()),
553 tags: vec!["a".to_string()],
554 note: None,
555 };
556 let Some(pb::value::Sum::Record(mut record)) = sample.to_value().sum else {
557 panic!("record expected");
558 };
559 record.fields.truncate(take);
560 if !labelled {
561 for field in &mut record.fields {
562 field.label = String::new();
563 }
564 }
565 Value {
566 sum: Some(pb::value::Sum::Record(record)),
567 }
568 }
569
570 #[test]
571 fn decodes_verbose_records_by_label() {
572 let decoded = AppInstall::from_value(&wire_record(true, 4)).unwrap();
573 assert_eq!(decoded.provider, Party::new("p::1"));
574 assert_eq!(decoded.note, None);
575 }
576
577 #[test]
578 fn decodes_non_verbose_records_by_position() {
579 let decoded = AppInstall::from_value(&wire_record(false, 4)).unwrap();
581 assert_eq!(decoded.provider, Party::new("p::1"));
582 assert_eq!(decoded.tags, vec!["a".to_string()]);
583 }
584
585 #[test]
586 fn decodes_normalized_records_with_trailing_optionals_dropped() {
587 for labelled in [true, false] {
591 let decoded = AppInstall::from_value(&wire_record(labelled, 3)).unwrap();
592 assert_eq!(decoded.note, None, "labelled={labelled}");
593 assert_eq!(
594 decoded.amount,
595 Numeric("1.5".to_string()),
596 "labelled={labelled}"
597 );
598 }
599 }
600
601 #[test]
602 fn genmap_is_constructible_for_real_daml_key_types() {
603 let mut map: GenMap<Party, Numeric> = GenMap::new();
607 assert!(map.is_empty());
608 map.insert(Party::new("alice::1"), Numeric::from_wire("1.5"));
609 assert_eq!(map.len(), 1);
610 assert_eq!(GenMap::<Party, Numeric>::default().len(), 0);
611
612 let collected: GenMap<Party, Int64> =
614 [(Party::new("a::1"), Int64(1))].into_iter().collect();
615 assert_eq!(collected.iter().count(), 1);
616 assert_eq!((&collected).into_iter().count(), 1);
617 assert_eq!(collected.into_iter().count(), 1);
618 }
619
620 #[test]
621 fn genmap_equality_is_order_insensitive_and_symmetric() {
622 let a = Party::new("a::1");
623 let b = Party::new("b::1");
624
625 let submitted: GenMap<Party, Int64> =
627 vec![(a.clone(), Int64(1)), (b.clone(), Int64(2))].into();
628 let echoed: GenMap<Party, Int64> =
629 vec![(b.clone(), Int64(2)), (a.clone(), Int64(1))].into();
630 assert_eq!(submitted, echoed);
631
632 let twice: GenMap<Party, Int64> = vec![(a.clone(), Int64(1)), (a.clone(), Int64(1))].into();
636 let mixed: GenMap<Party, Int64> = vec![(a.clone(), Int64(1)), (b, Int64(2))].into();
637 assert_ne!(twice, mixed);
638 assert_ne!(mixed, twice, "equality must be symmetric");
639 assert_eq!(twice, twice.clone());
640 }
641
642 #[test]
643 fn timestamp_and_date_convert_through_time_without_saturating() {
644 use time::macros::{date, datetime};
645
646 let at = Timestamp::from_datetime(datetime!(2026-07-31 12:00:00 UTC)).unwrap();
647 assert_eq!(
648 at.to_datetime().unwrap(),
649 datetime!(2026-07-31 12:00:00 UTC)
650 );
651 assert_eq!(Timestamp(at.micros()), at);
652
653 let before_epoch = datetime!(1969-12-31 23:59:59.999_999_5 UTC);
656 let floored = Timestamp::from_datetime(before_epoch).unwrap();
657 assert!(floored.to_datetime().unwrap() <= before_epoch);
658
659 let extreme = time::OffsetDateTime::new_utc(date!(9999 - 12 - 31), time::Time::MIDNIGHT);
668 let converted = Timestamp::from_datetime(extreme).expect("in range for default `time`");
669 assert_eq!(converted.to_datetime().unwrap(), extreme);
670
671 let day = Date::from_date(date!(2026 - 07 - 31)).unwrap();
672 assert_eq!(day.to_date().unwrap(), date!(2026 - 07 - 31));
673 assert_eq!(Date::from_date(date!(1970 - 01 - 01)).unwrap().days(), 0);
674 }
675
676 #[test]
677 fn numeric_parsing_is_strict_for_callers_and_lenient_on_the_wire() {
678 use std::str::FromStr as _;
679
680 assert_eq!(Numeric::from_str("1.5").unwrap().as_str(), "1.5");
682 assert_eq!(Numeric::try_from("-0.25").unwrap().to_string(), "-0.25");
683 let error = Numeric::from_str("twelve").unwrap_err();
684 assert_eq!(error.input(), "twelve");
685
686 let wire = Numeric::from_wire("1.5000000000");
688 assert_eq!(wire, Numeric::from_str("1.5").unwrap());
689
690 assert_eq!(Numeric::from_wire("42."), Numeric::from_wire("42"));
693 assert_eq!(Numeric::from_wire("42."), Numeric::from_wire("42.000"));
694 }
695
696 #[test]
697 fn value_errors_carry_a_path_and_flow_into_the_sdk_error() {
698 let error = ValueError::new("expected Text")
700 .at("city")
701 .at("address")
702 .at("owner");
703 assert_eq!(error.path(), ["owner", "address", "city"]);
704 assert!(
705 error.to_string().contains("`owner.address.city`"),
706 "{error}"
707 );
708 assert_eq!(error.message(), "expected Text");
709
710 let sdk: canton_core::Error = error.clone().into();
713 assert!(!sdk.is_retriable(), "a shape mismatch will not fix itself");
714 assert!(
715 std::error::Error::source(&sdk)
716 .map(ToString::to_string)
717 .unwrap_or_default()
718 .contains("owner.address.city"),
719 "the cause chain keeps the path"
720 );
721 }
722
723 #[test]
724 fn numeric_compares_by_value_not_by_text() {
725 use std::collections::hash_map::DefaultHasher;
726 use std::hash::{Hash, Hasher};
727
728 let submitted = Numeric("1.5".to_string());
731 let echoed = Numeric("1.5000000000".to_string());
732 assert_eq!(submitted, echoed);
733
734 let hash = |n: &Numeric| {
735 let mut hasher = DefaultHasher::new();
736 n.hash(&mut hasher);
737 hasher.finish()
738 };
739 assert_eq!(hash(&submitted), hash(&echoed), "Eq and Hash must agree");
740
741 assert_eq!(Numeric("-0".to_string()), Numeric("0.00".to_string()));
743 assert_eq!(Numeric("+07.10".to_string()), Numeric("7.1".to_string()));
744 assert!(Numeric("2".to_string()) < Numeric("10".to_string()));
745 assert!(Numeric("1.45".to_string()) < Numeric("1.5".to_string()));
746 assert!(Numeric("-1.5".to_string()) < Numeric("-1.45".to_string()));
747 assert!(Numeric("-1".to_string()) < Numeric("0.1".to_string()));
748
749 assert_ne!(Numeric("abc".to_string()), Numeric("1".to_string()));
751 assert!(Numeric::parse("1.5").is_ok());
752 assert!(Numeric::parse("-12.500").is_ok());
753 assert!(Numeric::parse("1.").is_ok());
756 for bad in ["", ".5", "1.2.3", "1e5", "abc", "1,5"] {
757 assert!(Numeric::parse(bad).is_err(), "{bad:?} should be rejected");
758 }
759 }
760
761 #[test]
762 fn contract_id_retags_for_interface_exercise() {
763 use canton_proto::com::daml::ledger::api::v2::command::Command as Cmd;
764
765 let concrete: ContractId<AppInstall> = ContractId::new("00abc");
767 let as_interface: ContractId<Holding> = concrete.retag();
768 assert_eq!(as_interface.as_str(), "00abc");
769 let cmd = exercise_command(&as_interface, &Holding_Transfer);
770 match cmd.command {
771 Some(Cmd::Exercise(e)) => {
772 assert_eq!(
773 e.template_id.unwrap().package_id,
774 "#splice-api-token-holding"
775 );
776 }
777 _ => panic!("expected an interface exercise"),
778 }
779
780 let mut set = std::collections::HashSet::new();
782 set.insert(ContractId::<AppInstall>::new("00abc"));
783 assert!(set.contains(&ContractId::<AppInstall>::new("00abc")));
784 }
785
786 #[test]
787 fn from_created_event_decodes_the_typed_read_path() {
788 use canton_proto::com::daml::ledger::api::v2 as pb;
789
790 let payload = AppInstall {
791 provider: Party::new("p::1"),
792 amount: Numeric("2".to_string()),
793 tags: vec![],
794 note: None,
795 };
796 let Some(pb::value::Sum::Record(record)) = payload.to_value().sum else {
797 panic!("record expected");
798 };
799 let event = pb::CreatedEvent {
800 template_id: Some(pb::Identifier {
801 package_id: "someotherhash".to_string(),
803 module_name: AppInstall::MODULE_NAME.to_string(),
804 entity_name: AppInstall::ENTITY_NAME.to_string(),
805 }),
806 package_name: AppInstall::PACKAGE_NAME.to_string(),
807 create_arguments: Some(record),
808 ..Default::default()
809 };
810
811 let decoded = AppInstall::from_created_event(&event).unwrap();
812 assert_eq!(decoded, payload);
813
814 let mut wrong = event.clone();
817 wrong.template_id.as_mut().unwrap().entity_name = "SomethingElse".to_string();
818 let error = AppInstall::from_created_event(&wrong).unwrap_err();
819 assert!(error.message().contains("SomethingElse"), "{error}");
820 assert!(error.message().contains("AppInstall"), "{error}");
821
822 let mut foreign = event.clone();
825 foreign.package_name = "someone-elses-app".to_string();
826 let error = AppInstall::from_created_event(&foreign).unwrap_err();
827 assert!(error.message().contains("someone-elses-app"), "{error}");
828
829 let mut empty = event.clone();
831 empty.create_arguments = None;
832 assert!(AppInstall::from_created_event(&empty).is_err());
833 }
834
835 #[test]
836 fn missing_required_field_is_an_error_naming_the_field() {
837 let error = AppInstall::from_value(&wire_record(true, 2)).unwrap_err();
839 assert!(
840 error.message().contains("tags"),
841 "error should name the missing field: {error}"
842 );
843 }
844
845 #[test]
848 fn from_json_created_event_decodes_and_refuses_the_wrong_template() {
849 use serde_json::json;
850
851 let payload = json!({
852 "provider": "alice::1220ab",
853 "amount": "1.5",
854 "tags": ["a"],
855 "noteText": null
856 });
857
858 let event = json!({"CreatedEvent": {
860 "contractId": "00abc",
861 "templateId": "somepackage:Licensing.AppInstall:AppInstall",
862 "packageName": "app-install",
863 "createArgument": payload,
864 }});
865 let decoded = AppInstall::from_json_created_event(&event).unwrap();
866 assert_eq!(decoded.provider, Party::new("alice::1220ab"));
867 assert_eq!(decoded.amount, Numeric::from_wire("1.5"));
868
869 let bare = json!({
871 "templateId": "somepackage:Licensing.AppInstall:AppInstall",
872 "packageName": "app-install",
873 "createArgument": payload,
874 });
875 assert!(AppInstall::from_json_created_event(&bare).is_ok());
876
877 let impostor = json!({"CreatedEvent": {
881 "templateId": "somepackage:Other.Module:SomethingElse",
882 "packageName": "app-install",
883 "createArgument": payload,
884 }});
885 let error = AppInstall::from_json_created_event(&impostor)
886 .expect_err("a different template must not decode");
887 assert!(error.to_string().contains("Other.Module"), "{error}");
888 assert!(
889 error.to_string().contains("Licensing.AppInstall"),
890 "{error}"
891 );
892
893 let upgraded = json!({"CreatedEvent": {
896 "templateId": "adifferentpackageid:Licensing.AppInstall:AppInstall",
897 "packageName": "app-install",
898 "createArgument": payload,
899 }});
900 assert!(AppInstall::from_json_created_event(&upgraded).is_ok());
901
902 let foreign = json!({"CreatedEvent": {
905 "templateId": "adifferentpackageid:Licensing.AppInstall:AppInstall",
906 "packageName": "someone-elses-app",
907 "createArgument": payload,
908 }});
909 let error = AppInstall::from_json_created_event(&foreign)
910 .expect_err("another package must not decode");
911 assert!(error.to_string().contains("someone-elses-app"), "{error}");
912
913 let empty = json!({"CreatedEvent": {"contractId": "00abc"}});
915 assert!(AppInstall::from_json_created_event(&empty).is_err());
916 }
917 #[test]
925 fn a_container_element_is_named_in_the_path() {
926 use std::collections::BTreeMap;
927
928 let list = wrong_at_index();
930 let error = <Vec<Party> as FromValue>::from_value(&list).unwrap_err();
931 assert_eq!(error.path(), ["2"], "{error}");
932 assert!(error.to_string().contains("expected Party"), "{error}");
933
934 let record = record(vec![("holders", list)]);
936 let error = <AppInstallHolders as FromValue>::from_value(&record).unwrap_err();
937 assert_eq!(error.path(), ["holders", "2"], "{error}");
938 assert!(error.to_string().contains("`holders.2`"), "{error}");
939
940 let map = Value {
942 sum: Some(
943 canton_proto::com::daml::ledger::api::v2::value::Sum::TextMap(
944 canton_proto::com::daml::ledger::api::v2::TextMap {
945 entries: vec![canton_proto::com::daml::ledger::api::v2::text_map::Entry {
946 key: "alice".to_string(),
947 value: Some(42_i64.to_value()),
948 }],
949 },
950 ),
951 ),
952 };
953 let error = <BTreeMap<String, Party> as FromValue>::from_value(&map).unwrap_err();
954 assert_eq!(error.path(), ["alice"], "{error}");
955
956 let genmap = Value {
958 sum: Some(
959 canton_proto::com::daml::ledger::api::v2::value::Sum::GenMap(
960 canton_proto::com::daml::ledger::api::v2::GenMap {
961 entries: vec![canton_proto::com::daml::ledger::api::v2::gen_map::Entry {
962 key: Some(Party::new("alice::1220ab").to_value()),
963 value: Some(42_i64.to_value()),
964 }],
965 },
966 ),
967 ),
968 };
969 let error = <GenMap<Party, Party> as FromValue>::from_value(&genmap).unwrap_err();
970 assert_eq!(error.path(), ["0", "value"], "{error}");
971 }
972
973 fn wrong_at_index() -> Value {
975 list(vec![
976 Party::new("a::1220ab").to_value(),
977 Party::new("b::1220ab").to_value(),
978 42_i64.to_value(),
979 ])
980 }
981
982 fn list(elements: Vec<Value>) -> Value {
983 use canton_proto::com::daml::ledger::api::v2 as pb;
984 Value {
985 sum: Some(pb::value::Sum::List(pb::List { elements })),
986 }
987 }
988
989 #[derive(Debug, PartialEq)]
990 struct AppInstallHolders {
991 holders: Vec<Party>,
992 }
993 impl FromValue for AppInstallHolders {
994 fn from_value(value: &Value) -> Result<Self, ValueError> {
995 Ok(Self {
996 holders: FromValue::from_value(required_field(value, 0, "holders")?)
997 .map_err(|e| e.at("holders"))?,
998 })
999 }
1000 }
1001 #[test]
1008 fn a_party_id_a_caller_supplies_is_validated() {
1009 assert_eq!("".parse::<Party>(), Err(PartyParseError::Empty));
1013
1014 assert_eq!(
1015 "alice\nbob".parse::<Party>(),
1016 Err(PartyParseError::UnexpectedCharacter { character: '\n' })
1017 );
1018 assert!(matches!(
1019 "x".repeat(256).parse::<Party>(),
1020 Err(PartyParseError::TooLong { length: 256 })
1021 ));
1022
1023 let real = "app_provider_quickstart-dimakozoliy-1::\
1026 12204a2925a78214d3a4c81f3b425f89894a42858b9e8f13a8427c68513232cdf640";
1027 assert!(real.parse::<Party>().is_ok(), "{real}");
1028 assert_eq!(real.parse::<Party>().unwrap().as_str(), real);
1029
1030 let odd = Party::new("something\nthe ledger vouched for");
1033 let value = odd.to_value();
1034 assert_eq!(<Party as FromValue>::from_value(&value).unwrap(), odd);
1035
1036 let message = PartyParseError::Empty.to_string();
1039 assert!(message.contains("cannot be empty"), "{message}");
1040 }
1041}