Skip to main content

radix_engine/blueprints/locker/
blueprint.rs

1#![allow(clippy::too_many_arguments)]
2
3use super::*;
4use crate::internal_prelude::*;
5use radix_engine_interface::blueprints::account::*;
6use radix_engine_interface::blueprints::locker::*;
7use radix_native_sdk::modules::metadata::*;
8use radix_native_sdk::modules::role_assignment::*;
9use radix_native_sdk::resource::*;
10use radix_native_sdk::runtime::*;
11
12pub const STORER_ROLE: &str = "storer";
13pub const STORER_UPDATER_ROLE: &str = "storer_updater";
14pub const RECOVERER_ROLE: &str = "recoverer";
15pub const RECOVERER_UPDATER_ROLE: &str = "recoverer_updater";
16
17pub struct AccountLockerBlueprint;
18
19#[allow(unused_variables)]
20impl AccountLockerBlueprint {
21    pub fn definition() -> BlueprintDefinitionInit {
22        let mut aggregator = TypeAggregator::<ScryptoCustomTypeKind>::new();
23
24        let feature_set = AccountLockerFeatureSet::all_features();
25        let state = AccountLockerStateSchemaInit::create_schema_init(&mut aggregator);
26
27        let functions = function_schema! {
28            aggregator,
29            AccountLocker {
30                instantiate: None,
31                instantiate_simple: None,
32                store: Some(ReceiverInfo::normal_ref_mut()),
33                airdrop: Some(ReceiverInfo::normal_ref_mut()),
34                recover: Some(ReceiverInfo::normal_ref_mut()),
35                recover_non_fungibles: Some(ReceiverInfo::normal_ref_mut()),
36                claim: Some(ReceiverInfo::normal_ref_mut()),
37                claim_non_fungibles: Some(ReceiverInfo::normal_ref_mut()),
38                get_amount: Some(ReceiverInfo::normal_ref()),
39                get_non_fungible_local_ids: Some(ReceiverInfo::normal_ref()),
40            }
41        };
42
43        let events = event_schema! {
44            aggregator,
45            [
46                StoreEvent,
47                RecoverEvent,
48                ClaimEvent,
49            ]
50        };
51
52        let schema = generate_full_schema(aggregator);
53
54        BlueprintDefinitionInit {
55            blueprint_type: BlueprintType::default(),
56            is_transient: false,
57            feature_set,
58            dependencies: indexset!(),
59            schema: BlueprintSchemaInit {
60                generics: vec![],
61                schema,
62                state,
63                events,
64                types: BlueprintTypeSchemaInit::default(),
65                functions: BlueprintFunctionsSchemaInit { functions },
66                hooks: BlueprintHooksInit::default(),
67            },
68
69            royalty_config: PackageRoyaltyConfig::default(),
70            auth_config: AuthConfig {
71                function_auth: FunctionAuth::AllowAll,
72                method_auth: MethodAuthTemplate::StaticRoleDefinition(roles_template!(
73                    roles {
74                        STORER_ROLE => updaters: [STORER_UPDATER_ROLE];
75                        STORER_UPDATER_ROLE => updaters: [STORER_UPDATER_ROLE];
76                        RECOVERER_ROLE => updaters: [RECOVERER_UPDATER_ROLE];
77                        RECOVERER_UPDATER_ROLE => updaters: [RECOVERER_UPDATER_ROLE];
78                    },
79                    methods {
80                        ACCOUNT_LOCKER_STORE_IDENT => [STORER_ROLE];
81                        ACCOUNT_LOCKER_AIRDROP_IDENT => [STORER_ROLE];
82
83                        ACCOUNT_LOCKER_RECOVER_IDENT => [RECOVERER_ROLE];
84                        ACCOUNT_LOCKER_RECOVER_NON_FUNGIBLES_IDENT => [RECOVERER_ROLE];
85
86                        ACCOUNT_LOCKER_CLAIM_IDENT => MethodAccessibility::Public;
87                        ACCOUNT_LOCKER_CLAIM_NON_FUNGIBLES_IDENT => MethodAccessibility::Public;
88                        ACCOUNT_LOCKER_GET_AMOUNT_IDENT => MethodAccessibility::Public;
89                        ACCOUNT_LOCKER_GET_NON_FUNGIBLE_LOCAL_IDS_IDENT => MethodAccessibility::Public;
90                    }
91                )),
92            },
93        }
94    }
95
96    pub fn invoke_export<Y: SystemApi<RuntimeError>>(
97        export_name: &str,
98        input: &IndexedScryptoValue,
99        api: &mut Y,
100    ) -> Result<IndexedScryptoValue, RuntimeError> {
101        dispatch! {
102            EXPORT_NAME,
103            export_name,
104            input,
105            api,
106            AccountLocker,
107            [
108                instantiate,
109                instantiate_simple,
110                store,
111                airdrop,
112                recover,
113                recover_non_fungibles,
114                claim,
115                claim_non_fungibles,
116                get_amount,
117                get_non_fungible_local_ids,
118            ]
119        }
120    }
121
122    fn instantiate<Y: SystemApi<RuntimeError>>(
123        AccountLockerInstantiateInput {
124            owner_role,
125            storer_role,
126            storer_updater_role,
127            recoverer_role,
128            recoverer_updater_role,
129            address_reservation,
130        }: AccountLockerInstantiateInput,
131        api: &mut Y,
132    ) -> Result<AccountLockerInstantiateOutput, RuntimeError> {
133        Self::instantiate_internal(
134            owner_role,
135            storer_role,
136            storer_updater_role,
137            recoverer_role,
138            recoverer_updater_role,
139            metadata_init! {
140                "admin_badge" => EMPTY, locked;
141            },
142            address_reservation,
143            api,
144        )
145    }
146
147    fn instantiate_simple<Y: SystemApi<RuntimeError>>(
148        AccountLockerInstantiateSimpleInput { allow_recover }: AccountLockerInstantiateSimpleInput,
149        api: &mut Y,
150    ) -> Result<AccountLockerInstantiateSimpleOutput, RuntimeError> {
151        // Two address reservations are needed. One for the badge and another for the account locker
152        // that we're instantiating.
153        let (locker_reservation, locker_address) = api.allocate_global_address(BlueprintId {
154            package_address: LOCKER_PACKAGE,
155            blueprint_name: ACCOUNT_LOCKER_BLUEPRINT.into(),
156        })?;
157        let (badge_reservation, badge_address) = api.allocate_global_address(BlueprintId {
158            package_address: RESOURCE_PACKAGE,
159            blueprint_name: FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT.into(),
160        })?;
161        let badge_address = ResourceAddress::new_or_panic(badge_address.as_node_id().0);
162
163        let (badge_address, badge) = api
164            .call_function(
165                RESOURCE_PACKAGE,
166                FUNGIBLE_RESOURCE_MANAGER_BLUEPRINT,
167                FUNGIBLE_RESOURCE_MANAGER_CREATE_WITH_INITIAL_SUPPLY_IDENT,
168                scrypto_encode(&FungibleResourceManagerCreateWithInitialSupplyInput {
169                    owner_role: OwnerRole::Updatable(rule!(require(badge_address))),
170                    track_total_supply: true,
171                    divisibility: 0,
172                    resource_roles: Default::default(),
173                    metadata: metadata! {
174                        init {
175                            "account_locker" => locker_address, locked;
176                        }
177                    },
178                    address_reservation: Some(badge_reservation),
179                    initial_supply: dec!(1),
180                })
181                .unwrap(),
182            )
183            .map(|rtn| {
184                scrypto_decode::<FungibleResourceManagerCreateWithInitialSupplyOutput>(&rtn)
185                    .unwrap()
186            })?;
187
188        // Preparing all of the roles and rules.
189        let rule = rule!(require(badge_address));
190        let recoverer_rule = match allow_recover {
191            true => rule.clone(),
192            false => rule!(deny_all),
193        };
194
195        Self::instantiate_internal(
196            OwnerRole::Updatable(rule.clone()),
197            rule.clone(),
198            rule.clone(),
199            recoverer_rule.clone(),
200            recoverer_rule,
201            metadata_init! {
202                "admin_badge" => badge_address, locked;
203            },
204            Some(locker_reservation),
205            api,
206        )
207        .map(|rtn| (rtn, badge))
208    }
209
210    fn instantiate_internal<Y: SystemApi<RuntimeError>>(
211        owner_role: OwnerRole,
212        storer_role: AccessRule,
213        storer_updater_role: AccessRule,
214        recoverer_role: AccessRule,
215        recoverer_updater_role: AccessRule,
216        metadata_init: MetadataInit,
217        address_reservation: Option<GlobalAddressReservation>,
218        api: &mut Y,
219    ) -> Result<Global<AccountLockerMarker>, RuntimeError> {
220        // Main module
221        let object_id = api.new_simple_object(ACCOUNT_LOCKER_BLUEPRINT, indexmap! {})?;
222
223        // Role Assignment Module
224        let roles = indexmap! {
225            ModuleId::Main => roles2! {
226                STORER_ROLE => storer_role, updatable;
227                STORER_UPDATER_ROLE => storer_updater_role, updatable;
228                RECOVERER_ROLE => recoverer_role, updatable;
229                RECOVERER_UPDATER_ROLE => recoverer_updater_role, updatable;
230            }
231        };
232        let role_assignment = RoleAssignment::create(owner_role, roles, api)?.0;
233
234        // Metadata Module
235        let metadata = Metadata::create_with_data(metadata_init, api)?;
236
237        // Globalize
238        let address = api.globalize(
239            object_id,
240            indexmap!(
241                AttachedModuleId::RoleAssignment => role_assignment.0,
242                AttachedModuleId::Metadata => metadata.0,
243            ),
244            address_reservation,
245        )?;
246        let component_address = ComponentAddress::new_or_panic(address.as_node_id().0);
247
248        Ok(Global::new(component_address))
249    }
250
251    fn store<Y: SystemApi<RuntimeError>>(
252        AccountLockerStoreInput {
253            claimant,
254            bucket,
255            try_direct_send,
256        }: AccountLockerStoreInput,
257        api: &mut Y,
258    ) -> Result<AccountLockerStoreOutput, RuntimeError> {
259        // If we should try to send first then attempt the deposit into the account
260        let bucket = if try_direct_send {
261            // Getting the node-id of the actor and constructing the non-fungible global id of the
262            // global caller.
263            let actor_node_id = api.actor_get_node_id(ACTOR_STATE_SELF)?;
264            let global_caller_non_fungible_global_id =
265                global_caller(GlobalAddress::new_or_panic(actor_node_id.0));
266
267            let bucket = api
268                .call_method(
269                    claimant.0.as_node_id(),
270                    ACCOUNT_TRY_DEPOSIT_OR_REFUND_IDENT,
271                    scrypto_encode(&AccountTryDepositOrRefundInput {
272                        bucket,
273                        authorized_depositor_badge: Some(global_caller_non_fungible_global_id),
274                    })
275                    .unwrap(),
276                )
277                .map(|rtn| scrypto_decode::<AccountTryDepositOrRefundOutput>(&rtn).unwrap())?;
278            match bucket {
279                Some(bucket) => bucket,
280                None => return Ok(()),
281            }
282        } else {
283            bucket
284        };
285
286        // Deposit into the account either was not requested or failed. Store the resources into the
287        // account locker.
288
289        // Bucket info.
290        let (resource_address, resource_specifier) = bucket_to_resource_specifier(&bucket, api)?;
291
292        // Store in the vault.
293        Self::with_vault_create_on_traversal(
294            claimant.0,
295            resource_address,
296            api,
297            |mut vault, api| vault.put(bucket, api),
298        )?;
299
300        // Emit an event with the stored resource
301        Runtime::emit_event(
302            api,
303            StoreEvent {
304                claimant,
305                resource_address,
306                resources: resource_specifier,
307            },
308        )?;
309
310        Ok(())
311    }
312
313    fn airdrop<Y: SystemApi<RuntimeError>>(
314        AccountLockerAirdropInput {
315            claimants,
316            bucket,
317            try_direct_send,
318        }: AccountLockerAirdropInput,
319        api: &mut Y,
320    ) -> Result<AccountLockerAirdropOutput, RuntimeError> {
321        // Distribute and call `store`
322        let resource_address = bucket.resource_address(api)?;
323        for (account_address, specifier) in claimants.iter() {
324            let claim_bucket = match specifier {
325                ResourceSpecifier::Fungible(amount) => bucket.take(*amount, api)?,
326                ResourceSpecifier::NonFungible(ids) => {
327                    bucket.take_non_fungibles(ids.clone(), api)?.into()
328                }
329            };
330
331            Self::store(
332                AccountLockerStoreInput {
333                    claimant: *account_address,
334                    bucket: claim_bucket,
335                    try_direct_send,
336                },
337                api,
338            )?;
339        }
340
341        if bucket.is_empty(api)? {
342            bucket.drop_empty(api)?;
343            Ok(None)
344        } else {
345            Ok(Some(bucket))
346        }
347    }
348
349    fn recover<Y: SystemApi<RuntimeError>>(
350        AccountLockerRecoverInput {
351            claimant,
352            resource_address,
353            amount,
354        }: AccountLockerRecoverInput,
355        api: &mut Y,
356    ) -> Result<AccountLockerRecoverOutput, RuntimeError> {
357        // Recover the resources from the vault.
358        let bucket = Self::with_vault_create_on_traversal(
359            claimant.0,
360            resource_address,
361            api,
362            |mut vault, api| vault.take(amount, api),
363        )?;
364
365        // Emitting the event
366        let (resource_address, resource_specifier) = bucket_to_resource_specifier(&bucket, api)?;
367        Runtime::emit_event(
368            api,
369            RecoverEvent {
370                claimant,
371                resource_address,
372                resources: resource_specifier,
373            },
374        )?;
375
376        // Return
377        Ok(bucket)
378    }
379
380    fn recover_non_fungibles<Y: SystemApi<RuntimeError>>(
381        AccountLockerRecoverNonFungiblesInput {
382            claimant,
383            resource_address,
384            ids,
385        }: AccountLockerRecoverNonFungiblesInput,
386        api: &mut Y,
387    ) -> Result<AccountLockerRecoverNonFungiblesOutput, RuntimeError> {
388        // Recover the resources from the vault.
389        let bucket = Self::with_vault_create_on_traversal(
390            claimant.0,
391            resource_address,
392            api,
393            |mut vault, api| vault.take_non_fungibles(ids, api),
394        )?;
395
396        // Emitting the event
397        let (resource_address, resource_specifier) = bucket_to_resource_specifier(&bucket, api)?;
398        Runtime::emit_event(
399            api,
400            RecoverEvent {
401                claimant,
402                resource_address,
403                resources: resource_specifier,
404            },
405        )?;
406
407        // Return
408        Ok(bucket)
409    }
410
411    fn claim<Y: SystemApi<RuntimeError>>(
412        AccountLockerClaimInput {
413            claimant,
414            resource_address,
415            amount,
416        }: AccountLockerClaimInput,
417        api: &mut Y,
418    ) -> Result<AccountLockerClaimOutput, RuntimeError> {
419        // Read and assert against the owner role of the claimant.
420        let claimant_owner_role = api
421            .call_module_method(
422                claimant.0.as_node_id(),
423                AttachedModuleId::RoleAssignment,
424                ROLE_ASSIGNMENT_GET_OWNER_ROLE_IDENT,
425                scrypto_encode(&RoleAssignmentGetOwnerRoleInput).unwrap(),
426            )
427            .map(|rtn| scrypto_decode::<RoleAssignmentGetOwnerRoleOutput>(&rtn).unwrap())?;
428        Runtime::assert_access_rule(claimant_owner_role.rule, api)?;
429
430        // Recover the resources from the vault.
431        let bucket = Self::with_vault_create_on_traversal(
432            claimant.0,
433            resource_address,
434            api,
435            |mut vault, api| vault.take(amount, api),
436        )?;
437
438        // Emitting the event
439        let (resource_address, resource_specifier) = bucket_to_resource_specifier(&bucket, api)?;
440        Runtime::emit_event(
441            api,
442            ClaimEvent {
443                claimant,
444                resource_address,
445                resources: resource_specifier,
446            },
447        )?;
448
449        // Return
450        Ok(bucket)
451    }
452
453    fn claim_non_fungibles<Y: SystemApi<RuntimeError>>(
454        AccountLockerClaimNonFungiblesInput {
455            claimant,
456            resource_address,
457            ids,
458        }: AccountLockerClaimNonFungiblesInput,
459        api: &mut Y,
460    ) -> Result<AccountLockerClaimNonFungiblesOutput, RuntimeError> {
461        // Read and assert against the owner role of the claimant.
462        let claimant_owner_role = api
463            .call_module_method(
464                claimant.0.as_node_id(),
465                AttachedModuleId::RoleAssignment,
466                ROLE_ASSIGNMENT_GET_OWNER_ROLE_IDENT,
467                scrypto_encode(&RoleAssignmentGetOwnerRoleInput).unwrap(),
468            )
469            .map(|rtn| scrypto_decode::<RoleAssignmentGetOwnerRoleOutput>(&rtn).unwrap())?;
470        Runtime::assert_access_rule(claimant_owner_role.rule, api)?;
471
472        // Recover the resources from the vault.
473        let bucket = Self::with_vault_create_on_traversal(
474            claimant.0,
475            resource_address,
476            api,
477            |mut vault, api| vault.take_non_fungibles(ids, api),
478        )?;
479
480        // Emitting the event
481        let (resource_address, resource_specifier) = bucket_to_resource_specifier(&bucket, api)?;
482        Runtime::emit_event(
483            api,
484            ClaimEvent {
485                claimant,
486                resource_address,
487                resources: resource_specifier,
488            },
489        )?;
490
491        // Return
492        Ok(bucket)
493    }
494
495    fn get_amount<Y: SystemApi<RuntimeError>>(
496        AccountLockerGetAmountInput {
497            claimant,
498            resource_address,
499        }: AccountLockerGetAmountInput,
500        api: &mut Y,
501    ) -> Result<AccountLockerGetAmountOutput, RuntimeError> {
502        Self::with_vault(claimant.0, resource_address, api, |vault, api| {
503            vault
504                .map(|vault| vault.amount(api))
505                .unwrap_or(Ok(Decimal::ZERO))
506        })
507    }
508
509    fn get_non_fungible_local_ids<Y: SystemApi<RuntimeError>>(
510        AccountLockerGetNonFungibleLocalIdsInput {
511            claimant,
512            resource_address,
513            limit,
514        }: AccountLockerGetNonFungibleLocalIdsInput,
515        api: &mut Y,
516    ) -> Result<AccountLockerGetNonFungibleLocalIdsOutput, RuntimeError> {
517        Self::with_vault(claimant.0, resource_address, api, |vault, api| {
518            vault
519                .map(|vault| vault.non_fungible_local_ids(limit, api))
520                .unwrap_or(Ok(indexset! {}))
521        })
522    }
523
524    fn with_vault_create_on_traversal<Y: SystemApi<RuntimeError>, O>(
525        account_address: ComponentAddress,
526        resource_address: ResourceAddress,
527        api: &mut Y,
528        handler: impl FnOnce(Vault, &mut Y) -> Result<O, RuntimeError>,
529    ) -> Result<O, RuntimeError> {
530        // The collection on the blueprint maps an account address to a key value store. We read the
531        // node id of that key value store.
532        let account_claims_handle = api.actor_open_key_value_entry(
533            ACTOR_STATE_SELF,
534            AccountLockerCollection::AccountClaimsKeyValue.collection_index(),
535            &scrypto_encode(&account_address).unwrap(),
536            LockFlags::MUTABLE,
537        )?;
538        let account_claims = api
539            .key_value_entry_get_typed::<VersionedAccountLockerAccountClaims>(
540                account_claims_handle,
541            )?
542            .map(|entry| entry.fully_update_and_into_latest_version());
543
544        let account_claims_kv_store = match account_claims {
545            Some(account_claims_kv_store) => account_claims_kv_store,
546            None => {
547                // Create a new kv-store
548                let key_value_store = api
549                    .key_value_store_new(
550                        KeyValueStoreDataSchema::new_local_without_self_package_replacement::<
551                            ResourceAddress,
552                            Vault,
553                        >(true),
554                    )
555                    .map(Own)?;
556                // Write the kv-store's node id to the collection entry.
557                api.key_value_entry_set_typed(
558                    account_claims_handle,
559                    AccountLockerAccountClaimsVersions::V1(key_value_store).into_versioned(),
560                )?;
561                // Return the NodeId of the kv-store.
562                key_value_store
563            }
564        };
565
566        // Lock the entry in the key-value store which contains the vault and attempt to get it.
567        let vault_entry_handle = api.key_value_store_open_entry(
568            account_claims_kv_store.as_node_id(),
569            &scrypto_encode(&resource_address).unwrap(),
570            LockFlags::MUTABLE,
571        )?;
572
573        let vault_entry = api.key_value_entry_get_typed::<Vault>(vault_entry_handle)?;
574        let vault = match vault_entry {
575            Some(vault) => vault,
576            None => {
577                // Creating the vault.
578                let vault = Vault::create(resource_address, api)?;
579                // Writing it to the kv-entry
580                api.key_value_entry_set_typed(vault_entry_handle, Vault(vault.0))?;
581                // Return the vault.
582                vault
583            }
584        };
585
586        // Call the callback - if the callback fails then the following code will not be executed
587        // and the substate locks will not be released. We are making the assumption that a failed
588        // callback that returns an `Err(RuntimeError)` can not be recovered from.
589        let rtn = handler(vault, api)?;
590
591        // Close the opened kv-entries.
592        api.key_value_entry_close(vault_entry_handle)?;
593        api.key_value_entry_close(account_claims_handle)?;
594
595        // Return the rtn result
596        Ok(rtn)
597    }
598
599    fn with_vault<Y: SystemApi<RuntimeError>, O>(
600        account_address: ComponentAddress,
601        resource_address: ResourceAddress,
602        api: &mut Y,
603        handler: impl FnOnce(Option<Vault>, &mut Y) -> Result<O, RuntimeError>,
604    ) -> Result<O, RuntimeError> {
605        // The collection on the blueprint maps an account address to a key value store. We read the
606        // node id of that key value store.
607        let account_claims_handle = api.actor_open_key_value_entry(
608            ACTOR_STATE_SELF,
609            AccountLockerCollection::AccountClaimsKeyValue.collection_index(),
610            &scrypto_encode(&account_address).unwrap(),
611            LockFlags::read_only(),
612        )?;
613        let account_claims = api
614            .key_value_entry_get_typed::<VersionedAccountLockerAccountClaims>(
615                account_claims_handle,
616            )?
617            .map(|entry| entry.fully_update_and_into_latest_version());
618
619        let account_claims_kv_store = match account_claims {
620            Some(account_claims_kv_store) => account_claims_kv_store,
621            None => {
622                // Call the callback function.
623                let rtn = handler(None, api)?;
624                // Dropping the lock on the collection entry.
625                api.key_value_entry_close(account_claims_handle)?;
626                // Return the result of the callback.
627                return Ok(rtn);
628            }
629        };
630
631        // Lock the entry in the key-value store which contains the vault and attempt to get it. If
632        // we're allowed to create the vault.
633        let vault_entry_handle = api.key_value_store_open_entry(
634            account_claims_kv_store.as_node_id(),
635            &scrypto_encode(&resource_address).unwrap(),
636            LockFlags::read_only(),
637        )?;
638
639        let vault_entry = api.key_value_entry_get_typed::<Vault>(vault_entry_handle)?;
640
641        // Call the callback - if the callback fails then the following code will not be executed
642        // and the substate locks will not be released. We are making the assumption that a failed
643        // callback that returns an `Err(RuntimeError)` can not be recovered from.
644        let rtn = handler(vault_entry, api)?;
645
646        // Close the opened kv-entries.
647        api.key_value_entry_close(vault_entry_handle)?;
648        api.key_value_entry_close(account_claims_handle)?;
649
650        // Return the rtn result
651        Ok(rtn)
652    }
653}
654
655fn bucket_to_resource_specifier<Y: SystemApi<RuntimeError>>(
656    bucket: &Bucket,
657    api: &mut Y,
658) -> Result<(ResourceAddress, ResourceSpecifier), RuntimeError> {
659    let resource_address = bucket.resource_address(api)?;
660    if resource_address.is_fungible() {
661        let amount = bucket.amount(api)?;
662        Ok((resource_address, ResourceSpecifier::Fungible(amount)))
663    } else {
664        let ids = bucket.non_fungible_local_ids(api)?;
665        Ok((resource_address, ResourceSpecifier::NonFungible(ids)))
666    }
667}