1pub mod events;
28
29use canton_core::Result;
30use canton_daml as rt;
31use canton_splice_api_token_allocation_instruction_v2::splice_api_token_allocation_instruction_v2::Splice_Api_Token_AllocationInstructionV2 as ai;
32use canton_splice_api_token_allocation_v2::splice_api_token_allocation_v2::Splice_Api_Token_AllocationV2 as al;
33use canton_splice_api_token_holding_v2::splice_api_token_holding_v2::Splice_Api_Token_HoldingV2 as h;
34use canton_splice_api_token_metadata_v1::splice_api_token_metadata_v1::Splice_Api_Token_MetadataV1 as md;
35use canton_splice_api_token_transfer_instruction_v2::splice_api_token_transfer_instruction_v2::Splice_Api_Token_TransferInstructionV2 as ti;
36
37use crate::registry::{
38 AllocationChoice, AllocationInstructionChoice, RegistryClient, TransferInstructionChoice,
39};
40use crate::transfer::{TokenCommand, empty_extra_args};
41
42pub async fn transfer(
52 registry: &RegistryClient,
53 transfer: ti::Transfer,
54 actors: Vec<rt::Party>,
55) -> Result<TokenCommand> {
56 let probe = ti::TransferFactory_Transfer {
57 transfer,
58 actors,
59 extra_args: empty_extra_args(),
60 };
61 let arguments = serde_json::to_value(&probe).map_err(|e| {
62 canton_core::Error::InvalidRequest(format!("the transfer does not serialize: {e}"))
63 })?;
64
65 let factory = registry.transfer_factory_v2(&arguments).await?;
66 let choice = ti::TransferFactory_Transfer {
67 extra_args: md::ExtraArgs {
68 context: factory.context.decode()?,
69 meta: probe.extra_args.meta,
70 },
71 ..probe
72 };
73 let factory_id = rt::ContractId::<ti::TransferFactory>::new(factory.factory_id);
74
75 Ok(TokenCommand::new(
76 rt::exercise_command(&factory_id, &choice),
77 factory.context.into_disclosed_contracts(),
78 factory.transfer_kind,
79 ))
80}
81
82pub async fn accept(
87 registry: &RegistryClient,
88 instruction_id: &rt::ContractId<ti::TransferInstruction>,
89 actors: Vec<rt::Party>,
90) -> Result<TokenCommand> {
91 on_instruction(
92 registry,
93 instruction_id,
94 TransferInstructionChoice::Accept,
95 |extra_args| ti::TransferInstruction_Accept { actors, extra_args },
96 )
97 .await
98}
99
100pub async fn reject(
105 registry: &RegistryClient,
106 instruction_id: &rt::ContractId<ti::TransferInstruction>,
107 actors: Vec<rt::Party>,
108) -> Result<TokenCommand> {
109 on_instruction(
110 registry,
111 instruction_id,
112 TransferInstructionChoice::Reject,
113 |extra_args| ti::TransferInstruction_Reject { actors, extra_args },
114 )
115 .await
116}
117
118pub async fn withdraw(
123 registry: &RegistryClient,
124 instruction_id: &rt::ContractId<ti::TransferInstruction>,
125 actors: Vec<rt::Party>,
126) -> Result<TokenCommand> {
127 on_instruction(
128 registry,
129 instruction_id,
130 TransferInstructionChoice::Withdraw,
131 |extra_args| ti::TransferInstruction_Withdraw { actors, extra_args },
132 )
133 .await
134}
135
136pub async fn allocate(
146 registry: &RegistryClient,
147 settlement: al::SettlementInfo,
148 allocation: al::AllocationSpecification,
149 requested_at: rt::Timestamp,
150 input_holding_cids: Vec<rt::ContractId<h::Holding>>,
151 actors: Vec<rt::Party>,
152) -> Result<TokenCommand> {
153 use canton_splice_api_token_allocation_instruction_v2::splice_api_token_allocation_instruction_v2::Splice_Api_Token_AllocationInstructionV2 as ai;
154
155 let probe = ai::AllocationFactory_Allocate {
156 settlement,
157 allocation,
158 requested_at,
159 input_holding_cids,
160 actors,
161 extra_args: empty_extra_args(),
162 };
163 let arguments = serde_json::to_value(&probe).map_err(|e| {
164 canton_core::Error::InvalidRequest(format!("the allocation does not serialize: {e}"))
165 })?;
166
167 let factory = registry.allocation_factory_v2(&arguments).await?;
168 let choice = ai::AllocationFactory_Allocate {
169 extra_args: md::ExtraArgs {
170 context: factory.context.decode()?,
171 meta: probe.extra_args.meta,
172 },
173 ..probe
174 };
175 let factory_id = rt::ContractId::<ai::AllocationFactory>::new(factory.factory_id);
176
177 Ok(TokenCommand::new(
178 rt::exercise_command(&factory_id, &choice),
179 factory.context.into_disclosed_contracts(),
180 None,
181 ))
182}
183
184pub async fn settle_batch(
194 registry: &RegistryClient,
195 settlement: al::SettlementInfo,
196 transfer_legs: Vec<al::TransferLeg>,
197 allocations: Vec<al::FinalizedAllocation>,
198 actors: Vec<rt::Party>,
199) -> Result<TokenCommand> {
200 let probe = al::SettlementFactory_SettleBatch {
201 settlement,
202 transfer_legs,
203 allocations,
204 actors,
205 extra_args: empty_extra_args(),
206 };
207 let arguments = serde_json::to_value(&probe).map_err(|e| {
208 canton_core::Error::InvalidRequest(format!("the settlement does not serialize: {e}"))
209 })?;
210
211 let factory = registry.settlement_factory_v2(&arguments).await?;
212 let choice = al::SettlementFactory_SettleBatch {
213 extra_args: md::ExtraArgs {
214 context: factory.context.decode()?,
215 meta: probe.extra_args.meta,
216 },
217 ..probe
218 };
219 let factory_id = rt::ContractId::<al::SettlementFactory>::new(factory.factory_id);
220
221 Ok(TokenCommand::new(
222 rt::exercise_command(&factory_id, &choice),
223 factory.context.into_disclosed_contracts(),
224 None,
225 ))
226}
227
228pub async fn withdraw_allocation(
233 registry: &RegistryClient,
234 allocation_id: &rt::ContractId<al::Allocation>,
235 actors: Vec<rt::Party>,
236) -> Result<TokenCommand> {
237 on_allocation(
238 registry,
239 allocation_id,
240 AllocationChoice::Withdraw,
241 |extra_args| al::Allocation_Withdraw { actors, extra_args },
242 )
243 .await
244}
245
246pub async fn cancel(
251 registry: &RegistryClient,
252 allocation_id: &rt::ContractId<al::Allocation>,
253 actors: Vec<rt::Party>,
254) -> Result<TokenCommand> {
255 on_allocation(
256 registry,
257 allocation_id,
258 AllocationChoice::Cancel,
259 |extra_args| al::Allocation_Cancel { actors, extra_args },
260 )
261 .await
262}
263
264pub async fn accept_allocation_instruction(
273 registry: &RegistryClient,
274 instruction_id: &rt::ContractId<ai::AllocationInstruction>,
275 actors: Vec<rt::Party>,
276) -> Result<TokenCommand> {
277 on_allocation_instruction(
278 registry,
279 instruction_id,
280 AllocationInstructionChoice::Accept,
281 |extra_args| ai::AllocationInstruction_Accept { actors, extra_args },
282 )
283 .await
284}
285
286pub async fn withdraw_allocation_instruction(
291 registry: &RegistryClient,
292 instruction_id: &rt::ContractId<ai::AllocationInstruction>,
293 actors: Vec<rt::Party>,
294) -> Result<TokenCommand> {
295 on_allocation_instruction(
296 registry,
297 instruction_id,
298 AllocationInstructionChoice::Withdraw,
299 |extra_args| ai::AllocationInstruction_Withdraw { actors, extra_args },
300 )
301 .await
302}
303
304async fn on_instruction<C>(
306 registry: &RegistryClient,
307 instruction_id: &rt::ContractId<ti::TransferInstruction>,
308 choice: TransferInstructionChoice,
309 build: impl FnOnce(md::ExtraArgs) -> C,
310) -> Result<TokenCommand>
311where
312 C: rt::Choice<ti::TransferInstruction> + rt::ToValue,
313{
314 let context = registry
315 .transfer_instruction_context_v2(
316 instruction_id.as_str(),
317 choice,
318 &crate::ChoiceContextRequest::default(),
319 )
320 .await?;
321 let argument = build(md::ExtraArgs {
322 context: context.decode()?,
323 meta: md::Metadata {
324 values: rt::TextMap::new(),
325 },
326 });
327 Ok(TokenCommand::new(
328 rt::exercise_command(instruction_id, &argument),
329 context.into_disclosed_contracts(),
330 None,
331 ))
332}
333
334async fn on_allocation_instruction<C>(
337 registry: &RegistryClient,
338 instruction_id: &rt::ContractId<ai::AllocationInstruction>,
339 choice: AllocationInstructionChoice,
340 build: impl FnOnce(md::ExtraArgs) -> C,
341) -> Result<TokenCommand>
342where
343 C: rt::Choice<ai::AllocationInstruction> + rt::ToValue,
344{
345 let context = registry
346 .allocation_instruction_context_v2(
347 instruction_id.as_str(),
348 choice,
349 &crate::ChoiceContextRequest::default(),
350 )
351 .await?;
352 let argument = build(md::ExtraArgs {
353 context: context.decode()?,
354 meta: md::Metadata {
355 values: rt::TextMap::new(),
356 },
357 });
358 Ok(TokenCommand::new(
359 rt::exercise_command(instruction_id, &argument),
360 context.into_disclosed_contracts(),
361 None,
362 ))
363}
364
365async fn on_allocation<C>(
371 registry: &RegistryClient,
372 allocation_id: &rt::ContractId<al::Allocation>,
373 choice: AllocationChoice,
374 build: impl FnOnce(md::ExtraArgs) -> C,
375) -> Result<TokenCommand>
376where
377 C: rt::Choice<al::Allocation> + rt::ToValue,
378{
379 let context = registry
380 .allocation_context_v2(
381 allocation_id.as_str(),
382 choice,
383 &crate::ChoiceContextRequest::default(),
384 )
385 .await?;
386 let argument = build(md::ExtraArgs {
387 context: context.decode()?,
388 meta: md::Metadata {
389 values: rt::TextMap::new(),
390 },
391 });
392 Ok(TokenCommand::new(
393 rt::exercise_command(allocation_id, &argument),
394 context.into_disclosed_contracts(),
395 None,
396 ))
397}
398
399#[cfg(test)]
400#[allow(clippy::unwrap_used, clippy::expect_used)]
401mod tests {
402 use super::*;
403
404 fn party(s: &str) -> rt::Party {
405 rt::Party::parse(s).expect("a party")
406 }
407
408 fn account(owner: &str) -> h::Account {
409 h::Account {
410 owner: Some(party(owner)),
411 provider: None,
412 id: "main".to_string(),
413 }
414 }
415
416 #[test]
419 fn a_v2_transfer_moves_between_accounts_rather_than_parties() {
420 let transfer = ti::Transfer {
421 sender: account("alice::1220ab"),
422 receiver: account("bob::1220cd"),
423 amount: "10.0".parse().expect("a numeric"),
424 instrument_id: h::InstrumentId {
425 admin: party("dso::1220ef"),
426 id: "Amulet".to_string(),
427 },
428 requested_at: rt::Timestamp(0),
429 execute_before: rt::Timestamp(1_000_000),
430 input_holding_cids: Vec::new(),
431 meta: md::Metadata {
432 values: rt::TextMap::new(),
433 },
434 };
435 let json = serde_json::to_value(&transfer).unwrap();
436
437 assert_eq!(json["sender"]["owner"], "alice::1220ab");
440 assert_eq!(json["sender"]["id"], "main");
441 assert_eq!(json["receiver"]["owner"], "bob::1220cd");
442 assert_eq!(json["amount"], "10.0");
443 }
444
445 #[test]
448 fn every_v2_choice_names_its_actors() {
449 let probe = ti::TransferFactory_Transfer {
450 transfer: ti::Transfer {
451 sender: account("alice::1220ab"),
452 receiver: account("bob::1220cd"),
453 amount: "1.0".parse().expect("a numeric"),
454 instrument_id: h::InstrumentId {
455 admin: party("dso::1220ef"),
456 id: "Amulet".to_string(),
457 },
458 requested_at: rt::Timestamp(0),
459 execute_before: rt::Timestamp(1),
460 input_holding_cids: Vec::new(),
461 meta: md::Metadata {
462 values: rt::TextMap::new(),
463 },
464 },
465 actors: vec![party("alice::1220ab")],
466 extra_args: empty_extra_args(),
467 };
468 let json = serde_json::to_value(&probe).unwrap();
469 assert_eq!(json["actors"], serde_json::json!(["alice::1220ab"]));
470 assert_eq!(
472 json["extraArgs"]["context"]["values"],
473 serde_json::json!({})
474 );
475 }
476
477 #[test]
479 fn the_v2_choices_are_named_as_the_ledger_knows_them() {
480 assert_eq!(
481 <al::Allocation_Settle as rt::Choice<al::Allocation>>::NAME,
482 "Allocation_Settle"
483 );
484 assert_eq!(
485 <al::SettlementFactory_SettleBatch as rt::Choice<al::SettlementFactory>>::NAME,
486 "SettlementFactory_SettleBatch"
487 );
488 assert_eq!(
489 <ti::TransferInstruction_Accept as rt::Choice<ti::TransferInstruction>>::NAME,
490 "TransferInstruction_Accept"
491 );
492 }
493}