radix_engine/blueprints/resource/auth_zone/
auth_zone_composition.rs

1use crate::blueprints::resource::*;
2use crate::errors::{ApplicationError, RuntimeError};
3use crate::internal_prelude::*;
4use crate::kernel::kernel_api::KernelSubstateApi;
5use crate::system::system_callback::SystemLockData;
6use crate::system::system_substates::FieldSubstate;
7use radix_engine_interface::api::LockFlags;
8use radix_engine_interface::api::SystemApi;
9use radix_engine_interface::blueprints::resource::*;
10use radix_native_sdk::resource::ResourceManager;
11
12use super::AuthZoneError;
13
14#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
15pub enum ComposeProofError {
16    NonFungibleOperationNotSupported,
17    InsufficientBaseProofs,
18    InvalidAmount,
19    UnexpectedDecimalComputationError,
20}
21
22pub enum ComposedProof {
23    Fungible(
24        ProofMoveableSubstate,
25        FungibleProofSubstate,
26        Vec<SubstateHandle>,
27    ),
28    NonFungible(
29        ProofMoveableSubstate,
30        NonFungibleProofSubstate,
31        Vec<SubstateHandle>,
32    ),
33}
34
35impl From<ComposedProof> for BTreeMap<SubstateKey, IndexedScryptoValue> {
36    fn from(value: ComposedProof) -> Self {
37        match value {
38            ComposedProof::Fungible(info, proof, ..) => btreemap!(
39                FungibleProofField::Moveable.into() => IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(info)),
40                FungibleProofField::ProofRefs.into() => IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(proof)),
41            ),
42            ComposedProof::NonFungible(info, proof, ..) => btreemap!(
43                NonFungibleProofField::Moveable.into() => IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(info)),
44                NonFungibleProofField::ProofRefs.into() => IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(proof)),
45            ),
46        }
47    }
48}
49
50/// Compose a proof by amount, given a list of proofs of any address.
51pub fn compose_proof_by_amount<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
52    proofs: &[Proof],
53    resource_address: ResourceAddress,
54    amount: Option<Decimal>,
55    api: &mut Y,
56) -> Result<ComposedProof, RuntimeError> {
57    let resource_type = ResourceManager(resource_address).resource_type(api)?;
58
59    if let Some(amount) = amount {
60        if !resource_type.check_amount(amount) {
61            return Err(RuntimeError::ApplicationError(
62                ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
63                    ComposeProofError::InvalidAmount,
64                )),
65            ));
66        }
67    }
68
69    match resource_type {
70        ResourceType::Fungible { .. } => {
71            compose_fungible_proof(proofs, resource_address, amount, api).map(|(proof, handles)| {
72                ComposedProof::Fungible(ProofMoveableSubstate { restricted: false }, proof, handles)
73            })
74        }
75        ResourceType::NonFungible { .. } => compose_non_fungible_proof(
76            proofs,
77            resource_address,
78            match amount {
79                Some(amount) => {
80                    NonFungiblesSpecification::Some(amount.to_string().parse().map_err(|_| {
81                        RuntimeError::ApplicationError(ApplicationError::AuthZoneError(
82                            AuthZoneError::ComposeProofError(ComposeProofError::InvalidAmount),
83                        ))
84                    })?)
85                }
86                None => NonFungiblesSpecification::All,
87            },
88            api,
89        )
90        .map(|(proof, handles)| {
91            ComposedProof::NonFungible(ProofMoveableSubstate { restricted: false }, proof, handles)
92        }),
93    }
94}
95
96/// Compose a proof by ids, given a list of proofs of any address.
97pub fn compose_proof_by_ids<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
98    proofs: &[Proof],
99    resource_address: ResourceAddress,
100    ids: Option<IndexSet<NonFungibleLocalId>>,
101    api: &mut Y,
102) -> Result<ComposedProof, RuntimeError> {
103    let resource_type = ResourceManager(resource_address).resource_type(api)?;
104
105    match resource_type {
106        ResourceType::Fungible { .. } => Err(RuntimeError::ApplicationError(
107            ApplicationError::AuthZoneError(super::AuthZoneError::ComposeProofError(
108                ComposeProofError::NonFungibleOperationNotSupported,
109            )),
110        )),
111        ResourceType::NonFungible { .. } => compose_non_fungible_proof(
112            proofs,
113            resource_address,
114            match ids {
115                Some(ids) => NonFungiblesSpecification::Exact(ids),
116                None => NonFungiblesSpecification::All,
117            },
118            api,
119        )
120        .map(|(proof, handles)| {
121            ComposedProof::NonFungible(ProofMoveableSubstate { restricted: false }, proof, handles)
122        }),
123    }
124}
125
126//====================
127// Helper functions
128//====================
129
130fn max_amount_locked<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
131    proofs: &[Proof],
132    resource_address: ResourceAddress,
133    api: &mut Y,
134) -> Result<(Decimal, IndexMap<LocalRef, Decimal>), RuntimeError> {
135    // calculate the max locked amount of each container
136    let mut max: IndexMap<LocalRef, Decimal> = index_map_new();
137    for proof in proofs {
138        let blueprint_id = api.get_blueprint_id(proof.0.as_node_id())?;
139
140        if blueprint_id.blueprint_name.eq(FUNGIBLE_PROOF_BLUEPRINT) {
141            let outer_object = api.get_outer_object(proof.0.as_node_id())?;
142            let proof_resource = ResourceAddress::new_or_panic(outer_object.into());
143            if proof_resource == resource_address {
144                let handle = api.kernel_open_substate(
145                    proof.0.as_node_id(),
146                    MAIN_BASE_PARTITION,
147                    &FungibleProofField::ProofRefs.into(),
148                    LockFlags::read_only(),
149                    SystemLockData::default(),
150                )?;
151                let proof: FieldSubstate<FungibleProofSubstate> =
152                    api.kernel_read_substate(handle)?.as_typed().unwrap();
153                for (container, locked_amount) in &proof.into_payload().evidence {
154                    if let Some(existing) = max.get_mut(container) {
155                        *existing = Decimal::max(*existing, *locked_amount);
156                    } else {
157                        max.insert(*container, *locked_amount);
158                    }
159                }
160                api.kernel_close_substate(handle)?;
161            }
162        }
163    }
164
165    let mut total = Decimal::ZERO;
166    for v in max.values().cloned() {
167        total = total.checked_add(v).ok_or(RuntimeError::ApplicationError(
168            ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
169                ComposeProofError::UnexpectedDecimalComputationError,
170            )),
171        ))?;
172    }
173    let per_container = max.into_iter().collect();
174    Ok((total, per_container))
175}
176
177#[allow(clippy::type_complexity)]
178fn max_ids_locked<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
179    proofs: &[Proof],
180    resource_address: ResourceAddress,
181    api: &mut Y,
182) -> Result<
183    (
184        IndexSet<NonFungibleLocalId>,
185        NonIterMap<LocalRef, IndexSet<NonFungibleLocalId>>,
186    ),
187    RuntimeError,
188> {
189    let mut total: IndexSet<NonFungibleLocalId> = index_set_new();
190    // calculate the max locked non-fungibles of each container
191    let mut per_container = NonIterMap::<LocalRef, IndexSet<NonFungibleLocalId>>::new();
192    for proof in proofs {
193        let blueprint_id = api.get_blueprint_id(proof.0.as_node_id())?;
194        if blueprint_id.blueprint_name.eq(NON_FUNGIBLE_PROOF_BLUEPRINT) {
195            let outer_object = api.get_outer_object(proof.0.as_node_id())?;
196            let proof_resource = ResourceAddress::new_or_panic(outer_object.into());
197            if proof_resource == resource_address {
198                let handle = api.kernel_open_substate(
199                    proof.0.as_node_id(),
200                    MAIN_BASE_PARTITION,
201                    &NonFungibleProofField::ProofRefs.into(),
202                    LockFlags::read_only(),
203                    SystemLockData::default(),
204                )?;
205                let proof: FieldSubstate<NonFungibleProofSubstate> =
206                    api.kernel_read_substate(handle)?.as_typed().unwrap();
207                for (container, locked_ids) in &proof.into_payload().evidence {
208                    total.extend(locked_ids.clone());
209                    if let Some(ids) = per_container.get_mut(container) {
210                        ids.extend(locked_ids.clone());
211                    } else {
212                        per_container.insert(*container, locked_ids.clone());
213                    }
214                }
215            }
216        }
217    }
218    Ok((total, per_container))
219}
220
221fn compose_fungible_proof<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
222    proofs: &[Proof],
223    resource_address: ResourceAddress,
224    amount: Option<Decimal>,
225    api: &mut Y,
226) -> Result<(FungibleProofSubstate, Vec<SubstateHandle>), RuntimeError> {
227    let (max_locked, mut per_container) = max_amount_locked(proofs, resource_address, api)?;
228    let amount = amount.unwrap_or(max_locked);
229
230    // Check if base proofs are sufficient for the request amount
231    if amount > max_locked {
232        return Err(RuntimeError::ApplicationError(
233            ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
234                ComposeProofError::InsufficientBaseProofs,
235            )),
236        ));
237    }
238
239    let mut evidence = index_map_new();
240    let mut remaining = amount;
241    let mut lock_handles = Vec::new();
242    'outer: for proof in proofs {
243        let handle = api.kernel_open_substate(
244            proof.0.as_node_id(),
245            MAIN_BASE_PARTITION,
246            &FungibleProofField::ProofRefs.into(),
247            LockFlags::read_only(),
248            SystemLockData::default(),
249        )?;
250        let substate: FieldSubstate<FungibleProofSubstate> =
251            api.kernel_read_substate(handle)?.as_typed().unwrap();
252        let proof = substate.into_payload();
253        for (container, _) in &proof.evidence {
254            if remaining.is_zero() {
255                break 'outer;
256            }
257
258            if let Some(quota) = per_container.swap_remove(container) {
259                let amount = Decimal::min(remaining, quota);
260                api.call_method(
261                    container.as_node_id(),
262                    match container {
263                        LocalRef::Bucket(_) => FUNGIBLE_BUCKET_LOCK_AMOUNT_IDENT,
264                        LocalRef::Vault(_) => FUNGIBLE_VAULT_LOCK_FUNGIBLE_AMOUNT_IDENT,
265                    },
266                    scrypto_args!(amount),
267                )?;
268                remaining = remaining
269                    .checked_sub(amount)
270                    .ok_or(RuntimeError::ApplicationError(
271                        ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
272                            ComposeProofError::UnexpectedDecimalComputationError,
273                        )),
274                    ))?;
275                evidence.insert(*container, amount);
276            }
277        }
278        lock_handles.push(handle);
279    }
280
281    Ok((
282        FungibleProofSubstate::new(amount, evidence)
283            .map_err(|e| RuntimeError::ApplicationError(ApplicationError::ProofError(e)))?,
284        lock_handles,
285    ))
286}
287
288enum NonFungiblesSpecification {
289    All,
290    Some(usize),
291    Exact(IndexSet<NonFungibleLocalId>),
292}
293
294fn compose_non_fungible_proof<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
295    proofs: &[Proof],
296    resource_address: ResourceAddress,
297    ids: NonFungiblesSpecification,
298    api: &mut Y,
299) -> Result<(NonFungibleProofSubstate, Vec<SubstateHandle>), RuntimeError> {
300    let (max_locked, mut per_container) = max_ids_locked(proofs, resource_address, api)?;
301    let ids = match ids {
302        NonFungiblesSpecification::All => max_locked.clone(),
303        NonFungiblesSpecification::Some(n) => {
304            let ids: IndexSet<NonFungibleLocalId> = max_locked.iter().take(n).cloned().collect();
305            if ids.len() != n {
306                return Err(RuntimeError::ApplicationError(
307                    ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
308                        ComposeProofError::InsufficientBaseProofs,
309                    )),
310                ));
311            }
312            ids
313        }
314        NonFungiblesSpecification::Exact(ids) => {
315            if !max_locked.is_superset(&ids) {
316                return Err(RuntimeError::ApplicationError(
317                    ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
318                        ComposeProofError::InsufficientBaseProofs,
319                    )),
320                ));
321            }
322            ids
323        }
324    };
325
326    if !max_locked.is_superset(&ids) {
327        return Err(RuntimeError::ApplicationError(
328            ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
329                ComposeProofError::InsufficientBaseProofs,
330            )),
331        ));
332    }
333
334    let mut evidence = index_map_new();
335    let mut remaining = ids.clone();
336    let mut lock_handles = Vec::new();
337    'outer: for proof in proofs {
338        let handle = api.kernel_open_substate(
339            proof.0.as_node_id(),
340            MAIN_BASE_PARTITION,
341            &NonFungibleProofField::ProofRefs.into(),
342            LockFlags::read_only(),
343            SystemLockData::default(),
344        )?;
345        let substate: FieldSubstate<NonFungibleProofSubstate> =
346            api.kernel_read_substate(handle)?.as_typed().unwrap();
347        let proof = substate.into_payload().clone();
348        for (container, _) in &proof.evidence {
349            if remaining.is_empty() {
350                break 'outer;
351            }
352
353            if let Some(quota) = per_container.remove(container) {
354                let ids = remaining.intersection(&quota).cloned().collect();
355                api.call_method(
356                    container.as_node_id(),
357                    match container {
358                        LocalRef::Bucket(_) => NON_FUNGIBLE_BUCKET_LOCK_NON_FUNGIBLES_IDENT,
359                        LocalRef::Vault(_) => NON_FUNGIBLE_VAULT_LOCK_NON_FUNGIBLES_IDENT,
360                    },
361                    scrypto_args!(&ids),
362                )?;
363                for id in &ids {
364                    remaining.swap_remove(id);
365                }
366                evidence.insert(*container, ids);
367            }
368        }
369        lock_handles.push(handle);
370    }
371
372    Ok((
373        NonFungibleProofSubstate::new(ids.clone(), evidence)
374            .map_err(|e| RuntimeError::ApplicationError(ApplicationError::ProofError(e)))?,
375        lock_handles,
376    ))
377}