1use canton_core::Result;
20use canton_daml as rt;
21use canton_proto::com::daml::ledger::api::v2 as pb;
22use canton_splice_api_token_metadata_v1::splice_api_token_metadata_v1::Splice_Api_Token_MetadataV1 as md;
23use canton_splice_api_token_transfer_instruction_v1::splice_api_token_transfer_instruction_v1::Splice_Api_Token_TransferInstructionV1 as ti;
24
25use crate::registry::{RegistryClient, TransferInstructionChoice, TransferKind};
26
27#[derive(Clone, Debug)]
34pub struct TokenCommand {
35 command: pb::Command,
36 disclosed_contracts: Vec<pb::DisclosedContract>,
37 transfer_kind: Option<TransferKind>,
38}
39
40impl TokenCommand {
41 pub(crate) fn new(
42 command: pb::Command,
43 disclosed_contracts: Vec<pb::DisclosedContract>,
44 transfer_kind: Option<TransferKind>,
45 ) -> Self {
46 Self {
47 command,
48 disclosed_contracts,
49 transfer_kind,
50 }
51 }
52
53 #[must_use]
55 pub fn command(&self) -> &pb::Command {
56 &self.command
57 }
58
59 #[must_use]
61 pub fn disclosed_contracts(&self) -> &[pb::DisclosedContract] {
62 &self.disclosed_contracts
63 }
64
65 #[must_use]
71 pub fn transfer_kind(&self) -> Option<TransferKind> {
72 self.transfer_kind
73 }
74
75 #[must_use]
77 pub fn into_submit(self, act_as: impl Into<String>) -> canton_ledger::Submit {
78 self.disclosed_contracts.into_iter().fold(
79 canton_ledger::Submit::new(act_as).add_command(self.command),
80 canton_ledger::Submit::add_disclosed_contract,
81 )
82 }
83
84 #[must_use]
87 pub fn into_prepare(self, act_as: impl Into<String>) -> canton_ledger::Prepare {
88 canton_ledger::Prepare::new(act_as)
89 .add_command(self.command)
90 .with_disclosed_contracts(self.disclosed_contracts)
91 }
92
93 #[must_use]
95 pub fn into_parts(self) -> (pb::Command, Vec<pb::DisclosedContract>) {
96 (self.command, self.disclosed_contracts)
97 }
98}
99
100#[must_use]
103pub fn empty_extra_args() -> md::ExtraArgs {
104 md::ExtraArgs {
105 context: md::ChoiceContext {
106 values: rt::TextMap::new(),
107 },
108 meta: md::Metadata {
109 values: rt::TextMap::new(),
110 },
111 }
112}
113
114pub async fn transfer(
128 registry: &RegistryClient,
129 expected_admin: &rt::Party,
130 transfer: ti::Transfer,
131) -> Result<TokenCommand> {
132 let probe = ti::TransferFactory_Transfer {
136 expected_admin: expected_admin.clone(),
137 transfer,
138 extra_args: empty_extra_args(),
139 };
140 let arguments = serde_json::to_value(&probe).map_err(|e| {
141 canton_core::Error::InvalidRequest(format!("the transfer does not serialize: {e}"))
142 })?;
143
144 let factory = registry.transfer_factory(&arguments).await?;
145 let context: md::ChoiceContext = factory.context.decode()?;
146
147 let choice = ti::TransferFactory_Transfer {
149 extra_args: md::ExtraArgs {
150 context,
151 meta: probe.extra_args.meta,
152 },
153 ..probe
154 };
155 let factory_id = rt::ContractId::<ti::TransferFactory>::new(factory.factory_id);
156
157 Ok(TokenCommand::new(
158 rt::exercise_command(&factory_id, &choice),
159 factory.context.into_disclosed_contracts(),
160 factory.transfer_kind,
161 ))
162}
163
164pub async fn accept(
173 registry: &RegistryClient,
174 instruction_id: &rt::ContractId<ti::TransferInstruction>,
175) -> Result<TokenCommand> {
176 on_instruction(
177 registry,
178 instruction_id,
179 TransferInstructionChoice::Accept,
180 |extra_args| ti::TransferInstruction_Accept { extra_args },
181 )
182 .await
183}
184
185pub async fn reject(
190 registry: &RegistryClient,
191 instruction_id: &rt::ContractId<ti::TransferInstruction>,
192) -> Result<TokenCommand> {
193 on_instruction(
194 registry,
195 instruction_id,
196 TransferInstructionChoice::Reject,
197 |extra_args| ti::TransferInstruction_Reject { extra_args },
198 )
199 .await
200}
201
202pub async fn withdraw(
207 registry: &RegistryClient,
208 instruction_id: &rt::ContractId<ti::TransferInstruction>,
209) -> Result<TokenCommand> {
210 on_instruction(
211 registry,
212 instruction_id,
213 TransferInstructionChoice::Withdraw,
214 |extra_args| ti::TransferInstruction_Withdraw { extra_args },
215 )
216 .await
217}
218
219async fn on_instruction<C>(
224 registry: &RegistryClient,
225 instruction_id: &rt::ContractId<ti::TransferInstruction>,
226 choice: TransferInstructionChoice,
227 build: impl FnOnce(md::ExtraArgs) -> C,
228) -> Result<TokenCommand>
229where
230 C: rt::Choice<ti::TransferInstruction> + rt::ToValue,
231{
232 let context = registry
233 .transfer_instruction_context(
234 instruction_id.as_str(),
235 choice,
236 &crate::ChoiceContextRequest::default(),
237 )
238 .await?;
239 let argument = build(md::ExtraArgs {
240 context: context.decode()?,
241 meta: md::Metadata {
242 values: rt::TextMap::new(),
243 },
244 });
245 Ok(TokenCommand::new(
246 rt::exercise_command(instruction_id, &argument),
247 context.into_disclosed_contracts(),
248 None,
249 ))
250}
251
252#[cfg(test)]
253#[allow(clippy::unwrap_used, clippy::expect_used)]
254mod tests {
255 use super::*;
256
257 fn a_transfer() -> ti::Transfer {
258 use canton_splice_api_token_holding_v1::splice_api_token_holding_v1::Splice_Api_Token_HoldingV1 as h;
259 ti::Transfer {
260 sender: rt::Party::parse("alice::1220ab").expect("a party"),
261 receiver: rt::Party::parse("bob::1220cd").expect("a party"),
262 amount: "10.0".parse().expect("a numeric"),
263 instrument_id: h::InstrumentId {
264 admin: rt::Party::parse("dso::1220ef").expect("a party"),
265 id: "Amulet".to_string(),
266 },
267 requested_at: rt::Timestamp(0),
268 execute_before: rt::Timestamp(1_000_000),
269 input_holding_cids: Vec::new(),
270 meta: md::Metadata {
271 values: rt::TextMap::new(),
272 },
273 }
274 }
275
276 #[test]
280 fn the_factory_probe_carries_the_real_transfer_and_empty_extra_args() {
281 let probe = ti::TransferFactory_Transfer {
282 expected_admin: rt::Party::parse("dso::1220ef").expect("a party"),
283 transfer: a_transfer(),
284 extra_args: empty_extra_args(),
285 };
286 let json = serde_json::to_value(&probe).unwrap();
287
288 assert_eq!(
289 json["extraArgs"]["context"]["values"],
290 serde_json::json!({})
291 );
292 assert_eq!(json["extraArgs"]["meta"]["values"], serde_json::json!({}));
293 assert_eq!(json["transfer"]["sender"], "alice::1220ab");
294 assert_eq!(json["transfer"]["receiver"], "bob::1220cd");
295 assert_eq!(json["expectedAdmin"], "dso::1220ef");
296 }
297
298 #[test]
301 fn the_amount_is_encoded_as_the_daml_json_api_encodes_a_numeric() {
302 let json = serde_json::to_value(a_transfer()).unwrap();
303 assert_eq!(json["amount"], "10.0");
304 }
305
306 #[test]
311 fn the_returned_context_is_what_gets_exercised() {
312 let wire = crate::WireChoiceContext {
313 choice_context_data: serde_json::json!({
314 "values": { "amulet-rules": { "tag": "AV_ContractId", "value": "00rules" } }
315 }),
316 disclosed_contracts: vec![crate::WireDisclosedContract {
317 template_id: "pkg:Splice.AmuletRules:AmuletRules".to_string(),
318 contract_id: "00rules".to_string(),
319 created_event_blob: "AQI=".to_string(),
320 synchronizer_id: "sync::1220ab".to_string(),
321 }],
322 };
323 let context = crate::ChoiceContext::from_wire(wire).unwrap();
324 let decoded: md::ChoiceContext = context.decode().unwrap();
325
326 let choice = ti::TransferFactory_Transfer {
327 expected_admin: rt::Party::parse("dso::1220ef").expect("a party"),
328 transfer: a_transfer(),
329 extra_args: md::ExtraArgs {
330 context: decoded,
331 meta: md::Metadata {
332 values: rt::TextMap::new(),
333 },
334 },
335 };
336 let json = serde_json::to_value(&choice).unwrap();
337 assert_eq!(
338 json["extraArgs"]["context"]["values"]["amulet-rules"],
339 serde_json::json!({ "tag": "AV_ContractId", "value": "00rules" })
340 );
341 }
342
343 #[test]
347 fn a_token_command_carries_its_disclosures_into_both_submission_paths() {
348 let disclosed = vec![pb::DisclosedContract {
349 template_id: None,
350 contract_id: "00rules".to_string(),
351 created_event_blob: vec![1, 2],
352 synchronizer_id: "sync".to_string(),
353 }];
354 let command = TokenCommand::new(
355 pb::Command { command: None },
356 disclosed.clone(),
357 Some(TransferKind::Direct),
358 );
359
360 assert_eq!(command.transfer_kind(), Some(TransferKind::Direct));
361 assert_eq!(command.disclosed_contracts().len(), 1);
362
363 let (_, taken) = command.clone().into_parts();
364 assert_eq!(taken, disclosed);
365
366 let prepared = command.clone().into_prepare("alice::1220ab");
370 assert_eq!(prepared.disclosed_contracts().len(), 1);
371 }
372}