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 { .. } => {
107            return Err(RuntimeError::ApplicationError(
108                ApplicationError::AuthZoneError(super::AuthZoneError::ComposeProofError(
109                    ComposeProofError::NonFungibleOperationNotSupported,
110                )),
111            ));
112        }
113        ResourceType::NonFungible { .. } => compose_non_fungible_proof(
114            proofs,
115            resource_address,
116            match ids {
117                Some(ids) => NonFungiblesSpecification::Exact(ids),
118                None => NonFungiblesSpecification::All,
119            },
120            api,
121        )
122        .map(|(proof, handles)| {
123            ComposedProof::NonFungible(ProofMoveableSubstate { restricted: false }, proof, handles)
124        }),
125    }
126}
127
128//====================
129// Helper functions
130//====================
131
132fn max_amount_locked<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
133    proofs: &[Proof],
134    resource_address: ResourceAddress,
135    api: &mut Y,
136) -> Result<(Decimal, IndexMap<LocalRef, Decimal>), RuntimeError> {
137    // calculate the max locked amount of each container
138    let mut max: IndexMap<LocalRef, Decimal> = index_map_new();
139    for proof in proofs {
140        let blueprint_id = api.get_blueprint_id(proof.0.as_node_id())?;
141
142        if blueprint_id.blueprint_name.eq(FUNGIBLE_PROOF_BLUEPRINT) {
143            let outer_object = api.get_outer_object(proof.0.as_node_id())?;
144            let proof_resource = ResourceAddress::new_or_panic(outer_object.into());
145            if proof_resource == resource_address {
146                let handle = api.kernel_open_substate(
147                    proof.0.as_node_id(),
148                    MAIN_BASE_PARTITION,
149                    &FungibleProofField::ProofRefs.into(),
150                    LockFlags::read_only(),
151                    SystemLockData::default(),
152                )?;
153                let proof: FieldSubstate<FungibleProofSubstate> =
154                    api.kernel_read_substate(handle)?.as_typed().unwrap();
155                for (container, locked_amount) in &proof.into_payload().evidence {
156                    if let Some(existing) = max.get_mut(container) {
157                        *existing = Decimal::max(*existing, locked_amount.clone());
158                    } else {
159                        max.insert(container.clone(), locked_amount.clone());
160                    }
161                }
162                api.kernel_close_substate(handle)?;
163            }
164        }
165    }
166
167    let mut total = Decimal::ZERO;
168    for v in max.values().cloned() {
169        total = total.checked_add(v).ok_or(RuntimeError::ApplicationError(
170            ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
171                ComposeProofError::UnexpectedDecimalComputationError,
172            )),
173        ))?;
174    }
175    let per_container = max.into_iter().collect();
176    Ok((total, per_container))
177}
178
179fn max_ids_locked<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
180    proofs: &[Proof],
181    resource_address: ResourceAddress,
182    api: &mut Y,
183) -> Result<
184    (
185        IndexSet<NonFungibleLocalId>,
186        NonIterMap<LocalRef, IndexSet<NonFungibleLocalId>>,
187    ),
188    RuntimeError,
189> {
190    let mut total: IndexSet<NonFungibleLocalId> = index_set_new();
191    // calculate the max locked non-fungibles of each container
192    let mut per_container = NonIterMap::<LocalRef, IndexSet<NonFungibleLocalId>>::new();
193    for proof in proofs {
194        let blueprint_id = api.get_blueprint_id(proof.0.as_node_id())?;
195        if blueprint_id.blueprint_name.eq(NON_FUNGIBLE_PROOF_BLUEPRINT) {
196            let outer_object = api.get_outer_object(proof.0.as_node_id())?;
197            let proof_resource = ResourceAddress::new_or_panic(outer_object.into());
198            if proof_resource == resource_address {
199                let handle = api.kernel_open_substate(
200                    proof.0.as_node_id(),
201                    MAIN_BASE_PARTITION,
202                    &NonFungibleProofField::ProofRefs.into(),
203                    LockFlags::read_only(),
204                    SystemLockData::default(),
205                )?;
206                let proof: FieldSubstate<NonFungibleProofSubstate> =
207                    api.kernel_read_substate(handle)?.as_typed().unwrap();
208                for (container, locked_ids) in &proof.into_payload().evidence {
209                    total.extend(locked_ids.clone());
210                    if let Some(ids) = per_container.get_mut(container) {
211                        ids.extend(locked_ids.clone());
212                    } else {
213                        per_container.insert(container.clone(), locked_ids.clone());
214                    }
215                }
216            }
217        }
218    }
219    Ok((total, per_container))
220}
221
222fn compose_fungible_proof<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
223    proofs: &[Proof],
224    resource_address: ResourceAddress,
225    amount: Option<Decimal>,
226    api: &mut Y,
227) -> Result<(FungibleProofSubstate, Vec<SubstateHandle>), RuntimeError> {
228    let (max_locked, mut per_container) = max_amount_locked(proofs, resource_address, api)?;
229    let amount = amount.unwrap_or(max_locked);
230
231    // Check if base proofs are sufficient for the request amount
232    if amount > max_locked {
233        return Err(RuntimeError::ApplicationError(
234            ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
235                ComposeProofError::InsufficientBaseProofs,
236            )),
237        ));
238    }
239
240    let mut evidence = index_map_new();
241    let mut remaining = amount.clone();
242    let mut lock_handles = Vec::new();
243    'outer: for proof in proofs {
244        let handle = api.kernel_open_substate(
245            proof.0.as_node_id(),
246            MAIN_BASE_PARTITION,
247            &FungibleProofField::ProofRefs.into(),
248            LockFlags::read_only(),
249            SystemLockData::default(),
250        )?;
251        let substate: FieldSubstate<FungibleProofSubstate> =
252            api.kernel_read_substate(handle)?.as_typed().unwrap();
253        let proof = substate.into_payload();
254        for (container, _) in &proof.evidence {
255            if remaining.is_zero() {
256                break 'outer;
257            }
258
259            if let Some(quota) = per_container.swap_remove(container) {
260                let amount = Decimal::min(remaining, quota);
261                api.call_method(
262                    container.as_node_id(),
263                    match container {
264                        LocalRef::Bucket(_) => FUNGIBLE_BUCKET_LOCK_AMOUNT_IDENT,
265                        LocalRef::Vault(_) => FUNGIBLE_VAULT_LOCK_FUNGIBLE_AMOUNT_IDENT,
266                    },
267                    scrypto_args!(amount),
268                )?;
269                remaining = remaining
270                    .checked_sub(amount)
271                    .ok_or(RuntimeError::ApplicationError(
272                        ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
273                            ComposeProofError::UnexpectedDecimalComputationError,
274                        )),
275                    ))?;
276                evidence.insert(container.clone(), amount);
277            }
278        }
279        lock_handles.push(handle);
280    }
281
282    Ok((
283        FungibleProofSubstate::new(amount, evidence)
284            .map_err(|e| RuntimeError::ApplicationError(ApplicationError::ProofError(e)))?,
285        lock_handles,
286    ))
287}
288
289enum NonFungiblesSpecification {
290    All,
291    Some(usize),
292    Exact(IndexSet<NonFungibleLocalId>),
293}
294
295fn compose_non_fungible_proof<Y: KernelSubstateApi<SystemLockData> + SystemApi<RuntimeError>>(
296    proofs: &[Proof],
297    resource_address: ResourceAddress,
298    ids: NonFungiblesSpecification,
299    api: &mut Y,
300) -> Result<(NonFungibleProofSubstate, Vec<SubstateHandle>), RuntimeError> {
301    let (max_locked, mut per_container) = max_ids_locked(proofs, resource_address, api)?;
302    let ids = match ids {
303        NonFungiblesSpecification::All => max_locked.clone(),
304        NonFungiblesSpecification::Some(n) => {
305            let ids: IndexSet<NonFungibleLocalId> = max_locked.iter().cloned().take(n).collect();
306            if ids.len() != n {
307                return Err(RuntimeError::ApplicationError(
308                    ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
309                        ComposeProofError::InsufficientBaseProofs,
310                    )),
311                ));
312            }
313            ids
314        }
315        NonFungiblesSpecification::Exact(ids) => {
316            if !max_locked.is_superset(&ids) {
317                return Err(RuntimeError::ApplicationError(
318                    ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
319                        ComposeProofError::InsufficientBaseProofs,
320                    )),
321                ));
322            }
323            ids
324        }
325    };
326
327    if !max_locked.is_superset(&ids) {
328        return Err(RuntimeError::ApplicationError(
329            ApplicationError::AuthZoneError(AuthZoneError::ComposeProofError(
330                ComposeProofError::InsufficientBaseProofs,
331            )),
332        ));
333    }
334
335    let mut evidence = index_map_new();
336    let mut remaining = ids.clone();
337    let mut lock_handles = Vec::new();
338    'outer: for proof in proofs {
339        let handle = api.kernel_open_substate(
340            proof.0.as_node_id(),
341            MAIN_BASE_PARTITION,
342            &NonFungibleProofField::ProofRefs.into(),
343            LockFlags::read_only(),
344            SystemLockData::default(),
345        )?;
346        let substate: FieldSubstate<NonFungibleProofSubstate> =
347            api.kernel_read_substate(handle)?.as_typed().unwrap();
348        let proof = substate.into_payload().clone();
349        for (container, _) in &proof.evidence {
350            if remaining.is_empty() {
351                break 'outer;
352            }
353
354            if let Some(quota) = per_container.remove(container) {
355                let ids = remaining.intersection(&quota).cloned().collect();
356                api.call_method(
357                    container.as_node_id(),
358                    match container {
359                        LocalRef::Bucket(_) => NON_FUNGIBLE_BUCKET_LOCK_NON_FUNGIBLES_IDENT,
360                        LocalRef::Vault(_) => NON_FUNGIBLE_VAULT_LOCK_NON_FUNGIBLES_IDENT,
361                    },
362                    scrypto_args!(&ids),
363                )?;
364                for id in &ids {
365                    remaining.swap_remove(id);
366                }
367                evidence.insert(container.clone(), ids);
368            }
369        }
370        lock_handles.push(handle);
371    }
372
373    Ok((
374        NonFungibleProofSubstate::new(ids.clone(), evidence)
375            .map_err(|e| RuntimeError::ApplicationError(ApplicationError::ProofError(e)))?,
376        lock_handles,
377    ))
378}