Skip to main content

canton_token/
allocation.rs

1//! Allocations: reserving holdings now so a third party can settle later.
2//!
3//! A transfer moves an asset between two parties. An *allocation* is what a
4//! settlement needs instead: the sender commits holdings to one leg of a
5//! settlement, and an **executor** — neither sender nor receiver — settles it
6//! afterwards. That is what makes delivery-versus-payment expressible, since
7//! both legs can be allocated before either moves.
8//!
9//! It follows the same two steps as a transfer, and for the same reason: the
10//! factory is the registry's contract and only the registry knows what the
11//! choice needs.
12//!
13//! Once an allocation exists, the choices on it — execute, withdraw, cancel —
14//! each need a context of their own, and it is fetched per choice. The standard
15//! is explicit that a context must not be reused across choices.
16
17use 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
26/// Resolve the allocation factory and build the exercise that allocates
27/// `allocation`.
28///
29/// `expected_admin` is the registry's `admin_id`, and it is required here for
30/// the same reason as in a transfer: naming the administrator in the choice is
31/// what stops one being substituted.
32///
33/// `input_holding_cids` may be empty, in which case the registry selects
34/// holdings itself where its implementation allows it.
35///
36/// # Errors
37/// As any registry call, plus [`canton_core::Error::UnexpectedResponse`] if the
38/// context does not decode.
39pub 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
76/// Build the exercise that settles an allocation's transfer.
77///
78/// This is the executor's move, not the sender's: the executor named in the
79/// settlement is the party that acts.
80///
81/// # Errors
82/// As any registry call.
83pub 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
96/// Build the exercise that withdraws an allocation, releasing the holdings.
97///
98/// The sender's move, before settlement.
99///
100/// # Errors
101/// As any registry call.
102pub 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
115/// Build the exercise that cancels an allocation.
116///
117/// The executor's move, when the settlement will not happen.
118///
119/// # Errors
120/// As any registry call.
121pub 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
134/// The shape every choice on an existing allocation shares: fetch the context
135/// for *that* choice, build the argument, exercise it.
136///
137/// One context per choice, never shared — the standard says a context may be
138/// specific to the choice being exercised, so reusing one is a bug that would
139/// work until the day a registry starts distinguishing them.
140async 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    /// The executor is the point of an allocation: it is a third party, and the
212    /// settlement names it rather than either side of the transfer.
213    #[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    /// As for a transfer, the probe carries the real allocation and empty
225    /// `extraArgs` — the registry picks a context from the arguments.
226    #[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    /// Each choice keeps its own name on the ledger. Exercising the wrong one
245    /// with the right context is a rejection that says nothing useful.
246    #[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}