diff --git a/basic/flexi_faucet/.cargo/config b/basic/flexi_faucet/.cargo/config new file mode 100644 index 00000000..a05a706a --- /dev/null +++ b/basic/flexi_faucet/.cargo/config @@ -0,0 +1,2 @@ +[build] +rustdocflags = ["--document-private-items"] diff --git a/basic/flexi_faucet/.gitignore b/basic/flexi_faucet/.gitignore new file mode 100644 index 00000000..e2a3069b --- /dev/null +++ b/basic/flexi_faucet/.gitignore @@ -0,0 +1,2 @@ +/target +*~ diff --git a/basic/flexi_faucet/Cargo.toml b/basic/flexi_faucet/Cargo.toml new file mode 100644 index 00000000..ed277377 --- /dev/null +++ b/basic/flexi_faucet/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "flexi_faucet" +version = "1.0.0" +edition = "2021" + +[dependencies] +sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.11.0" } +scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.11.0" } + +[dev-dependencies] +transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.11.0" } +radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.11.0" } +scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v0.11.0" } + +[profile.release] +opt-level = 's' # Optimize for size. +lto = true # Enable Link Time Optimization. +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = 'abort' # Abort on panic. +strip = "debuginfo" # Strip debug info. +overflow-checks = true # Panic in the case of an overflow. + +[lib] +crate-type = ["cdylib", "lib"] + +[workspace] +# Set the package crate as its own empty workspace, to hide it from any potential ancestor workspace +# Remove this [workspace] section if you intend the package to be part of a Cargo workspace \ No newline at end of file diff --git a/basic/flexi_faucet/README.md b/basic/flexi_faucet/README.md new file mode 100644 index 00000000..94137ec1 --- /dev/null +++ b/basic/flexi_faucet/README.md @@ -0,0 +1,19 @@ +# FlexiFaucet - A flexible faucet with multiple configuration options + +## How to build the blueprint +Make sure you have the necessary toolchain installed, see +[here](https://docs-babylon.radixdlt.com/main/getting-started-developers/getting-started-developers.html) +for details. You will need Scrypto 0.11.0. +- From the command line, in the `flexifaucet` directory, run `scrypto build` + +### How to run the test suite +- From the command line, in the `flexifaucet` directory, run `scrypto test` + +The test suite includes transaction manifest building for the +blueprint's public API. + +### How to generate the documentation +- From the command line, in the `flexifaucet` directory, run `cargo doc` + +The generated web pages contain detailed documentation on how the +blueprint works. diff --git a/basic/flexi_faucet/src/lib.rs b/basic/flexi_faucet/src/lib.rs new file mode 100644 index 00000000..529de126 --- /dev/null +++ b/basic/flexi_faucet/src/lib.rs @@ -0,0 +1,1051 @@ +//! A faucet blueprint with very flexible configuration options. +//! +//! A faucet is a service that gives away some amount of tokens to +//! users, for free. It can typically be provided for marketing +//! purposes, for fun and games, or even perhaps out of sheer +//! generosity. +//! +//! The FlexiFaucet is able to hand out fungible or non-fungible +//! tokens. It can be configured to be open to the public, or it can +//! be limited to only holders of a specific badge resource. Those +//! badges can be fungible or non-fungible. Users can be limited in +//! how much they are able to tap, and how often a tap can happen. If +//! non-fungible user badges are employed then these limitations are +//! applied per-badge so that users have individually tracked limits. +//! +//! # Public interface +//! +//! These are the functions and methods this blueprint exposes to be +//! called from a transaction manifest. +//! +//! ## Creation functions +//! +//! - [new] Creates a new `FlexiFaucet` instance. +//! +//! - [new_non_fungible_resource] Creates a non-fungible resource +//! suitable to use as a non-fungible token that will be minted by +//! this faucet. +//! +//! ## Activation +//! +//! - [activate] Turns on the faucet, allowing users to start calling +//! [tap]. +//! +//! - [deactivate] Turns off the faucet, causing subsequent calls to +//! [tap] to panic. +//! +//! - [active] Determines whether the faucet is currently active. +//! +//! ## Minting +//! +//! - [set_minting_badge] Establishes or changes the minting badge to +//! use. So long as a minting badge is set the faucet will attempt to +//! mint tokens to cover any shortfall in the treasury. +//! +//! - [minting_badge_as_fungible] Retrieves information on the current +//! minting badge as if it were a fungible, which is to say it returns +//! the resource address and how many tokens are held. +//! +//! - [minting_badge_as_non_fungible] Retrieves information about the +//! current non-fungible minting badge. +//! +//! ## Treasury +//! +//! - [fund_treasury] Puts tokens into the faucet's treasury, which +//! will then be used to fund [tap] calls from users. +//! +//! - [defund_treasury_fungible] Takes tokens out of the treasury as +//! if they were fungible, which is to say it pulls them out by +//! amount. +//! +//! - [defund_treasury_non_fungible] Takens non-fungible tokens out of +//! the treasury. +//! +//! - [empty_treasury] Completely empties out the treasury. +//! +//! - [treasury_as_fungible] Retrieves information about the current +//! treasury as if it were fungible, which is to say it returns the +//! amount. +//! +//! - [treasury_as_non_fungible] Retrieves information about the +//! current non-fungible treasury. +//! +//! ## The main job +//! +//! - [tap] This method is called to access its main function, namely +//! to distribute tokens to our users. +//! +//! # Roles and access control +//! +//! The blueprint sets a fixed owner role, and assigns the owner as +//! the only one with access to the two roles we define. If you want +//! `admin` or `funder` roles to be accessible to others, you can +//! change those roles to your suiting. See [Treasury](#treasury-1) +//! for an example of how to do this. +//! +//! We define the following roles: +//! +//! - `admin` Can call the administrative methods of the +//! blueprint. The owner can change this role. +//! +//! - `funder` Can call the [fund_treasury] method. An admin can +//! change this role. +//! +//! Beyond this, anyone can call the creation functions and the query +//! methods. +//! +//! The [tap] method is open for everyone to call but depending on how +//! you have configured the faucet, there is internal access checking +//! that will stop unwanted users. +//! +//! # Typical call sequences +//! +//! ## Creating a faucet +//! +//! When initially setting up a faucet you will want to first have +//! created the token resource that the faucet is going to hand out, +//! you will want to have created any taker badge resource you intend +//! to use, and if you want the faucet to mint you also want to have +//! the minting badge resource ready. You can inspect the test suite +//! to see how this may be done. +//! +//! 1. Call [new_non_fungible_resource] if you want the faucet to be +//! minting non-fungible tokens to hand out. +//! +//! 2. Call [new] to create the new faucet. +//! +//! 3. Call [set_minting_badge] if you want the faucet to be able to +//! mint tokens. +//! +//! 4. Call [fund_treasury] if you want the faucet to hand out tokens +//! that have already been created. +//! +//! 5. Call [activate] to open up for business. +//! +//! ## Taking a tap from the faucet +//! +//! 1. Call [tap]. +//! +//! 2. That's it. There's no 2. Goto 1! +//! +//! # Faucet tap limits +//! +//! You can limit how much and how frequently the faucet can be used. +//! +//! The token limit per tap restricts how many tokens can be returned +//! from a single call to the [tap] method. Set this to e.g. 5 tokens +//! and anyone trying to call it to tap 6 will face an error message. +//! +//! The cooldown period restricts how frequently the [tap] method can +//! be called. Set this to e.g. 600 seconds and the faucet cannot be +//! tapped more than once every ten minutes. (Take note that the +//! ledger cannot tell time with higher precision than one full +//! minute, so if you set cooldown periods less than about two minutes +//! you may start seeing funny rounding effects.) +//! +//! Normally these two parameters work globally for all users, such +//! that if Alice has exceeded the limits with her call then Bob also +//! has to deal with the consequences of that. +//! +//! If you restrict tapping to owners of non-fungible taker badges +//! however, each user (actually each badge) tracks its limits +//! individually so that even if Alice has reached her limit, Bob can +//! still get *his* tap in. In this case, the token limit per tap is +//! modified to instead be a token limit per cooldown period such that +//! if Bob taps 2 tokens initially against a max limit of 5, then he +//! can still tap another 3 within the same cooldown period. +//! +//! # Taker badges +//! +//! User badges, which we also call taker badges, can be of any +//! resource: They can be a specifically badge-like resource +//! (i.e. divisibility 0 and typically minted in very low supply), or +//! they can be anything else. You can use XRD as your badge resource +//! if you like, or VKC, or RaDragon NFTs, or whatever. So you can +//! make a faucet for holders of your NFT collection as a service to +//! your community, for example; or you can make a faucet for "anyone +//! who holds at least 1000 VKC," if you're looking to reward +//! high-stake holders in your community. If you want to target +//! whales, you could make a faucet for "anyone who holds at least 1 +//! million XRD" -- and if you're catering even to smaller fry you +//! could allow "anyone who holds at least 0.01 BTC." +//! +//! If you use fungible badges (or no badges at all, meaning the +//! faucet is open to everyone) then the tap limit and cooldowns you +//! set apply globally. That is, if Alice taps at time t=100 then +//! *everyone* has to wait for the cooldown to run out at e.g. time +//! t=200 before anyone can tap again. +//! +//! If you use non-fungible badges however then max tap limits and +//! cooldowns are tracked per badge. In this case, if Alice taps at +//! time t=100 then *she* has to wait until t=200 to tap again; but +//! Bob could tap at time t=101 unaffected by Alice's activity (he +//! would then have to wait until t=201 to tap again). +//! +//! Note that as we do not use self-produced NFTs as badges, we do not +//! store cooldown period info inside non-fungible data. Instead we +//! can use *any* NFT series as badges and we store cooldown periods +//! inside a `KeyValueStore` in component state. This enables us to +//! market this faucet towards e.g. all the various NFT collections +//! such that everyone can use their favourite Undying Viking or +//! whatever to tap magic internet money from the faucet. +//! +//! # Token sources +//! +//! Tokens for tapping must come from one of two sources (or both): +//! Either the treasury, or from minting. +//! +//! The faucet always tries to take from treasury first, and only when +//! treasury runs out does it resort to minting new tokens. +//! +//! ## Treasury +//! +//! You can fund the treasury by calling [fund_treasury] and passing +//! in a pile of tokens. +//! +//! Note that in the default configuration that method can only be +//! called by the owner. If you want the general public to be able to +//! fund your faucet, you can change the access control of that method +//! by running a transaction manifest built along these lines: +//! +//! ```text +//! ManifestBuilder::new() +//! ... +//! .create_proof_from_account_of_non_fungibles( +//! owner_account, +//! owner_badge.resource_address(), +//! &BTreeSet::from([owner_badge.local_id().clone()])) +//! .update_role(flexifaucet, +//! ObjectModuleId::Main, +//! RoleKey::new("funder"), +//! rule!(allow_all)) +//! ... +//! ``` +//! +//! You can see the above in action in +//! `test_change_fund_treasury_access_control()` in the test suite. +//! (Of course, you don't have to set `allow_all` in there -- you can +//! set any rule you want.) +//! +//! In the case of drawing from treasury, if the treasury runs out +//! (and you haven't configured minting) then attempts to [tap] will +//! panic. +//! +//! ## Mint +//! +//! If you give the faucet a minting badge for its token resource then +//! it will be able to create new tokens to cover demand. +//! +//! When it comes to fungibles, we can mint anything you can provide a +//! minting badge for. For non-fungibles however we can only mint +//! resources that have been created with our own +//! [new_non_fungible_resource] function. The faucet can support other +//! types of non-fungibles than this but they will have to be provided +//! via treasury only. +//! +//! Minting can panic if the minting badge is wrong or, notably, in +//! the event that you run into cost unit limits while minting +//! non-fungibles. The latter can happen when trying to mint a large +//! number of non-fungible tokens in the same transaction. +//! +//! # Making your non-fungible resource +//! +//! For this faucet to be able to mint non-fungible tokens, they need +//! to be based on a non-fungible struct we know at compile-time. For +//! this reason the faucet can only mint non-fungibles that are based +//! on its own `FaucetNfData` non-fungible data structure. +//! +//! You can use the [new_non_fungible_resource] function to make a +//! resource like that. +//! +//! That function has been designed such that it gives you complete +//! flexibility in how to configure the new resource after it has been +//! made. The intention is that you first create the resource, then +//! you configure it with all the access rules, metadata, etc., that +//! you want, then you pass the new resource into the faucet. +//! +//! In `assert_metadata_and_access_update()` in the test suite there +//! is a transaction manifest which demonstrates how to change these +//! access rules. +//! +//! Also note that the [new_non_fungible_resource] function has a few +//! convenience parameters that I hope will make it feasible for +//! *most* people to receive an immediately usable resource out of the +//! function without having to do a lot of fiddly follow-up +//! configuration of it. +//! +//! # Making your fungible resource +//! +//! You can use absolutely any fungible resource, and the faucet will +//! be able to mint it so long as you can deliver it a minting badge. +//! +//! In the test suite there is an example of a transaction manifest +//! for building a fungible resource with custom roles in +//! `create_mintable_fungible_resource()` -- you may find this useful +//! if you're creating such a resource for use in this faucet. +//! +//! Also, there is a transaction manifest for *changing* the minting +//! role of a resource in `update_minting_role()`. This is maybe less +//! routinely useful but nevertheless it's there if you need it. +//! +//! # About minting badge vault handling +//! +//! In this component we allow admins to replace the minting badge in +//! use. The idea is that the resources we operate on can have their +//! roles changed and their badges replaced (perhaps in response to a +//! security threat) and the faucet should support such an +//! eventuality. +//! +//! A complication that arises is that when you replace the minting +//! badge with one of a wholly different resource, you cannot just +//! re-use the vault of the previous badge because the vault is locked +//! to the resource address it originally had. Instead you need to +//! make an entirely new vault to put the new badge in, and you need +//! to discard the old vault. +//! +//! Scrypto however doesn't support the discarding of vaults: you +//! always need to hang on to them. In order to accommodate this we +//! put our vaults in a `KeyValueStore` that is keyed by an +//! ever-increasing index and in which the highest index entry is the +//! vault that is in use. All other vaults are old discarded ones. +//! +//! You can see the handling of this by inspecting our use of the +//! `minting_badge` field of our component state. +//! +//! (This would have been a lot easier to deal with if Vault had had a +//! `drop_empty_vault` method -- then I probably could have let +//! `minting_badge` be an actual Vault and just use +//! `std::mem::replace` to switch it out. Here's hoping for Scrypto +//! v0.12!) +//! +//! # Test suite +//! +//! In the `tests` directory you will find a comprehensive test suite +//! for the faucet that runs through all its functionality. This can +//! be instructive both in demonstrating various ways in which the +//! faucet can be used, in showing how to build the various +//! transaction manifests needed, and of course in how to write tests +//! with `TestRunner`. +//! +//! ## Transaction manifests +//! +//! Transaction manifests for all publicly exposed methods are built +//! as part of the test suite. Look for the following methods in the +//! `tests/lib.rs` file: +//! +//! | Scrypto function | Transaction manifest in | +//! |---------------------------------|-------------------------| +//! | [new] | `call_new()` +//! | [new_non_fungible_resource] | `call_new_non_fungible_resource()` +//! | [activate] | `call_activate()` +//! | [deactivate] | `call_deactivate()` +//! | [active] | `call_active()` +//! | [set_minting_badge] | `call_set_minting_badge_f()`, `call_set_minting_badge_f()` +//! | [minting_badge_as_fungible] | `call_minting_badge_as_fungible()` +//! | [minting_badge_as_non_fungible] | `call_minting_badge_as_non_fungible()` +//! | [fund_treasury] | `call_fund_treasury_f()`, `call_fund_treasury_nf()` +//! | [defund_treasury_fungible] | `call_defund_treasury_fungible()` +//! | [defund_treasury_non_fungible] | `call_defund_treasury_non_fungible()` +//! | [empty_treasury] | `call_empty_treasury()` +//! | [treasury_as_fungible] | `call_treasury_as_fungible()` +//! | [treasury_as_non_fungible] | `call_treasury_as_non_fungible()` +//! | [tap] | `call_tap()` +//! +//! Note that the transaction manifests built in the test suite are +//! all run without charging transaction fees, and so none of them +//! bother to lock fees. If you intend to use them towards an actual +//! ledger you will want to add a `lock_fee` instruction at the top. +//! +//! ## Clock manipulation +//! +//! When setting the system clock in the test runner, note my use of +//! the global variable `ROUND_COUNTER` to make sure I always increase +//! the consensus round when changing the clock. A better way to do +//! this (nobody likes global variables) may have been to pass the +//! round counter around together with the test runner, but that is +//! a problem to solve for a future evolution of my test harness. +//! +//! # About this blueprint +//! +//! This project has been developed on Ubuntu Linux and while the +//! author expects everything in here to work on other platforms, if +//! you're having weird problems with it maybe this is the reason. +//! +//! This project has been developed for Scrypto v0.11.0 and was +//! submitted to the Scrypto developer incentive program which at time +//! of writing is available +//! [here.](https://www.radixdlt.com/blog/scrypto-developer-program-incentives) +//! +//! The author can be reached at `scryptonight@proton.me` +//! +//! +//! [new]: crate::flexi_faucet::FlexiFaucet::new +//! [new_non_fungible_resource]: crate::flexi_faucet::FlexiFaucet::new_non_fungible_resource +//! [active]: crate::flexi_faucet::FlexiFaucet::active +//! [minting_badge_as_fungible]: crate::flexi_faucet::FlexiFaucet::minting_badge_as_fungible +//! [minting_badge_as_non_fungible]: crate::flexi_faucet::FlexiFaucet::minting_badge_as_non_fungible +//! [treasury_as_fungible]: crate::flexi_faucet::FlexiFaucet::treasury_as_fungible +//! [treasury_as_non_fungible]: crate::flexi_faucet::FlexiFaucet::treasury_as_non_fungible +//! [activate]: crate::flexi_faucet::FlexiFaucet::activate +//! [deactivate]: crate::flexi_faucet::FlexiFaucet::deactivate +//! [set_minting_badge]: crate::flexi_faucet::FlexiFaucet::set_minting_badge +//! [defund_treasury_fungible]: crate::flexi_faucet::FlexiFaucet::defund_treasury_fungible +//! [defund_treasury_non_fungible]: crate::flexi_faucet::FlexiFaucet::defund_treasury_non_fungible +//! [empty_treasury]: crate::flexi_faucet::FlexiFaucet::empty_treasury +//! [fund_treasury]: crate::flexi_faucet::FlexiFaucet::fund_treasury +//! [tap]: crate::flexi_faucet::FlexiFaucet::tap +use scrypto::prelude::*; + +#[derive(ScryptoSbor)] +struct CooldownPeriod { + /// Unix-era seconds. + period_start: Option, + /// How much has been tapped this period. + tap_total: Decimal, +} + +#[derive(ScryptoSbor, ManifestSbor, NonFungibleData)] +pub struct FaucetNfData { + pub faucet: ComponentAddress, + #[mutable] + pub attributes: HashMap, +} + +#[blueprint] +mod flexi_faucet { + + enable_method_auth! { + roles { + admin => updatable_by: [OWNER]; + funder => updatable_by: [admin]; + }, + methods { + active => PUBLIC; + minting_badge_as_fungible => PUBLIC; + minting_badge_as_non_fungible => PUBLIC; + treasury_as_fungible => PUBLIC; + treasury_as_non_fungible => PUBLIC; + activate => restrict_to: [admin]; + deactivate => restrict_to: [admin]; + set_minting_badge => restrict_to: [admin]; + defund_treasury_fungible => restrict_to: [admin]; + defund_treasury_non_fungible => restrict_to: [admin]; + empty_treasury => restrict_to: [admin]; + + // If you want others than admins to be able to fund, + // change the funder role. + fund_treasury => restrict_to: [funder]; + + // The below method employs intent-based access control: + // It is restricted to our taker_badges if those have been + // set to Some(...), or else if they have been set to None + // then the tap is open to the public. + tap => PUBLIC; + } + } + + struct FlexiFaucet { + active: bool, + token: ResourceAddress, + max_per_tap: Option, + tap_cooldown_secs: Option, + last_tap: Option, + cooldown_periods: KeyValueStore, + taker_badges: Option, + minimum_fungible_badge_tokens : Option, + minting_badge: KeyValueStore>, + minting_badge_index: u128, + treasury: Option, + } + + impl FlexiFaucet { + /// Creates a new faucet and returns it. + /// + /// The new component will be owned by `owner` and will be + /// able to emit `token` type tokens. Its treasury and/or + /// minting will be for that resource. + /// + /// You may optionally specify a `max_per_tap` that limits how + /// many tokens can be extracted by each call of the `tap` + /// method. + /// + /// You may also optionally specify `tap_cooldown_secs` which + /// enforces a delay between subsequent taps. + /// + /// The faucet will be available to everyone unless you + /// specify `taker_badges`. With those specified only someone + /// who can prove such a badge may use the `tap` method. + /// + /// If you specify fungible `taker_badges` then you must also + /// specify how many tokens of that resource are necessary to + /// qualify for using the faucet. (If your `taker_badges` are + /// non-fungible then the amount needed is always exactly 1.) + /// + /// If you specify all three of `max_per_tap`, + /// `tap_cooldown_secs` and `taker_badges` then the maximum + /// per tap is treated as a maximum total tap within a single + /// cooldown period. For example, if you have 5 max and + /// cooldown 10, then you could tap 1 token at time 0, 2 more + /// tokens at time 7, a final 3 tokens at time 9; and then + /// you're all tapped out until next period starts (in this + /// case, at time 10). + /// + /// If you provide a `minting_badge` then the faucet will mint + /// tokens if necessary to service users. + /// + /// This function panics if it detects a problem with the + /// input arguments. + pub fn new(owner: NonFungibleGlobalId, + token: ResourceAddress, + max_per_tap: Option, + tap_cooldown_secs: Option, + taker_badges: Option, + minimum_fungible_badge_tokens: Option, + minting_badge: Option) + -> Global + { + if let Some(taker_badges) = taker_badges { + assert!(!taker_badges.is_fungible() + || minimum_fungible_badge_tokens.is_some(), + "fungible taker badges must have a minimum amount set"); + } else { + assert!(minimum_fungible_badge_tokens.is_none(), + "don't set minimum badge tokens without badges"); + } + + let minting_badge_store = KeyValueStore::new(); + if let Some(bucket) = minting_badge { + minting_badge_store.insert(0, + Some(Vault::with_bucket(bucket))); + } else { + minting_badge_store.insert(0, None); + } + Self { + active: false, + token, + max_per_tap, + tap_cooldown_secs, + last_tap: None, + cooldown_periods: KeyValueStore::new(), + taker_badges, + minimum_fungible_badge_tokens, + minting_badge: minting_badge_store, + minting_badge_index: 0, + treasury: None, + } + .instantiate() + .prepare_to_globalize( + OwnerRole::Fixed(rule!(require(owner.clone())))) + .roles(roles!( + admin => rule!(require(owner.clone())); + funder => rule!(require(owner)); + )) + .globalize() + } + + /// If you want this faucet to be able to mint non-fungible + /// tokens to provide to your users then the token resource + /// used needs to be created using this function. + /// + /// This function creates a new non-fungible resource. By + /// default we create one that is maximally flexible so that + /// you, the owner of the resource, can afterwards use normal + /// ledger calls (e.g. `update_role`, `set_metadata`) to + /// configure it to your liking. The main unique feature the + /// resource has from being created by us is that it uses our + /// `FaucetNfData` non-fungible data structure, and *that* you + /// cannot change. + /// + /// Access to configure and use the new resource is mostly + /// (see below) governed by the `owner_rule` you specify. As + /// this is an `AccessRule` it can be as simple or as complex + /// as you need it to be. When building your transaction + /// manifest it can look something like this: + /// + /// ```text + /// manifest_args!(rule!(require(resource_address)), ...) + /// ``` + /// + /// Where `resource_address` is the badge resource you want to + /// use for ownership. + /// + /// The resource created is a RUID-based non-fungible. New + /// tokens created on this resource will receive arbitrarily + /// selected non-fungible IDs, but be aware that the user + /// composing the transaction is effectively in some control + /// of what that ID will be. These IDs are *not* strongly + /// random. Don't use them as a random-source for a lottery. + /// + /// In order for the default resource we create to be + /// maximally configurable by you, the owner, it starts out + /// being freezable and recallable. Many owners may find this + /// to be undesirable (because holders tend to distrust such + /// tokens), and in order to make life easier for you there is + /// a `recallable` argument to the function that you can set + /// to `false` to cause the resource to *not* be freezable or + /// recallable and for this not to be changeable. + /// + /// Likewise, the default resource starts without any metadata + /// but many owners will want to set the `name` and + /// `description` of their NFT resource, and to make life a + /// little bit easier in this regard you can optionally supply + /// a `name` and/or `description` as arguments to the + /// function. + /// + /// Notice that our `FaucetNfData` struct has a mutable + /// `attributes` map that you can use to store per-NFT data. + /// You would need to do this on your own after each token has + /// been minted, the faucet doesn't do it for you. + /// + /// # Our default role rules + /// + /// - The owner can change the owner role. + /// - Everyone can deposit or withdraw the token. + /// - `minting_badge` (only) can mint the token. + /// - The owner can burn, recall or freeze the token as well as + /// update its metadata. + /// - The owner can change all these rules. + /// - **Except:** if `recallable` was `false` then the owner + /// *cannot* recall or freeze and *cannot* change the recall + /// or freeze rules. + /// + /// # Our default metadata + /// + /// - If you provide a `name` then this will be the `name` + /// metadata field and it will be locked. + /// - If you provide a `description` then this will be the + /// `descripton` metadata field and it will be locked. + /// - Any of those you do not provide, as well as all other + /// metadata fields, you can set later yourself using your + /// owner role. + pub fn new_non_fungible_resource(owner_rule: AccessRule, + minting_badge: ResourceAddress, + recallable: bool, + name: Option, + description: Option) + -> ResourceAddress + { + let recaller_rule = if recallable { owner_rule.clone() } + else { rule!(deny_all) }; + + let mut builder = + ResourceBuilder::new_ruid_non_fungible::( + OwnerRole::Updatable(owner_rule.clone())) + .mint_roles(mint_roles!( + minter => rule!(require(minting_badge)); + minter_updater => owner_rule.clone(); + )) + .burn_roles(burn_roles!( + burner => owner_rule.clone(); + burner_updater => owner_rule.clone(); + )) + .withdraw_roles(withdraw_roles!( + withdrawer => rule!(allow_all); + withdrawer_updater => owner_rule.clone(); + )) + .deposit_roles(deposit_roles!( + depositor => rule!(allow_all); + depositor_updater => owner_rule.clone(); + )) + .recall_roles(recall_roles!( + recaller => recaller_rule.clone(); + recaller_updater => recaller_rule.clone(); + )) + .freeze_roles(freeze_roles!( + freezer => recaller_rule.clone(); + freezer_updater => recaller_rule; + )) + .non_fungible_data_update_roles(non_fungible_data_update_roles!( + non_fungible_data_updater => owner_rule.clone(); + non_fungible_data_updater_updater => owner_rule; + )); + + if name.is_some() || description.is_some() { + let mut metadata = MetadataInit::new(); + if let Some(name) = name { + metadata.set_and_lock_metadata( + "name", name); + } + if let Some(description) = description { + metadata.set_and_lock_metadata( + "description", description); + } + builder = builder.metadata(ModuleConfig { + init: metadata, + roles: RolesInit::new(), + }); + } + + builder + .create_with_no_initial_supply() + .address() + } + + /// Activates the faucet, meaning that the `tap` method + /// becomes available. + pub fn activate(&mut self) { + self.active = true; + } + + /// Deactivates the faucet, causing calls to the `tap` method + /// to start panicking. + pub fn deactivate(&mut self) { + self.active = false; + } + + /// Determines whether the faucet is currently active or not. + pub fn active(&self) -> bool { + self.active + } + + /// Changes the badge to use for minting tokens. You call this + /// for one of three reasons: + /// + /// - To initially set a minting badge, passing a bucket + /// holding the minting badge. + /// + /// - To turn off minting by passing it `None`. + /// + /// - To change the minting badge (possibly because the + /// resource itself had its minting badge replaced) into a + /// different one. + /// + /// If a minting badge was already set then the old minting + /// badge bucket will be returned out of the method. + pub fn set_minting_badge(&mut self, minting_badge: Option) + -> Option + { + let orig_index = self.minting_badge_index; + + // First store the new minting badge + self.minting_badge_index += 1; + self.minting_badge.insert( + self.minting_badge_index, + minting_badge.map(|b| Vault::with_bucket(b))); + + // Then fetch the previous minting badge and return it to + // the user. This leaves its empty vault behind which we + // will never use again. + // + // (This whole thing would be a lot easier to deal with if + // there was a Vault::drop_empty_vault method that would + // let us simply delete a vault when it's empty.) + let mut old_badge = self.minting_badge.get_mut(&orig_index); + let old_badge = old_badge.as_mut().unwrap(); + if old_badge.is_some() { + let v = old_badge.as_mut().unwrap(); + Some(v.take_all()) + } else { + None + } + } + + /// Determines the identity of the current minting badge(s), + /// returning the resource address and all the non-fungible + /// local ids in our minting badge bucket. + /// + /// If the current minting badge is fungible then this method + /// will panic. + pub fn minting_badge_as_non_fungible(&self) -> + Option<(ResourceAddress, BTreeSet)> + { + let old_badge = self.minting_badge.get(&self.minting_badge_index); + let old_badge = old_badge.as_ref().unwrap(); + if old_badge.is_some() { + let v = old_badge.as_ref().unwrap(); + Some(( v.resource_address(), + v.as_non_fungible().non_fungible_local_ids() )) + } else { + None + } + } + + /// Determines the identity of the current minting badge(s), + /// returning the resource address and quantity of tokens in + /// our minting badge bucket. + /// + /// This method works equally well whether our minting badge + /// is fungible or non-fungible, but asking for the amount is + /// a fungible-ish activity hence the method name. + pub fn minting_badge_as_fungible(&self) -> + Option<(ResourceAddress, Decimal)> + { + let old_badge = self.minting_badge.get(&self.minting_badge_index); + let old_badge = old_badge.as_ref().unwrap(); + if old_badge.is_some() { + let v = old_badge.as_ref().unwrap(); + Some(( v.resource_address(), + v.amount() )) + } else { + None + } + } + + /// Adds tokens to our treasury. If the input bucket holds + /// tokens other than our intended token type this method + /// panics. + pub fn fund_treasury(&mut self, funds: Bucket) { + assert_eq!(self.token, funds.resource_address(), + "wrong resource"); + + if let Some(ref mut vault) = self.treasury { + vault.put(funds); + } else { + self.treasury = Some(Vault::with_bucket(funds)); + } + } + + /// Takes tokens out of our treasury. This works for fungibles + /// and non-fungibles alike. + pub fn defund_treasury_fungible(&mut self, amount: Decimal) -> Bucket { + self.treasury.as_mut().unwrap().take(amount) + } + + /// Takes tokens out of our treasury. + /// + /// If our treasury tokens are fungible then this method + /// panics. + pub fn defund_treasury_non_fungible(&mut self, + nflids: BTreeSet) + -> Bucket { + self.treasury.as_mut().unwrap().as_non_fungible() + .take_non_fungibles(&nflids) + .into() + } + + /// Takes all tokens out of our treasury. + pub fn empty_treasury(&mut self) -> Bucket { + self.treasury.as_mut().unwrap().take_all() + } + + /// Determines the contents of our treasury. This method works + /// equally well for fungible and non-fungible treasuries. + pub fn treasury_as_fungible(&self) -> Decimal { + if let Some(vault) = &self.treasury { + vault.amount() + } else { + Decimal::ZERO + } + } + + /// Determines the contents of our treasury. + /// + /// If our treasury tokens are fungible then this method + /// panics. + pub fn treasury_as_non_fungible(&self) -> BTreeSet { + if let Some(vault) = &self.treasury { + vault.as_non_fungible().non_fungible_local_ids() + } else { + BTreeSet::new() + } + } + + /// Retrieves funds from the faucet, as per the ruleset + /// specified for it. This is the method our users call to + /// make use of the faucet service. + /// + /// This method can panic for a few main reasons: + /// + /// - The faucet isn't currently active. + /// + /// - You asked for 0 tokens + /// + /// - You're not authorized to tap. + /// + /// - You are trying to tap beyond the limit restriction. + /// + /// - You are asking us to mint a non-whole number of + /// non-fungibles. + /// + /// - The treasury has insufficient tokens and we cannot mint + /// to cover the shortfall. + pub fn tap(&mut self, taker: Option, amount: Decimal) -> Bucket { + assert!(self.active, "faucet not active"); + + // We don't want requests for zero tokens to enter into + // our logic below, resetting period counters etc., so we + // just shortcut it here. (An alternative way of handling + // it would have been to return an empty bucket.) + assert!(!amount.is_zero(), "ask for more than zero tokens"); + + // If these get set to false it's because tapping got + // limit-checked by NFT-local restrictions and further + // checking is not needed. + let mut use_global_cooldown_limit = true; + let mut use_global_max_tap_limit = true; + + let now = unix_time_now(); + + if let Some(taker_badges) = self.taker_badges { + // This is our intent-based access check + let taker = taker.unwrap().check(taker_badges); + + if taker_badges.is_fungible() { + assert!(taker.amount() + >= self.minimum_fungible_badge_tokens.unwrap(), + "not enough badges"); + } else { + use_global_cooldown_limit = false; + let taker = taker.as_non_fungible(); + let taker_nfgid = NonFungibleGlobalId::new( + taker.resource_address(), taker.non_fungible_local_id()); + + if let Some(tap_cooldown_secs) = self.tap_cooldown_secs { + use_global_max_tap_limit = false; + + if self.cooldown_periods.get(&taker_nfgid).is_none() { + // Baby's first cooldown period + self.cooldown_periods.insert( + taker_nfgid.clone(), + CooldownPeriod { + period_start: None, // Will be filled in later + tap_total: Decimal::ZERO, + }); + } + + let mut cooldown_period = + self.cooldown_periods.get_mut(&taker_nfgid).unwrap(); + + let start_new_period = + if let Some(period_start) = cooldown_period.period_start + { + info!("now {}, period_start {}, tap_cooldown_secs {}", + now, period_start, tap_cooldown_secs); + now - period_start >= tap_cooldown_secs as i64 + } else { true }; + + if start_new_period { + cooldown_period.period_start = Some(now); + cooldown_period.tap_total = Decimal::ZERO; + } else { + // If we're within the same period as the + // previous tap, we only allow this if + // there's a max-per-period limit to count + // it against. + assert!(self.max_per_tap.is_some(), + "tap less often"); + } + + if let Some(max_per_tap) = self.max_per_tap { + // Check remaining limit + assert!(max_per_tap + >= cooldown_period.tap_total + amount, + "you tap too much"); + } + cooldown_period.tap_total += amount; + } + } + } else { + // There is no access check here because if we get + // here the faucet is configured to be open for all. + } + + if use_global_cooldown_limit { + if let Some(tap_cooldown_secs) = self.tap_cooldown_secs { + if let Some(last_tap) = self.last_tap { + assert!(now - last_tap >= tap_cooldown_secs as i64, + "tap too frequent"); + } + } + } + + if use_global_max_tap_limit { + if let Some(max_per_tap) = self.max_per_tap { + assert!(max_per_tap >= amount, "tap too big"); + } + } + + self.last_tap = Some(now); + + // If we got all this way without a panic then it's time + // to pay out. + self.pay_out(amount) + } + + /// Helper method to generate the funds we need to give to the + /// user. It primarily takes from treasury, and if the + /// treasury isn't enough then it tries to mint. If it tries + /// to mint but doesn't have a minting badge then it will + /// panic. + fn pay_out(&mut self, amount: Decimal) -> Bucket { + let mut loot; + if let Some(ref mut treasury) = self.treasury { + if treasury.amount() >= amount { + return treasury.take(amount); + } + loot = treasury.take_all(); + } else { + loot = Bucket::new(self.token); + } + + // If we get here then the treasury was not enough so we + // have to mint the rest. + + let minting_badge = + self.minting_badge.get(&self.minting_badge_index).unwrap(); + + let mint_amount = amount - loot.amount(); + let mut mint_fn = || loot.put(self.mint_loot(mint_amount)); + + if minting_badge.is_none() { + // Mint without auth, may well fail but worth trying. + // (Note that if we used the SELF virtual badge to + // guard minting, the following statement would result + // in a successful mint.) + mint_fn(); + } else { + let minting_badge = minting_badge.as_ref().unwrap(); + // Mint with auth + if minting_badge.resource_address().is_fungible() { + // Fungible mint should never fail + minting_badge.as_fungible().authorize_with_amount( + minting_badge.amount(), + || mint_fn()); + } else { + let minting_badge = minting_badge.as_non_fungible(); + minting_badge.authorize_with_non_fungibles( + &minting_badge.non_fungible_local_ids(), + || mint_fn()); + } + } + loot + } + + /// Helper method to mint the tokens we give to our users. + /// Authorization to mint must already have been established. + fn mint_loot(&self, amount: Decimal) -> Bucket + { + if self.token.is_fungible() { + ResourceManager::from(self.token).mint(amount) + } else { + // In an ideal world, counter would be an integer type. + let mut counter = amount.floor(); + assert!(counter == amount, + "ask for whole number of non-fungibles"); + let mut loot_bucket = Bucket::new(self.token); + + // Note that non-fungible mint may fail if we mint too + // many tokens, because of cost unit limits. Last time + // this was tested - with Scrypto v0.11 - we could + // mint about 160 before things started getting dicey. + while counter >= Decimal::ONE { + loot_bucket.put( + ResourceManager::from(self.token).mint_ruid_non_fungible( + FaucetNfData { + faucet: Runtime::global_address(), + attributes: HashMap::new(), + })); + // This might be a very expensive way to do loop + // management. + counter -= Decimal::ONE; + } + loot_bucket + } + } + } +} + +/// Helper function to determine the current UNIX-era time. +fn unix_time_now() -> i64 { + Clock::current_time_rounded_to_minutes().seconds_since_unix_epoch +} diff --git a/basic/flexi_faucet/tests/lib.rs b/basic/flexi_faucet/tests/lib.rs new file mode 100644 index 00000000..535e136b --- /dev/null +++ b/basic/flexi_faucet/tests/lib.rs @@ -0,0 +1,3004 @@ +use scrypto::prelude::*; +use scrypto_unit::*; +use transaction::builder::ManifestBuilder; + +use transaction::prelude::*; +use radix_engine::transaction::{CommitResult, BalanceChange}; +use scrypto::api::ObjectModuleId; + +use flexi_faucet::FaucetNfData; + +/// A convenience struct for grouping together a user account's data. +struct User { + pubkey: Secp256k1PublicKey, + _privkey: Secp256k1PrivateKey, + account: ComponentAddress, +} + +impl User { + /// Creates a new user account on the ledger. + fn new(test_runner: &mut TestRunner) -> Self { + let (user_pubk, user_privk, user_account) = + test_runner.new_allocated_account(); + + User { + pubkey: user_pubk, + _privkey: user_privk, + account: user_account, + } + } +} + + +/// Our badge requirement can be either fungible or non-fungible, and +/// this enum encapsulates that. +#[derive(Clone)] +enum BadgeSpec { + Fungible(ResourceAddress, Decimal), + NonFungible(NonFungibleGlobalId) +} + + +/// Mints a number of the faucet's non-fungible tokens, based on +/// `FaucetNfData` non-fungible data. +fn mint_ruid_non_fungibles(test_runner: &mut TestRunner, + notary: &User, + resource: ResourceAddress, + minting_badge: &NonFungibleGlobalId, + faucet: ComponentAddress, + amount: u64) { + + let mut minted = 0; + + // We mint in batches because there is a max-substates-write limit + // that we might hit otherwise when making lots of NFTs. + while minted < amount { + let mut to_mint = amount - minted; + if to_mint > 150 { to_mint = 150; } + + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + minting_badge.resource_address(), + &BTreeSet::from([minting_badge.local_id().clone()])) + .mint_ruid_non_fungible( + resource, + (0..to_mint).map( + |_| FaucetNfData { + faucet, attributes: HashMap::new() + }) + .collect::>()) + .deposit_batch(notary.account) + .build(); + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + receipt.expect_commit_success(); + minted += to_mint; + } +} + +/// Creates a new non-fungible resource and returns it and one of its +/// non-fungibles. +fn create_nf_resource_and_badge(test_runner: &mut TestRunner, user: &User) -> + (ResourceAddress, NonFungibleGlobalId) +{ + let resource = test_runner.create_non_fungible_resource(user.account); + + (resource, + NonFungibleGlobalId::new(resource, 1.into())) +} + +/// Creates a new fungible resource where the supplied +/// `minting_badges` resource has access to mint tokens. Everyone can +/// deposit and withdraw as well as change minter role. Everything +/// else is forbidden. +fn create_mintable_fungible_resource( + test_runner: &mut TestRunner, + notary: &User, + minting_badges: ResourceAddress, + supply: Option) -> ResourceAddress +{ + let roles = FungibleResourceRoles { + mint_roles: mint_roles!( + minter => rule!(require(minting_badges)); + minter_updater => rule!(allow_all); + ), + withdraw_roles: withdraw_roles!( + withdrawer => rule!(allow_all); + withdrawer_updater => rule!(deny_all); + ), + deposit_roles: deposit_roles!( + depositor => rule!(allow_all); + depositor_updater => rule!(deny_all); + ), + burn_roles: None, + freeze_roles: None, + recall_roles: None, + }; + let manifest = ManifestBuilder::new() + .create_fungible_resource(OwnerRole::None, + true, + 18, + roles, + metadata!(), + supply) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + receipt.expect_commit_success().new_resource_addresses()[0] +} + +/// Changes the minting role on a resource to require +/// `minting_badges`. Minting role update must be available to +/// everyone for this to work. +fn update_minting_role( + test_runner: &mut TestRunner, + notary: &User, + resource: ResourceAddress, + minting_badges: ResourceAddress) +{ + let manifest = ManifestBuilder::new() + .update_role(resource, + ObjectModuleId::Main, + RoleKey::new(MINTER_ROLE), + rule!(require(minting_badges))) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + receipt.expect_commit_success(); +} + +/// Builds a transaction manifest for `FlexiFaucet::new` and runs +/// it. Sets minting badge to None. Always expects success. +fn call_new(test_runner: &mut TestRunner, + package: PackageAddress, + notary: &User, + owner: &NonFungibleGlobalId, + token: ResourceAddress, + max_per_tap: Option, + tap_cooldown_secs: Option, + taker_badges: Option, + minimum_fungible_badge_tokens: Option) + -> (ComponentAddress, CommitResult) { + + let manifest = + ManifestBuilder::new() + .call_function( + package, + "FlexiFaucet", + "new", + manifest_args!( + owner, + token, + max_per_tap, + tap_cooldown_secs, + taker_badges, + minimum_fungible_badge_tokens, + None::, + ), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = receipt.expect_commit_success(); + let component = receipt.expect_commit(true).new_component_addresses()[0]; + + (component, result.clone()) +} + +/// Builds a transaction manifest for +/// `FlexiFaucet::call_new_non_fungible_resource` and runs it. Always +/// expects success. +fn call_new_non_fungible_resource( + test_runner: &mut TestRunner, + package: PackageAddress, + notary: &User, + owner: &NonFungibleGlobalId, + minting_badge: ResourceAddress, + recallable: bool, + name: Option, + description: Option) + -> (ResourceAddress, CommitResult) { + + let manifest = + ManifestBuilder::new() + .call_function( + package, + "FlexiFaucet", + "new_non_fungible_resource", + manifest_args!( + rule!(require(owner.clone())), + minting_badge, + recallable, + name, + description, + ), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = receipt.expect_commit_success(); + let resource = receipt.expect_commit(true).new_resource_addresses()[0]; + + (resource, result.clone()) +} + +/// Builds a transaction manifest for `FlexiFaucet::new` and runs +/// it. Supports fungible minting badges only. Always expects +/// success. +fn call_new_f(test_runner: &mut TestRunner, + package: PackageAddress, + notary: &User, + owner: &NonFungibleGlobalId, + token: ResourceAddress, + max_per_tap: Option, + tap_cooldown_secs: Option, + taker_badges: Option, + minimum_fungible_badge_tokens: Option, + minting_badge: (ResourceAddress, Decimal)) + -> (ComponentAddress, CommitResult) { + + let manifest = + ManifestBuilder::new() + .withdraw_from_account( + notary.account, + minting_badge.0, + minting_badge.1) + .take_all_from_worktop(minting_badge.0, + "minting_badge_bucket") + .call_function_with_name_lookup( + package, + "FlexiFaucet", + "new", + |lookup| manifest_args!( + owner, + token, + max_per_tap, + tap_cooldown_secs, + taker_badges, + minimum_fungible_badge_tokens, + Some(lookup.bucket("minting_badge_bucket")), + ), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = receipt.expect_commit_success(); + let component = receipt.expect_commit(true).new_component_addresses()[0]; + + (component, result.clone()) +} + +/// Builds a transaction manifest for `FlexiFaucet::new` and runs +/// it. Supports non-fungible minting badges only. Always expects +/// success. +fn call_new_nf(test_runner: &mut TestRunner, + package: PackageAddress, + notary: &User, + owner: &NonFungibleGlobalId, + token: ResourceAddress, + max_per_tap: Option, + tap_cooldown_secs: Option, + taker_badges: Option, + minimum_fungible_badge_tokens: Option, + minting_badge: &NonFungibleGlobalId) + -> (ComponentAddress, CommitResult) { + + let manifest = + ManifestBuilder::new() + .withdraw_non_fungibles_from_account( + notary.account, + minting_badge.resource_address(), + &BTreeSet::from([minting_badge.local_id().clone()])) + .take_all_from_worktop(minting_badge.resource_address(), + "minting_badge_bucket") + .call_function_with_name_lookup( + package, + "FlexiFaucet", + "new", + |lookup| manifest_args!( + owner, + token, + max_per_tap, + tap_cooldown_secs, + taker_badges, + minimum_fungible_badge_tokens, + Some(lookup.bucket("minting_badge_bucket")), + ), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = receipt.expect_commit_success(); + let component = receipt.expect_commit(true).new_component_addresses()[0]; + + (component, result.clone()) +} + + +/// Builds a transaction manifest for `FlexiFaucet::activate` and runs +/// it. +fn call_activate(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .call_method( + flexifaucet, + "activate", + manifest_args!(), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for `FlexiFaucet::deactivate` and +/// runs it. +fn call_deactivate(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .call_method( + flexifaucet, + "deactivate", + manifest_args!(), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for `FlexiFaucet::active` and runs +/// it, returning its return value. +fn call_active(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User) + -> (bool, CommitResult) { + let manifest = ManifestBuilder::new() + .call_method( + flexifaucet, + "active", + manifest_args!(), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = + receipt.expect_commit_success(); + + (result.output(1), result.clone()) +} + +/// Builds a transaction manifest for +/// `FlexiFaucet::minting_badge_as_fungible` and runs it, returning +/// its return value if successful. +fn call_minting_badge_as_fungible( + test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User) + -> (Option<(ResourceAddress, Decimal)>, CommitResult) +{ + let manifest = ManifestBuilder::new() + .call_method( + flexifaucet, + "minting_badge_as_fungible", + manifest_args!(), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = + receipt.expect_commit_success(); + + (result.output::>(1) + .map(|(addr, amount)| (addr, amount)), + result.clone()) +} + +/// Builds a transaction manifest for +/// `FlexiFaucet::minting_badge_as_non_fungible` and runs it, +/// returning its return value. Always expects success. +fn call_minting_badge_as_non_fungible( + test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User) + -> (Option<(ResourceAddress, BTreeSet)>, + CommitResult) +{ + let manifest = ManifestBuilder::new() + .call_method( + flexifaucet, + "minting_badge_as_non_fungible", + manifest_args!(), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = + receipt.expect_commit_success(); + + (result.output::)>>(1) + .map(|(addr, amount)| (addr, amount)), + result.clone()) +} + +/// Builds a transaction manifest for `FlexiFaucet::set_minting_badge` +/// to clear the minting badge for the faucet, and runs it. +fn call_set_minting_badge_to_none(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .call_method( + flexifaucet, + "set_minting_badge", + manifest_args!(None::)) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for `FlexiFaucet::set_minting_badge` +/// to set a fungible minting badge, and runs it. +fn call_set_minting_badge_f(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + minting_badge: (ResourceAddress, Decimal), + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .withdraw_from_account( + notary.account, + minting_badge.0, + minting_badge.1) + .take_all_from_worktop(minting_badge.0, + "minting_badge_bucket") + .call_method_with_name_lookup( + flexifaucet, + "set_minting_badge", + |lookup| manifest_args!(Some(lookup.bucket("minting_badge_bucket")))) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for `FlexiFaucet::set_minting_badge` +/// to set a non-fungible minting badge, and runs it. +fn call_set_minting_badge_nf(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + minting_badge: &NonFungibleGlobalId, + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .withdraw_non_fungibles_from_account( + notary.account, + minting_badge.resource_address(), + &BTreeSet::from([minting_badge.local_id().clone()])) + .take_all_from_worktop(minting_badge.resource_address(), + "minting_badge_bucket") + .call_method_with_name_lookup( + flexifaucet, + "set_minting_badge", + |lookup| manifest_args!(Some(lookup.bucket("minting_badge_bucket")))) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for +/// `FlexiFaucet::treasury_as_fungible`and runs it, returning the +/// return value. Always expects success. +fn call_treasury_as_fungible(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User) + -> (Decimal, CommitResult) { + let manifest = ManifestBuilder::new() + .call_method( + flexifaucet, + "treasury_as_fungible", + manifest_args!(), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = receipt.expect_commit_success(); + + (result.output(1), result.clone()) +} + +/// Builds a transaction manifest for +/// `FlexiFaucet::treasury_as_non_fungible`and runs it, returning the +/// return value. Always expects success. +fn call_treasury_as_non_fungible( + test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User) + -> (BTreeSet, + CommitResult) +{ + let manifest = ManifestBuilder::new() + .call_method( + flexifaucet, + "treasury_as_non_fungible", + manifest_args!(), + ) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + let result = receipt.expect_commit_success(); + + (result.output(1), result.clone()) +} + +/// Builds a transaction manifest for `FlexiFaucet::fund_treasury` +/// with fungible tokens, and runs it. +fn call_fund_treasury_f(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + owner: &NonFungibleGlobalId, + funds: (ResourceAddress, Decimal), + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .withdraw_from_account( + notary.account, + funds.0, + funds.1) + .take_all_from_worktop(funds.0, + "funds_bucket") + .create_proof_from_account_of_non_fungibles( + notary.account, + owner.resource_address(), + &BTreeSet::from([owner.local_id().clone()])) + .call_method_with_name_lookup( + flexifaucet, + "fund_treasury", + |lookup| manifest_args!(lookup.bucket("funds_bucket"))) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for `FlexiFaucet::fund_treasury` +/// with non-fungible tokens, and runs it. +fn call_fund_treasury_nf(test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + owner: &NonFungibleGlobalId, + funds: (ResourceAddress, BTreeSet), + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .withdraw_non_fungibles_from_account( + notary.account, + funds.0, + &funds.1) + .take_all_from_worktop(funds.0, + "funds_bucket") + .create_proof_from_account_of_non_fungibles( + notary.account, + owner.resource_address(), + &BTreeSet::from([owner.local_id().clone()])) + .call_method_with_name_lookup( + flexifaucet, + "fund_treasury", + |lookup| manifest_args!(lookup.bucket("funds_bucket"))) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for +/// `FlexiFaucet::defund_treasury_fungible` and runs it. +fn call_defund_treasury_fungible( + test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + amount: Decimal, + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .call_method( + flexifaucet, + "defund_treasury_fungible", + manifest_args!(amount)) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for +/// `FlexiFaucet::defund_treasury_non_fungible` and runs it. +fn call_defund_treasury_non_fungible( + test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + nflids: BTreeSet, + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .call_method( + flexifaucet, + "defund_treasury_non_fungible", + manifest_args!(nflids)) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for `FlexiFaucet::empty_treasury` +/// and runs it. +fn call_empty_treasury( + test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + user: &NonFungibleGlobalId, + succeed: bool) + -> CommitResult { + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + notary.account, + user.resource_address(), + &BTreeSet::from([user.local_id().clone()])) + .call_method( + flexifaucet, + "empty_treasury", + manifest_args!()) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +/// Builds a transaction manifest for `FlexiFaucet::tap` and runs it. +fn call_tap( + test_runner: &mut TestRunner, + flexifaucet: ComponentAddress, + notary: &User, + badge: Option, + amount: Decimal, + succeed: bool) + -> CommitResult { + let mut manifest = ManifestBuilder::new(); + if let Some(user) = &badge { + manifest = + match user { + BadgeSpec::NonFungible(nfgid) => manifest + .create_proof_from_account_of_non_fungibles( + notary.account, + nfgid.resource_address(), + &BTreeSet::from([nfgid.local_id().clone()])), + + BadgeSpec::Fungible(resource, amount) => manifest + .create_proof_from_account_of_amount( + notary.account, + resource.clone(), + *amount) + } + .pop_from_auth_zone("user_proof") + + } + let manifest = manifest + .call_method_with_name_lookup( + flexifaucet, + "tap", + |lookup| manifest_args!( + if badge.is_some() { Some(lookup.proof("user_proof")) } else { None }, + amount)) + .deposit_batch(notary.account) + .build(); + + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(¬ary.pubkey)], + ); + + if succeed != receipt.is_commit_success() { + println!("RECEIPT: {:?}", receipt); + } + + if succeed { receipt.expect_commit_success() } + else { receipt.expect_commit_failure() } + .clone() +} + +static mut ROUND_COUNTER: u64 = 2; + +/// Changes the test runner clock. +fn set_test_runner_clock( + test_runner: &mut TestRunner, + time_secs: i64) { + unsafe { + test_runner.advance_to_round_at_timestamp( + Round::of(ROUND_COUNTER), // must be incremented each time + time_secs * 1000, + ); + ROUND_COUNTER += 1; + } +} + +/// Helper function to try to change rules and metadata in a resource +/// to see if we can, and asserts that we cannot for those +/// rules/metadata that are supposed to be locked. +/// +/// The `succeed` parameter is subdivided as follows: +/// +/// .0 Should change of `name` metadata succeed? +/// +/// .1 Should change of ``description` metadata succeed? +/// +/// .2 Should change of non-recall/freeze access rules succeed? +/// +/// .3 Should change of recall and freeze access rules succeed? +fn assert_metadata_and_access_update(test_runner: &mut TestRunner, + token_resaddr: ResourceAddress, + manifest_preamble: impl Fn() -> ManifestBuilder, + execution_proofs: Vec, + succeed: ( bool, bool, bool, bool )) { + for (name, value, succeed) in + [ + ("name", "a name", succeed.0 ), + ("description", "a description", succeed.1 ) + ] + { + let manifest = manifest_preamble() + .set_metadata(token_resaddr, name, value) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, execution_proofs.clone()) + .expect_commit(succeed); + } + + for (succeed, role) in + [ + (succeed.2, MINTER_ROLE), + (succeed.2, MINTER_UPDATER_ROLE), + (succeed.2, BURNER_ROLE), + (succeed.2, BURNER_UPDATER_ROLE), + (succeed.2, WITHDRAWER_ROLE), + (succeed.2, WITHDRAWER_UPDATER_ROLE), + (succeed.2, DEPOSITOR_ROLE), + (succeed.2, DEPOSITOR_UPDATER_ROLE), + (succeed.3, RECALLER_ROLE), + (succeed.3, RECALLER_UPDATER_ROLE), + (succeed.3, FREEZER_ROLE), + (succeed.3, FREEZER_UPDATER_ROLE), + (succeed.2, NON_FUNGIBLE_DATA_UPDATER_ROLE), + (succeed.2, NON_FUNGIBLE_DATA_UPDATER_UPDATER_ROLE), + ] + { + let manifest = manifest_preamble() + .update_role( + token_resaddr, ObjectModuleId::Main, RoleKey::new(role), + rule!(allow_all)) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, + execution_proofs.clone()) + .expect_commit(succeed); + } +} + +/// Tests that `new_non_fungible_resource` when invoked to create a +/// fully flexible resource actually creates a resource the roles and +/// metadata of which can then be changed. +#[test] +fn test_new_non_fungible_resource_fully_flexible() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((owner_badges_resaddr, owner_badge_nfgid), + (minting_badges_resaddr, _), + (unrelated_badges_resaddr, unrelated_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + // Create a fully flexible resource + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + true, + None, + None); + + + // First we will try to change all the things using the wrong + // proof. This should never work. + + // Making this a closure so we can use it many times (because + // ManifestBuilder can't be cloned) + let manifest_preamble = || ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + unrelated_badges_resaddr, + &BTreeSet::from([unrelated_badge_nfgid.local_id().clone()])); + let execution_proofs = vec![NonFungibleGlobalId::from_public_key(&alice.pubkey)]; + + assert_metadata_and_access_update(&mut test_runner, + token_resaddr, + manifest_preamble, + execution_proofs.clone(), + (false, false, false, false)); + + // Now change all the things using the proper proof. This should + // work. + let manifest_preamble = || ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + owner_badges_resaddr, + &BTreeSet::from([owner_badge_nfgid.local_id().clone()])); + + assert_metadata_and_access_update(&mut test_runner, + token_resaddr, + manifest_preamble, + execution_proofs, + (true, true, true, true)); +} + +/// Tests that `new_non_fungible_resource` when invoked to create a +/// resource with locked metadata and locked recall/freeze actually +/// creates a resource on which those aspects cannot be changed. +#[test] +fn test_new_non_fungible_resource_locked_roles() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((owner_badges_resaddr, owner_badge_nfgid), + (minting_badges_resaddr, _), + (unrelated_badges_resaddr, unrelated_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + // Create a resource with locked name and description metadata, + // and disabled recall/freeze access + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + false, + Some("my name".to_owned()), + Some("my description".to_owned())); + + + // First try with a badge that's not allowed + let manifest_preamble = || ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + unrelated_badges_resaddr, + &BTreeSet::from([unrelated_badge_nfgid.local_id().clone()])); + let execution_proofs = vec![NonFungibleGlobalId::from_public_key(&alice.pubkey)]; + + assert_metadata_and_access_update(&mut test_runner, + token_resaddr, + manifest_preamble, + execution_proofs.clone(), + (false, false, false, false)); + + // And then the proper proof. It should be able to change + // everything except the locked metadata and the disabled access + // rules. + let manifest_preamble = || ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + owner_badges_resaddr, + &BTreeSet::from([owner_badge_nfgid.local_id().clone()])); + + assert_metadata_and_access_update(&mut test_runner, + token_resaddr, + manifest_preamble, + execution_proofs.clone(), + (false, false, true, false)); + +} + +/// Tests that `new_non_fungible_resource` when invoked to create a +/// resource with only one locked metadata field ("name") creates a +/// resource on which that field cannot be changed. +#[test] +fn test_new_non_fungible_resource_lock_name_metadata_field() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + let execution_proofs = vec![NonFungibleGlobalId::from_public_key(&alice.pubkey)]; + + let ((owner_badges_resaddr, owner_badge_nfgid), + (minting_badges_resaddr, _)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + // Create a resource with locked name and description metadata, + // and disabled recall/freeze access + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + true, + Some("my name".to_owned()), + None); + + // We should be able to change everything except "name" metadata + // field + let manifest_preamble = || ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + owner_badges_resaddr, + &BTreeSet::from([owner_badge_nfgid.local_id().clone()])); + + assert_metadata_and_access_update(&mut test_runner, + token_resaddr, + manifest_preamble, + execution_proofs.clone(), + (false, true, true, true)); +} + +/// Tests that `new_non_fungible_resource` when invoked to create a +/// resource with only one locked metadata field ("description") +/// creates a resource on which that field cannot be changed. +#[test] +fn test_new_non_fungible_resource_lock_description_metadata_field() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + let execution_proofs = vec![NonFungibleGlobalId::from_public_key(&alice.pubkey)]; + + let ((owner_badges_resaddr, owner_badge_nfgid), + (minting_badges_resaddr, _)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + // Create a resource with locked name and description metadata, + // and disabled recall/freeze access + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + true, + None, + Some("my description".to_owned())); + + // We should be able to change everything except "description" + // metadata field + let manifest_preamble = || ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + owner_badges_resaddr, + &BTreeSet::from([owner_badge_nfgid.local_id().clone()])); + + assert_metadata_and_access_update(&mut test_runner, + token_resaddr, + manifest_preamble, + execution_proofs.clone(), + (true, false, true, true)); +} + +/// Tests that `new_non_fungible_resource` creates a resource that can +/// thereafter have tokens minted for it. +#[test] +fn test_new_non_fungible_resource_minting() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (minting_badges_resaddr, minting_badge_nfgid), + (unrelated_badges_resaddr, unrelated_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + // Testing mint + + // Create a resource (flexible or not doesn't matter) + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + true, + None, + None); + let execution_proofs = vec![NonFungibleGlobalId::from_public_key(&alice.pubkey)]; + + + // Mint with unrelated badge should fail + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + unrelated_badges_resaddr, + &BTreeSet::from([unrelated_badge_nfgid.local_id().clone()])) + .mint_ruid_non_fungible(token_resaddr, [FaucetNfData{ + faucet:alice.account, attributes:HashMap::new()}]) + .deposit_batch(alice.account) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, execution_proofs.clone()) + .expect_commit(false); + + + // Mint with minting badge should succeed + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + minting_badges_resaddr, + &BTreeSet::from([minting_badge_nfgid.local_id().clone()])) + .mint_ruid_non_fungible(token_resaddr, [FaucetNfData{ + faucet:alice.account, attributes:HashMap::new()}]) + .deposit_batch(alice.account) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, execution_proofs.clone()) + .expect_commit(true); +} + +/// Tests that `activate`, `deactivate` and `active` all do what +/// they're supposed to. +#[test] +fn test_activate_deactivate() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let (_, owner_nfgid) = + create_nf_resource_and_badge(&mut test_runner, &alice); + + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_nfgid, + RADIX_TOKEN, + None, None, None, None); + + assert!(!call_active(&mut test_runner, flexifaucet, &alice).0, + "Faucet should start inactive"); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_nfgid, + true); + + assert!(call_active(&mut test_runner, flexifaucet, &alice).0, + "Faucet should now be active"); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_nfgid, + true); + + assert!(call_active(&mut test_runner, flexifaucet, &alice).0, + "Faucet should still be active"); + + call_deactivate(&mut test_runner, + flexifaucet, + &alice, + &owner_nfgid, + true); + + assert!(!call_active(&mut test_runner, flexifaucet, &alice).0, + "Faucet should be inactive again"); + + call_deactivate(&mut test_runner, + flexifaucet, + &alice, + &owner_nfgid, + true); + + assert!(!call_active(&mut test_runner, flexifaucet, &alice).0, + "Faucet should still be inactive"); +} + + +/// Tests that a faucet that starts out with a non-fungible minting +/// badge can have its minting badge successfully altered into other +/// non-fungibles, into a fungible, and also into not having a minting +/// badge. +#[test] +fn test_set_minting_badge_initial_non_fungible() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + // Start with non-fungible badge + let nfbadges_resaddr = + test_runner.create_non_fungible_resource(alice.account); + let owner_badge_nfgid = + NonFungibleGlobalId::new(nfbadges_resaddr, 1.into()); + let minting_badge1_nfgid = + NonFungibleGlobalId::new(nfbadges_resaddr, 2.into()); + let minting_badge2_nfgid = + NonFungibleGlobalId::new(nfbadges_resaddr, 3.into()); + + let (flexifaucet1, _) = + call_new_nf(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + RADIX_TOKEN, + None, None, None, None, + &minting_badge1_nfgid); + + let (minting_badge, _) = + call_minting_badge_as_non_fungible(&mut test_runner, flexifaucet1, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(minting_badge1_nfgid.resource_address(), + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(1, + minting_badge.1.len(), + "Minting badge should have exactly one nflid"); + assert_eq!(minting_badge1_nfgid.local_id(), + minting_badge.1.first().unwrap(), + "Minting badge should be correct nflid"); + + // Second nf badge resource + call_set_minting_badge_nf(&mut test_runner, + flexifaucet1, + &alice, + &owner_badge_nfgid, + &minting_badge2_nfgid, + true); + + let (minting_badge, _) = + call_minting_badge_as_non_fungible(&mut test_runner, flexifaucet1, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(minting_badge2_nfgid.resource_address(), + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(1, + minting_badge.1.len(), + "Minting badge should have exactly one nflid"); + assert_eq!(minting_badge2_nfgid.local_id(), + minting_badge.1.first().unwrap(), + "Minting badge should be correct nflid"); + + // Switch to fungible badge resource + let fbadges1_resaddr = + test_runner.create_fungible_resource(dec!(10), 0, alice.account); + + call_set_minting_badge_f(&mut test_runner, + flexifaucet1, + &alice, + &owner_badge_nfgid, + (fbadges1_resaddr, dec!(1)), + true); + + let (minting_badge, _) = + call_minting_badge_as_fungible(&mut test_runner, flexifaucet1, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(fbadges1_resaddr, + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(dec!(1), + minting_badge.1, + "Minting badge should be correct amount"); + + // Switch to no badge + call_set_minting_badge_to_none(&mut test_runner, + flexifaucet1, + &alice, + &owner_badge_nfgid, + true); + + let (minting_badge, _) = + call_minting_badge_as_fungible(&mut test_runner, flexifaucet1, &alice); + + assert!(minting_badge.is_none(), + "Should be no minting badge"); +} + +/// Tests that a faucet that starts out with a fungible minting +/// badge can have its minting badge successfully altered into other +/// fungibles, into a non-fungible, and also into not having a minting +/// badge. +#[test] +fn test_set_minting_badge_initial_fungible() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let (nfbadges_resaddr, owner_badge_nfgid) = + create_nf_resource_and_badge(&mut test_runner, &alice); + + let minting_badge2_nfgid = + NonFungibleGlobalId::new(nfbadges_resaddr, 3.into()); + + // Start with fungible badge + + // First f-badge + let fbadges1_resaddr = + test_runner.create_fungible_resource(dec!(10), 0, alice.account); + + let (flexifaucet, _) = + call_new_f(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + RADIX_TOKEN, + None, None, None, None, + (fbadges1_resaddr, dec!(3))); + + let (minting_badge, _) = + call_minting_badge_as_fungible(&mut test_runner, flexifaucet, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(fbadges1_resaddr, + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(dec!(3), + minting_badge.1, + "Minting badge should be correct amount"); + + + // Second f-badge + let fbadges2_resaddr = + test_runner.create_fungible_resource(dec!(10), 0, alice.account); + + call_set_minting_badge_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (fbadges2_resaddr, dec!(2)), + true); + + let (minting_badge, _) = + call_minting_badge_as_fungible(&mut test_runner, flexifaucet, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(fbadges2_resaddr, + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(dec!(2), + minting_badge.1, + "Minting badge should be correct amount"); + + + // Third f-badge + let fbadges3_resaddr = + test_runner.create_fungible_resource(dec!(10), 0, alice.account); + + call_set_minting_badge_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (fbadges3_resaddr, dec!(5)), + true); + + let (minting_badge, _) = + call_minting_badge_as_fungible(&mut test_runner, flexifaucet, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(fbadges3_resaddr, + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(dec!(5), + minting_badge.1, + "Minting badge should be correct amount"); + + + // Then no badge + call_set_minting_badge_to_none(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + let (minting_badge, _) = + call_minting_badge_as_fungible(&mut test_runner, flexifaucet, &alice); + + assert!(minting_badge.is_none(), + "Should be no minting badge"); + + + // Fourth f-badge + let fbadges4_resaddr = + test_runner.create_fungible_resource(dec!(10), 0, alice.account); + + call_set_minting_badge_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (fbadges4_resaddr, dec!(7)), + true); + + let (minting_badge, _) = + call_minting_badge_as_fungible(&mut test_runner, flexifaucet, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(fbadges4_resaddr, + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(dec!(7), + minting_badge.1, + "Minting badge should be correct amount"); + + // Switch to nf minting badge. + call_set_minting_badge_nf(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + &minting_badge2_nfgid, + true); + + let (minting_badge, _) = + call_minting_badge_as_non_fungible(&mut test_runner, flexifaucet, &alice); + + let minting_badge = minting_badge.unwrap(); + assert_eq!(minting_badge2_nfgid.resource_address(), + minting_badge.0, + "Minting badge should be correct resource"); + assert_eq!(1, + minting_badge.1.len(), + "Minting badge should have exactly one nflid"); + assert_eq!(minting_badge2_nfgid.local_id(), + minting_badge.1.first().unwrap(), + "Minting badge should be correct nflid"); +} + + +/// Tests `fund_treasury`, `defund_treasury_fungible`, +/// `empty_treasury` and `treasury_as_fungible` on a fungible +/// treasury. +#[test] +fn test_fund_defund_treasury_fungible() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let (owner_badge_resaddr, owner_badge_nfgid) = + create_nf_resource_and_badge(&mut test_runner, &alice); + + let token_resaddr = create_mintable_fungible_resource( + &mut test_runner, + &alice, + owner_badge_resaddr, + Some(dec!(1000))); + + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, None, None, None); + + // Testing f and nf read of empty treasury + assert_eq!(dec!(0), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Starting treasury should be empty"); + assert_eq!(0, + call_treasury_as_non_fungible(&mut test_runner, flexifaucet, &alice) + .0.len(), + "Starting treasury should be empty"); + + // Funding with wrong token should fail + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (RADIX_TOKEN, dec!(100)), + false); + + // Funding with correct token should succeed + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(100)), + true); + + assert_eq!(dec!(100), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have funds"); + + // Adding more funds should succeed + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(10)), + true); + + assert_eq!(dec!(110), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have more funds"); + + // Removing funds should succeed + call_defund_treasury_fungible(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + dec!(50), + true); + assert_eq!(dec!(60), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have less funds"); + + // Removing non-fungible funds should fail + call_defund_treasury_non_fungible(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + BTreeSet::from( + [owner_badge_nfgid.local_id().clone()]), + false); + + // Removing everything should succeed + call_empty_treasury(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + assert_eq!(dec!(0), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now be empty"); +} + +/// Tests `fund_treasury`, `defund_treasury_non_fungible`, +/// `empty_treasury`, `treasury_as_non_fungible` and +/// `treasury_as_fungible` on a non-fungible treasury. +#[test] +fn test_fund_defund_treasury_non_fungible() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (token_resaddr, _), + (other_nf_resaddr, _)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, None, None, None); + + // Testing f and nf read of empty treasury + assert_eq!(dec!(0), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Starting treasury should be empty"); + assert_eq!(0, + call_treasury_as_non_fungible(&mut test_runner, flexifaucet, &alice) + .0.len(), + "Starting treasury should be empty"); + + // Funding with wrong token should fail + call_fund_treasury_nf(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (other_nf_resaddr, BTreeSet::from([1.into()])), + false); + + // Funding with correct token should succeed + call_fund_treasury_nf(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, BTreeSet::from([1.into()])), + true); + + assert_eq!(dec!(1), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have funds"); + assert_eq!(BTreeSet::from([1.into()]), + call_treasury_as_non_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have funds"); + + // Adding more funds should succeed + call_fund_treasury_nf(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, BTreeSet::from([2.into(), 3.into()])), + true); + + assert_eq!(dec!(3), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have funds"); + assert_eq!(BTreeSet::from([1.into(), 2.into(), 3.into()]), + call_treasury_as_non_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have funds"); + + // Removing funds should succeed + call_defund_treasury_non_fungible(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + BTreeSet::from([1.into()]), + true); + assert_eq!(dec!(2), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have less funds"); + assert_eq!(BTreeSet::from([2.into(), 3.into()]), + call_treasury_as_non_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have less funds"); + + // Removing as fungible should also work + call_defund_treasury_fungible(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + dec!(1), + true); + assert_eq!(dec!(1), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now have even less funds"); + + // Removing everything should succeed + call_empty_treasury(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + assert_eq!(dec!(0), + call_treasury_as_fungible(&mut test_runner, flexifaucet, &alice).0, + "Treasury should now be empty"); +} + + +/// Tests that we can successfully tap from a fungibles faucet that +/// doesn't have a max per-tap limit. Tests tapping from treasury, +/// from minting, and from both combined. Also tests that we can +/// switch out the minting badge and tapping from mint still works. +#[test] +fn test_tap_fungible_with_no_limits() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (minting_badges_resaddr, minting_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let token_resaddr = + create_mintable_fungible_resource(&mut test_runner, + &alice, + minting_badges_resaddr, + Some(dec!(1_000_000))); + + // This faucet has no restrictions on tap + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, None, None, None); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + // Without treasury or mint, should fail + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1), + false); + + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(1000)), + true); + + // Should take from treasury + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(10), + true); + + if let BalanceChange::Fungible(change) = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(dec!(10), *change, "Alice should be 10 tokens up"); + } else { + panic!("Change should be fungible"); + } + + // Should fail because it's too much and we have no mint yet + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1_000_000), + false); + + // Start the mint + call_set_minting_badge_nf(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + &minting_badge_nfgid, + true); + + // And now we can tap a million easy + + let tokens_in_faucet = test_runner.get_component_resources(flexifaucet) + .get(&token_resaddr).unwrap().clone(); + assert_eq!(dec!(990), tokens_in_faucet, "Treasury should be stocked"); + + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1_000_000), + true); + + if let BalanceChange::Fungible(change) = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(dec!(1_000_000), *change, "Alice should be 1e6 tokens up"); + } else { + panic!("Change should be fungible"); + } + + let tokens_in_faucet = test_runner.get_component_resources(flexifaucet) + .get(&token_resaddr).unwrap().clone(); + assert_eq!(dec!(0), tokens_in_faucet, "Treasury should be empty"); + + + // Now we change the token resource from using nf badges for + // minting to using a different, fungible, badge resource + + let minting_badges_resaddr = + test_runner.create_fungible_resource(dec!(100), 0, alice.account); + + update_minting_role(&mut test_runner, + &alice, + token_resaddr, + minting_badges_resaddr); + + // Since we haven't given the faucet the new minting badge yet, + // tap should fail as the old badge is bad. + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(10), + false); + + // Give a new minting badge to the faucet + call_set_minting_badge_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (minting_badges_resaddr, dec!(1)), + true); + + // And tap should work again + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(25_000), + true); + + if let BalanceChange::Fungible(change) = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(dec!(25_000), *change, "Alice should be 25k tokens up"); + } else { + panic!("Change should be fungible"); + } +} + +/// Tests that we can successfully tap from a fungibles faucet that +/// has a max per-tap limit. Tests tapping from treasury, from +/// minting, and from both combined. +#[test] +fn test_tap_fungible_with_max_limit() { + let mut test_runner = TestRunner::builder().build(); + + let package = test_runner.compile_and_publish(this_package!()); + + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (minting_badges_resaddr, _)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let token_resaddr = + create_mintable_fungible_resource(&mut test_runner, + &alice, + minting_badges_resaddr, + Some(dec!(1_000_000))); + + // This faucet has max 10 tokens per tap + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + Some(dec!(10)), + None, None, None); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(30)), + true); + + // Should work + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1), + true); + + if let BalanceChange::Fungible(change) = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(dec!(1), *change, "Alice should be 1 token up"); + } else { + panic!("Change should be fungible"); + } + + // Should work again + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(10), + true); + + if let BalanceChange::Fungible(change) = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(dec!(10), *change, "Alice should be 10 coins up"); + } else { + panic!("Change should be fungible"); + } + + // Should fail because it's beyond the max-per-tap limit of 10 + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(11), + false); + + // We want there to be 9 tokens left in the faucet + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(10), + true); + let tokens_in_faucet = test_runner.get_component_resources(flexifaucet) + .get(&token_resaddr).unwrap().clone(); + assert_eq!(dec!(9), tokens_in_faucet, "Treasury must have 9 tokens"); + + // And we give the faucet the ability to mint + call_set_minting_badge_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (minting_badges_resaddr, dec!(1)), + true); + + // A malfunctioning faucet might give us the 11 tokens we ask for + // at this point (if it gets confused when it's taking both from + // treasury and from mint) so we test this. + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(11), + false); + + // Now lets empty out the treasury totally + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(10), + true); + + // Again, it's important that the treasury is now empty + let tokens_in_faucet = test_runner.get_component_resources(flexifaucet) + .get(&token_resaddr).unwrap().clone(); + assert_eq!(dec!(0), tokens_in_faucet, "Treasury must be empty"); + + // And we check that a minting-only faucet also enforces the + // max-per-tap limit + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(11), + false); +} + +/// Tests that we can successfully tap from a non-fungibles faucet +/// that doesn't have a max per-tap limit. Tests tapping from +/// treasury, from minting, and from both combined. +#[test] +fn test_tap_non_fungible_with_no_limits() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (minting_badges_resaddr, minting_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + false, + None, + None); + + // This faucet has no restrictions on tap + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, None, None, None); + + mint_ruid_non_fungibles(&mut test_runner, + &alice, + token_resaddr, + &minting_badge_nfgid, + flexifaucet, + 10000); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + // Without treasury or mint, should fail + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1), + false); + + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(100)), + true); + + // Should take from treasury + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(60), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(60, added.len(), "Alice should be 60 tokens up"); + } else { + panic!("Change should be non-fungible"); + } + + // Should fail because it's too much and we have no mint yet + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(100), + false); + + // Start the mint + call_set_minting_badge_nf(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + &minting_badge_nfgid, + true); + + // And now we can tap a hundred easy + + let faucet_vaults = test_runner.get_component_vaults( + flexifaucet, token_resaddr); + let tokens_in_faucet = faucet_vaults.iter() + .map(|node| test_runner.inspect_non_fungible_vault(*node).unwrap().0) + .sum(); + assert_eq!(dec!(40), tokens_in_faucet, "Treasury should be stocked"); + + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(100), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(100, added.len(), "Alice should be 10 tokens up"); + } else { + panic!("Change should be non-fungible"); + } + + let faucet_vaults = test_runner.get_component_vaults( + flexifaucet, token_resaddr); + let tokens_in_faucet = faucet_vaults.iter() + .map(|node| test_runner.inspect_non_fungible_vault(*node).unwrap().0) + .sum(); + assert_eq!(dec!(0), tokens_in_faucet, "Treasury should be empty"); + + // We shouldn't be allowed to try to mint a non-whole number of + // non-fungibles + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!("1.1"), + false); + + + // In a different test we checked that for a fungibles faucet we + // could successfully change it mid-life from having a + // non-fungible minting badge to having a fungible minting + // badge. If it worked there then there is no reason it's not also + // going to work for a non-fungibles faucet so we do not test it + // again here. +} + + +/// Tests that we can successfully tap from a faucet that has a max +/// per-tap limit. +#[test] +fn test_tap_with_max_limit() { + let mut test_runner = TestRunner::builder().build(); + let alice = User::new(&mut test_runner); + assert_tap_with_max_limit(&mut test_runner, alice, None, None, None); +} + +/// Tests that we can successfully tap from a faucet that has a max +/// per-tap limit and fungible taker badges. +#[test] +fn test_tap_with_max_limit_and_fungible_taker_badges() { + let mut test_runner = TestRunner::builder().build(); + let alice = User::new(&mut test_runner); + let taker_badges = test_runner.create_fungible_resource( + dec!(1000), 0, alice.account); + assert_tap_with_max_limit(&mut test_runner, + alice, + Some(taker_badges), + Some(dec!(3)), + Some(BadgeSpec::Fungible(taker_badges, dec!(3)))); +} + +/// Tests that we can successfully tap from a faucet that has a max +/// per-tap limit and non-fungible taker badges. +#[test] +fn test_tap_with_max_limit_and_non_fungible_taker_badges() { + let mut test_runner = TestRunner::builder().build(); + let alice = User::new(&mut test_runner); + let (taker_badges_resaddr, taker_badge_nfgid) = + create_nf_resource_and_badge(&mut test_runner, &alice); + assert_tap_with_max_limit(&mut test_runner, + alice, + Some(taker_badges_resaddr), + None, + Some(BadgeSpec::NonFungible(taker_badge_nfgid))); +} + +/// Common implementation for testing simple max limit faucet. Tests +/// tapping from treasury, from minting, and from both combined. +fn assert_tap_with_max_limit( + test_runner: &mut TestRunner, + alice: User, + taker_badges: Option, + taker_badge_amount: Option, + badge_spec: Option) { + let package = test_runner.compile_and_publish(this_package!()); + + let ((_, owner_badge_nfgid), + (minting_badges_resaddr, minting_badge_nfgid)) = + (create_nf_resource_and_badge(test_runner, &alice), + create_nf_resource_and_badge(test_runner, &alice)); + + let (token_resaddr, _) = + call_new_non_fungible_resource(test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + false, + None, + None); + + // This faucet has max 10 tokens per tap + let (flexifaucet, _) = + call_new(test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + Some(dec!(10)), + None, + taker_badges, + taker_badge_amount); + + mint_ruid_non_fungibles(test_runner, + &alice, + token_resaddr, + &minting_badge_nfgid, + flexifaucet, + 10000); + + call_activate(test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + call_fund_treasury_f(test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(30)), + true); + + if badge_spec.is_some() { + // Tapping without badge when badge is set should fail + call_tap(test_runner, + flexifaucet, + &alice, + None, + dec!(1), + false); + } + + // Should work + let result = + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(1), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(1, added.len(), "Alice should be 1 token up"); + } else { + panic!("Change should be non-fungible"); + } + + // Should work again + let result = + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(10), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(10, added.len(), "Alice should be 10 coins up"); + } else { + panic!("Change should be non-fungible"); + } + + // Should fail because it's beyond the max-per-tap limit of 10 + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(11), + false); + + // Let's clear out all but nine of the tokens in the faucet + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(10), + true); + + // It's important that there's less then 10 left so we check + let faucet_vaults = test_runner.get_component_vaults( + flexifaucet, token_resaddr); + let tokens_in_faucet = faucet_vaults.iter() + .map(|node| test_runner.inspect_non_fungible_vault(*node).unwrap().0) + .sum(); + assert_eq!(dec!(9), tokens_in_faucet, "Treasury must have 9 tokens"); + + // And we give the faucet the ability to mint + call_set_minting_badge_nf(test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + &minting_badge_nfgid, + true); + + // A malfunctioning faucet might give us the 11 tokens we ask for + // at this point (if it gets confused when it's taking both from + // treasury and from mint) so we test this. + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(11), + false); + + // Now lets empty out the treasury totally + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(10), + true); + + // Again, it's important that the treasury is now empty + let faucet_vaults = test_runner.get_component_vaults( + flexifaucet, token_resaddr); + let tokens_in_faucet = faucet_vaults.iter() + .map(|node| test_runner.inspect_non_fungible_vault(*node).unwrap().0) + .sum(); + assert_eq!(dec!(0), tokens_in_faucet, "Treasury must be empty"); + + // And we check that a minting-only faucet also enforces the + // max-per-tap limit + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(11), + false); +} + +/// Tests that when we set non-fungible taker badges, only holders of +/// such can tap. +#[test] +fn test_tap_with_non_fungible_taker_badges() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (taker_badges_resaddr, taker_badge_nfgid), + (minting_badges_resaddr, minting_badge_nfgid), + (_, unrelated_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + false, + None, + None); + + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, None, + Some(taker_badges_resaddr), + None); + + mint_ruid_non_fungibles(&mut test_runner, + &alice, + token_resaddr, + &minting_badge_nfgid, + flexifaucet, + 100); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(30)), + true); + + // Presenting no badge shouldn't work + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1), + false); + + // Presenting an unrelated badge shouldn't work + call_tap(&mut test_runner, + flexifaucet, + &alice, + Some(BadgeSpec::NonFungible(unrelated_badge_nfgid)), + dec!(1), + false); + + // But using an actual user badge should work + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + Some(BadgeSpec::NonFungible(taker_badge_nfgid)), + dec!(1), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(1, added.len(), "Alice should be 1 token up"); + } else { + panic!("Change should be non-fungible"); + } +} + + +/// Tests that when we set fungible taker badges, only holders of +/// sufficient such can tap. +#[test] +fn test_tap_with_fungible_taker_badges() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (_, unrelated_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let token_resaddr = test_runner.create_fungible_resource( + dec!(1000), 0, alice.account); + + let taker_badges_resaddr = test_runner.create_fungible_resource( + dec!(1000), 0, alice.account); + let unrelated_fungible_badge_resaddr = test_runner.create_fungible_resource( + dec!(1000), 0, alice.account); + + // This faucet requires you to present at least 3 badge tokens to + // tap. + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, None, + Some(taker_badges_resaddr), + Some(dec!(3))); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(30)), + true); + + // Presenting no badge shouldn't work + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1), + false); + + // Presenting an unrelated fungible badge shouldn't work + call_tap(&mut test_runner, + flexifaucet, + &alice, + Some(BadgeSpec::Fungible(unrelated_fungible_badge_resaddr, dec!(3))), + dec!(1), + false); + + // Presenting an unrelated non-fungible badge shouldn't work + call_tap(&mut test_runner, + flexifaucet, + &alice, + Some(BadgeSpec::NonFungible(unrelated_badge_nfgid)), + dec!(1), + false); + + // Presenting too few of the correct badge shouldn't work + call_tap(&mut test_runner, + flexifaucet, + &alice, + Some(BadgeSpec::Fungible(taker_badges_resaddr, dec!("2.9"))), + dec!(1), + false); + + // But using the correct number of badges should work + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + Some(BadgeSpec::Fungible(taker_badges_resaddr, dec!(3))), + dec!(1), + true); + + if let BalanceChange::Fungible(change) = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(dec!(1), *change, "Alice should be 1 token up"); + } else { + panic!("Change should be fungible"); + } + + // using too many badges should also work + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + Some(BadgeSpec::Fungible(taker_badges_resaddr, dec!(30))), + dec!(1), + true); + + if let BalanceChange::Fungible(change) = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(dec!(1), *change, "Alice should be 1 token up"); + } else { + panic!("Change should be fungible"); + } +} + +/// Tests that when we set cooldown without taker badges we are +/// prevented from tapping while waiting for cooldown. +#[test] +fn test_tap_with_cooldown() { + let mut test_runner = TestRunner::builder() + .with_custom_genesis(CustomGenesis::default( + Epoch::of(1), + CustomGenesis::default_consensus_manager_config(), + )) + .without_trace() + .build(); + let alice = User::new(&mut test_runner); + + assert_tap_with_cooldown(&mut test_runner, + alice, + None, None, None); +} + +/// Tests that when we set cooldown with fungible taker badges we are +/// prevented from tapping while waiting for cooldown. +#[test] +fn test_tap_with_fungible_taker_badges_and_cooldown() { + let mut test_runner = TestRunner::builder() + .with_custom_genesis(CustomGenesis::default( + Epoch::of(1), + CustomGenesis::default_consensus_manager_config(), + )) + .without_trace() + .build(); + let alice = User::new(&mut test_runner); + let taker_badges_resaddr = test_runner.create_fungible_resource( + dec!(1000), 0, alice.account); + + assert_tap_with_cooldown(&mut test_runner, + alice, + Some(taker_badges_resaddr), + Some(dec!(3)), + Some(BadgeSpec::Fungible(taker_badges_resaddr, dec!(3)))); +} + +/// Tests that when we set cooldown with non-fungible taker badges we +/// are prevented from tapping while waiting for cooldown. +#[test] +fn test_tap_with_non_fungible_taker_badges_and_cooldown() { + let mut test_runner = TestRunner::builder() + .with_custom_genesis(CustomGenesis::default( + Epoch::of(1), + CustomGenesis::default_consensus_manager_config(), + )) + .without_trace() + .build(); + + let alice = User::new(&mut test_runner); + let (taker_badges_resaddr, taker_badge_nfgid) = + create_nf_resource_and_badge(&mut test_runner, &alice); + + assert_tap_with_cooldown(&mut test_runner, + alice, + Some(taker_badges_resaddr), + None, + Some(BadgeSpec::NonFungible(taker_badge_nfgid))); +} + +/// Implements test of tapping with cooldown without max limit. +fn assert_tap_with_cooldown( + test_runner: &mut TestRunner, + alice: User, + taker_badges: Option, + taker_badge_amount: Option, + badge_spec: Option) +{ + let package = test_runner.compile_and_publish(this_package!()); + let (_, owner_badge_nfgid) = + create_nf_resource_and_badge(test_runner, &alice); + let token_resaddr = test_runner.create_fungible_resource( + dec!(1000), 0, alice.account); + + // This faucet imposes a 600 second (10 minute) cooldown between + // taps + let (flexifaucet, _) = + call_new(test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, Some(600), + taker_badges, + taker_badge_amount); + + call_activate(test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + call_fund_treasury_f(test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + (token_resaddr, dec!(30)), + true); + + if badge_spec.is_some() { + // Shouldn't be able to take without a badge when badges have + // been set + call_tap(test_runner, + flexifaucet, + &alice, + None, + dec!(1), + false); + } + + // First tap should work + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(1), + true); + + // But then we're denied + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(1), + false); + + // We advance time not quite to the next period + set_test_runner_clock(test_runner, 599); + + // And get denied again + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(1), + false); + + // Time advances to the next period + set_test_runner_clock(test_runner, 600); + + // And we can tap again + call_tap(test_runner, + flexifaucet, + &alice, + badge_spec.clone(), + dec!(1), + true); +} + +/// Tests that when we set taker badges, cooldown and a max limit, +/// taps are limited both in total tap per period. +#[test] +fn test_tap_with_non_fungible_taker_badges_and_limits_and_cooldown() { + let mut test_runner = TestRunner::builder() + .with_custom_genesis(CustomGenesis::default( + Epoch::of(1), + CustomGenesis::default_consensus_manager_config(), + )) + .without_trace() + .build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + + let ((_, owner_badge_nfgid), + (taker_badges_resaddr, taker_badge1_nfgid), + (minting_badges_resaddr, minting_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let badge1_spec = Some(BadgeSpec::NonFungible(taker_badge1_nfgid)); + let badge2_spec = Some(BadgeSpec::NonFungible(NonFungibleGlobalId::new( + taker_badges_resaddr, 2.into()))); + + let (token_resaddr, _) = + call_new_non_fungible_resource(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + minting_badges_resaddr, + false, + None, + None); + + // This faucet gives max 3 tokens per 600 seconds per user. + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + Some(dec!(3)), + Some(600), + Some(taker_badges_resaddr), + None); + + call_set_minting_badge_nf(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + &minting_badge_nfgid, + true); + + call_activate(&mut test_runner, + flexifaucet, + &alice, + &owner_badge_nfgid, + true); + + // Presenting no badge shouldn't work + call_tap(&mut test_runner, + flexifaucet, + &alice, + None, + dec!(1), + false); + + // badge1 does some tapping + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(1), + true); + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(1), + true); + + // Advance time a bit just because + set_test_runner_clock(&mut test_runner, 240); + + // And this fills our quota for this period + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(1), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(1, added.len(), "Alice should be 1 token up"); + } else { + panic!("Change should be non-fungible"); + } + + // Another should not be allowed + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(1), + false); + + // But badge2 should still be allowed because it's not been used yet + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge2_spec.clone(), + dec!(3), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(3, added.len(), "Alice should be 3 tokens up"); + } else { + panic!("Change should be non-fungible"); + } + + // Again, another should not be allowed + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge2_spec.clone(), + dec!(1), + false); + + // Advance time to just before the end of badge1's period + set_test_runner_clock(&mut test_runner, 599); + + // And this should still fail + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(1), + false); + + // Enter next period for badge1 + set_test_runner_clock(&mut test_runner, 600); + + // badge2 still can't tap + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge2_spec.clone(), + dec!(1), + false); + + // But badge1 can tap again + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(3), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(3, added.len(), "Alice should be 3 tokens up"); + } else { + panic!("Change should be non-fungible"); + } + + // And that's all 3 gone for badge1 so this now fails + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(1), + false); + + // Enter next period for badge2 + set_test_runner_clock(&mut test_runner, 840); + + // Can't tap 4 for badge1 + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge1_spec.clone(), + dec!(4), + false); + + // But badge2 can now tap 3 + let result = + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge2_spec.clone(), + dec!(3), + true); + + if let BalanceChange::NonFungible{ added, .. } = result.balance_changes() + .get(&GlobalAddress::from(alice.account)).unwrap() + .get(&token_resaddr).unwrap() { + assert_eq!(3, added.len(), "Alice should be 3 tokens up"); + } else { + panic!("Change should be non-fungible"); + } + + // Can't tap another for badge2 + call_tap(&mut test_runner, + flexifaucet, + &alice, + badge2_spec.clone(), + dec!(1), + false); +} + +/// Tests that we can change the `funder` role on a faucet. +#[test] +fn test_change_fund_treasury_access_control() { + let mut test_runner = TestRunner::builder().build(); + let package = test_runner.compile_and_publish(this_package!()); + let alice = User::new(&mut test_runner); + let execution_proofs = vec![NonFungibleGlobalId::from_public_key(&alice.pubkey)]; + + let ((owner_badges_resaddr, owner_badge_nfgid), + (_, unrelated_badge_nfgid)) = + (create_nf_resource_and_badge(&mut test_runner, &alice), + create_nf_resource_and_badge(&mut test_runner, &alice)); + + let token_resaddr = + test_runner.create_fungible_resource(dec!(1000), 18, alice.account); + + + let (flexifaucet, _) = + call_new(&mut test_runner, + package, + &alice, + &owner_badge_nfgid, + token_resaddr, + None, None, None, None); + + // Unrelated badges shouldn't be able to fund by default + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &unrelated_badge_nfgid, + (token_resaddr, dec!(30)), + false); + + // Change the funder role to allow all + let manifest = ManifestBuilder::new() + .create_proof_from_account_of_non_fungibles( + alice.account, + owner_badges_resaddr, + &BTreeSet::from([owner_badge_nfgid.local_id().clone()])) + .update_role(flexifaucet, + ObjectModuleId::Main, + RoleKey::new("funder"), + rule!(allow_all)) + .build(); + test_runner.execute_manifest_ignoring_fee(manifest, execution_proofs.clone()) + .expect_commit(true); + + // Now anyone can fund us + call_fund_treasury_f(&mut test_runner, + flexifaucet, + &alice, + &unrelated_badge_nfgid, + (token_resaddr, dec!(30)), + true); +}