1use canton_core::Result;
18use canton_daml as rt;
19use canton_splice_api_token_allocation_instruction_v1::splice_api_token_allocation_instruction_v1::Splice_Api_Token_AllocationInstructionV1 as ai;
20use canton_splice_api_token_allocation_v1::splice_api_token_allocation_v1::Splice_Api_Token_AllocationV1 as al;
21use canton_splice_api_token_metadata_v1::splice_api_token_metadata_v1::Splice_Api_Token_MetadataV1 as md;
22
23use crate::registry::{AllocationChoice, RegistryClient};
24use crate::transfer::{TokenCommand, empty_extra_args};
25
26pub async fn allocate(
40 registry: &RegistryClient,
41 expected_admin: &rt::Party,
42 allocation: al::AllocationSpecification,
43 requested_at: rt::Timestamp,
44 input_holding_cids: Vec<rt::ContractId<crate::Holding>>,
45) -> Result<TokenCommand> {
46 let probe = ai::AllocationFactory_Allocate {
47 expected_admin: expected_admin.clone(),
48 allocation,
49 requested_at,
50 input_holding_cids,
51 extra_args: empty_extra_args(),
52 };
53 let arguments = serde_json::to_value(&probe).map_err(|e| {
54 canton_core::Error::InvalidRequest(format!("the allocation does not serialize: {e}"))
55 })?;
56
57 let factory = registry.allocation_factory(&arguments).await?;
58 let context: md::ChoiceContext = factory.context.decode()?;
59
60 let choice = ai::AllocationFactory_Allocate {
61 extra_args: md::ExtraArgs {
62 context,
63 meta: probe.extra_args.meta,
64 },
65 ..probe
66 };
67 let factory_id = rt::ContractId::<ai::AllocationFactory>::new(factory.factory_id);
68
69 Ok(TokenCommand::new(
70 rt::exercise_command(&factory_id, &choice),
71 factory.context.into_disclosed_contracts(),
72 factory.transfer_kind,
73 ))
74}
75
76pub async fn execute_transfer(
84 registry: &RegistryClient,
85 allocation_id: &rt::ContractId<al::Allocation>,
86) -> Result<TokenCommand> {
87 on_allocation(
88 registry,
89 allocation_id,
90 AllocationChoice::ExecuteTransfer,
91 |extra_args| al::Allocation_ExecuteTransfer { extra_args },
92 )
93 .await
94}
95
96pub async fn withdraw(
103 registry: &RegistryClient,
104 allocation_id: &rt::ContractId<al::Allocation>,
105) -> Result<TokenCommand> {
106 on_allocation(
107 registry,
108 allocation_id,
109 AllocationChoice::Withdraw,
110 |extra_args| al::Allocation_Withdraw { extra_args },
111 )
112 .await
113}
114
115pub async fn cancel(
122 registry: &RegistryClient,
123 allocation_id: &rt::ContractId<al::Allocation>,
124) -> Result<TokenCommand> {
125 on_allocation(
126 registry,
127 allocation_id,
128 AllocationChoice::Cancel,
129 |extra_args| al::Allocation_Cancel { extra_args },
130 )
131 .await
132}
133
134async fn on_allocation<C>(
141 registry: &RegistryClient,
142 allocation_id: &rt::ContractId<al::Allocation>,
143 choice: AllocationChoice,
144 build: impl FnOnce(md::ExtraArgs) -> C,
145) -> Result<TokenCommand>
146where
147 C: rt::Choice<al::Allocation> + rt::ToValue,
148{
149 let context = registry
150 .allocation_context(
151 allocation_id.as_str(),
152 choice,
153 &crate::ChoiceContextRequest::default(),
154 )
155 .await?;
156 let decoded: md::ChoiceContext = context.decode()?;
157 let argument = build(md::ExtraArgs {
158 context: decoded,
159 meta: md::Metadata {
160 values: rt::TextMap::new(),
161 },
162 });
163 Ok(TokenCommand::new(
164 rt::exercise_command(allocation_id, &argument),
165 context.into_disclosed_contracts(),
166 None,
167 ))
168}
169
170#[cfg(test)]
171#[allow(clippy::unwrap_used, clippy::expect_used)]
172mod tests {
173 use super::*;
174
175 fn party(s: &str) -> rt::Party {
176 rt::Party::parse(s).expect("a party")
177 }
178
179 fn a_specification() -> al::AllocationSpecification {
180 use canton_splice_api_token_holding_v1::splice_api_token_holding_v1::Splice_Api_Token_HoldingV1 as h;
181 al::AllocationSpecification {
182 settlement: al::SettlementInfo {
183 executor: party("venue::1220ff"),
184 settlement_ref: al::Reference {
185 id: "trade-1".to_string(),
186 cid: None,
187 },
188 requested_at: rt::Timestamp(0),
189 allocate_before: rt::Timestamp(1_000_000),
190 settle_before: rt::Timestamp(2_000_000),
191 meta: md::Metadata {
192 values: rt::TextMap::new(),
193 },
194 },
195 transfer_leg_id: "leg-1".to_string(),
196 transfer_leg: al::TransferLeg {
197 sender: party("alice::1220ab"),
198 receiver: party("bob::1220cd"),
199 amount: "10.0".parse().expect("a numeric"),
200 instrument_id: h::InstrumentId {
201 admin: party("dso::1220ef"),
202 id: "Amulet".to_string(),
203 },
204 meta: md::Metadata {
205 values: rt::TextMap::new(),
206 },
207 },
208 }
209 }
210
211 #[test]
214 fn the_allocation_names_an_executor_that_is_neither_side() {
215 let spec = a_specification();
216 assert_ne!(spec.settlement.executor, spec.transfer_leg.sender);
217 assert_ne!(spec.settlement.executor, spec.transfer_leg.receiver);
218
219 let json = serde_json::to_value(&spec).unwrap();
220 assert_eq!(json["settlement"]["executor"], "venue::1220ff");
221 assert_eq!(json["transferLeg"]["sender"], "alice::1220ab");
222 }
223
224 #[test]
227 fn the_factory_probe_carries_the_real_allocation_and_empty_extra_args() {
228 let probe = ai::AllocationFactory_Allocate {
229 expected_admin: party("dso::1220ef"),
230 allocation: a_specification(),
231 requested_at: rt::Timestamp(0),
232 input_holding_cids: Vec::new(),
233 extra_args: empty_extra_args(),
234 };
235 let json = serde_json::to_value(&probe).unwrap();
236 assert_eq!(
237 json["extraArgs"]["context"]["values"],
238 serde_json::json!({})
239 );
240 assert_eq!(json["expectedAdmin"], "dso::1220ef");
241 assert_eq!(json["allocation"]["transferLegId"], "leg-1");
242 }
243
244 #[test]
247 fn each_allocation_choice_is_the_one_it_names() {
248 assert_eq!(
249 <al::Allocation_ExecuteTransfer as rt::Choice<al::Allocation>>::NAME,
250 "Allocation_ExecuteTransfer"
251 );
252 assert_eq!(
253 <al::Allocation_Withdraw as rt::Choice<al::Allocation>>::NAME,
254 "Allocation_Withdraw"
255 );
256 assert_eq!(
257 <al::Allocation_Cancel as rt::Choice<al::Allocation>>::NAME,
258 "Allocation_Cancel"
259 );
260 }
261}