diff --git a/basic/utility_token_service/Cargo.toml b/basic/utility_token_service/Cargo.toml new file mode 100644 index 00000000..d78e096c --- /dev/null +++ b/basic/utility_token_service/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "utility-token-service" +version = "0.1.0" +edition = "2018" + +[dependencies] +sbor = { path = "../../../sbor" } +scrypto = { path = "../../../scrypto" } + +[dev-dependencies] +radix-engine = { path = "../../../radix-engine" } + +[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. + +[lib] +crate-type = ["cdylib", "lib"] +name = "out" diff --git a/basic/utility_token_service/revup.json b/basic/utility_token_service/revup.json new file mode 100644 index 00000000..16def959 --- /dev/null +++ b/basic/utility_token_service/revup.json @@ -0,0 +1,80 @@ +{ + "commands": [ + { + "cmd": "reset", + "args": [], + "envs": [] + }, + { + "cmd": "new-account", + "args": [], + "envs": [ + "account", + "pubkey" + ] + }, + { + "cmd": "new-account", + "args": [], + "envs": [ + "account2", + "pubkey2" + ] + }, + { + "cmd": "new-token-fixed", + "args": [ + "10000", + "--name", + "emunie", + "--symbol", + "EMT" + ], + "envs": [ + "tokenEMT" + ] + }, + { + "cmd": "new-token-fixed", + "args": [ + "10000", + "--name", + "gmunie", + "--symbol", + "GMT" + ], + "envs": [ + "tokenGMT" + ] + }, + { + "cmd": "publish", + "args": [ + "." + ], + "envs": [ + "package" + ] + }, + { + "cmd": "call-function", + "args": [ + "$package", + "UtilityTokenFactory", + "new", + "Rock", + "RadQuilders", + "RGT", + "token of merit for RadGuild Members", + "5", + "50", + "25" + ], + "envs": [ + "badgeUTF", + "tokenRGT", + "utf_component" + ] + } + ] +} diff --git a/basic/utility_token_service/src/lib.rs b/basic/utility_token_service/src/lib.rs new file mode 100644 index 00000000..e18223f4 --- /dev/null +++ b/basic/utility_token_service/src/lib.rs @@ -0,0 +1,4 @@ +// Here we declare all of the modules that should be included into this package. + +mod util_token_fac; +mod service_stub; diff --git a/basic/utility_token_service/src/service_stub.rs b/basic/utility_token_service/src/service_stub.rs new file mode 100644 index 00000000..80f3535a --- /dev/null +++ b/basic/utility_token_service/src/service_stub.rs @@ -0,0 +1,100 @@ +use scrypto::prelude::*; + +/* +This blueprint is an example of wrapping another component - UtilityTokenFactory. + +No actual service is provided but two stub services (regular and premium) are demoed. +*/ + +use crate::util_token_fac::UtilityTokenFactory; + +blueprint! { + struct ServiceStub { + utf: UtilityTokenFactory, + used_tokens: Vault, + simple_service_count: u32, + premium_service_count: u32 + } + + impl ServiceStub { + + // Create a UtilityTokenFactory using the simulator and pass it into this constructor. + // See the README.md file for an example of how to do this. + // + pub fn new( comp:Address ) -> Component { + let my_utf: UtilityTokenFactory = UtilityTokenFactory::from(comp); + let my_utf_address = my_utf.address(); + Self { + utf: my_utf, + used_tokens: Vault::new(ResourceDef::from(my_utf_address)), + simple_service_count: 0, + premium_service_count: 0 + } + .instantiate() + } + + // Send the collected UT coins out to be burned. + // Not required to do this often but letting the vault get too big is a mild safety concern. + // + fn maybe_redeem(&mut self) { + if self.used_tokens.amount() > 100.into() { + self.utf.redeem( self.used_tokens.take_all()) + }; + } + + // Show how many services have been delivered. + // + pub fn show(&self) { // Ideally this function would be secured by a badge here. + info!("Simple Services performed: {}", self.simple_service_count); + info!("Premium Services performed: {}", self.premium_service_count); + // Ideally we could safely call show() on the UTF too. + } + + // Now we define our services + pub fn simple_service(&mut self, payment: Bucket) -> Bucket { + scrypto_assert!(payment.resource_def().address() == self.utf.address(), "Simple service requires 1 util token"); + scrypto_assert!(payment.amount() >= 1.into(), "Simple service requires 1 util token"); + scrypto_assert!(self.utf.address() == self.used_tokens.resource_def().address(), "Mismatch in Vault setup."); + self.used_tokens.put(payment.take(1)); + info!("Performing Simple Service now."); + self.simple_service_count += 1; + self.maybe_redeem(); + payment // return the user's change (if any) + } + + pub fn premium_service(&mut self, payment: Bucket) -> Bucket { + scrypto_assert!(payment.resource_def().address() == self.utf.address(), "Premium service requires 3 util tokens"); + scrypto_assert!(payment.amount() >= 3.into(), "Premium service requires 3 util tokens"); + self.used_tokens.put(payment.take(3)); + info!("Performing Premium Service now."); + self.premium_service_count += 1; + self.maybe_redeem(); + payment // return the user's change (if any) + } + + + // The original constructur shown below used these hard-coded values. + // const MAX_BUY: u32 = 100; // Limit users to purchasing 100 tokens at a time. + // const UTIL_TOKEN_NAME: &str = "My Service"; + // const UTIL_TOKEN_SYMBOL: &str = "STUB"; + // const UTIL_TOKEN_DESCRIPTION: &str = "Do nothing service"; + + /* This was the original constructor that was used to bootstrap this bluerint. + // It worked fine but it is not particularly flexible and was deemd to be inferior. + // Left here for the sake of posterity. + // + pub fn new() -> Component { + // The new func takes: name, symbol, description, price (in XRD), mint_size, max_buy + let my_utf: UtilityTokenFactory = UtilityTokenFactory::new(UTIL_TOKEN_NAME.to_string(), UTIL_TOKEN_SYMBOL.to_string(), UTIL_TOKEN_DESCRIPTION.to_string(), 5, 500, MAX_BUY).into(); + let my_utf_address = my_utf.address(); + Self { + utf: my_utf, + used_tokens: Vault::new(ResourceDef::from(my_utf_address)), + simple_service_count: 0, + premium_service_count: 0 + } + .instantiate() + } + */ + } +} diff --git a/basic/utility_token_service/src/util_token_fac.rs b/basic/utility_token_service/src/util_token_fac.rs new file mode 100644 index 00000000..366ac383 --- /dev/null +++ b/basic/utility_token_service/src/util_token_fac.rs @@ -0,0 +1,117 @@ +use scrypto::prelude::*; + +/* +A blueprint for creating and managing a utility token. +*/ + +blueprint! { + struct UtilityTokenFactory { + ut_minter_vault: Vault, + ut_minter_badge: ResourceDef, + available_ut: Vault, // The available supply of utility tokens which are used to pay for some services. + ut_token_price: Decimal, // How many XRD the UT Tokens cost. (Fractional amounts are not recommended at this time.) + collected_xrd: Vault, // proceeds from selling UT tokens + ut_max_buy : u32, // Maximum number of UT tokens that can be purchased at a time. + ut_mint_size: u32, // How many UT tokens to mint in each batch. + total_claimed: Decimal, // Total XRD claimed during the contract lifetime. + total_minted: u32, // How many utility tokens have been minted. + total_redeemed: Decimal, // How many utility tokens have been redeemed and burned. + } + + impl UtilityTokenFactory { + + pub fn new( my_id: String, ut_name: String, ut_symbol: String, ut_description: String, price: u32, mint_size: u32, max_buy: u32 ) -> (Component, Bucket) { + let ut_minter_bucket = ResourceBuilder::new() + .metadata("name", my_id) + .new_badge_fixed(2); + let ut_minter_resource_def = ut_minter_bucket.resource_def(); + let ut_minter_return_bucket: Bucket = ut_minter_bucket.take(1); // Return this badge to the caller + + let ut_resource_def = ResourceBuilder::new() + .metadata("name", ut_name) + .metadata("symbol", ut_symbol) + .metadata("description", ut_description) + .new_token_mutable(ut_minter_bucket.resource_address()); + let ut_tokens = ut_resource_def.mint(mint_size, ut_minter_bucket.borrow()); + + scrypto_assert!(mint_size > 0, "You must specify a non-zero number for the mint_size."); + scrypto_assert!(max_buy <= mint_size, "The single purchase max buy size should be less than or equal to the mint size."); + let component = Self { + ut_minter_vault: Vault::with_bucket(ut_minter_bucket), + ut_minter_badge: ut_minter_resource_def, + + available_ut: Vault::with_bucket(ut_tokens), + ut_token_price: price.into(), + collected_xrd: Vault::new(RADIX_TOKEN), + + ut_max_buy: max_buy, + ut_mint_size: mint_size, + + total_claimed: 0.into(), + total_minted: mint_size, + total_redeemed: 0.into() + } + .instantiate(); + (component, ut_minter_return_bucket) + } + + // Convenience function returns the address of the Utility Token + // + pub fn address(&self) -> Address { + self.available_ut.resource_def().address() + } + + // Purchase UT tokens + // + pub fn purchase(&mut self, number: u32, payment: Bucket) -> (Bucket, Bucket) { + scrypto_assert!(payment.resource_def() == RADIX_TOKEN.into(), "You must purchase the utility tokens with Radix (XRD)."); + let ut_bucket = Bucket::new(self.address()); + let mut num = number; + if num > self.ut_max_buy { + num = self.ut_max_buy; // 1,000 is the max allowable purchase for now. + info!("A max of {} tokens can be purcahsed at a time.", self.ut_max_buy); + } + if payment.amount() < self.ut_token_price * num { + info!("Insufficient funds. Required payment for {} UT tokens is {} XRD.", num, self.ut_token_price * num); + } else { + info!("Thank you!"); + if self.available_ut.amount() < num.into() { // if they are needed, mint more UT tokens + let new_tokens = self.ut_minter_vault.authorize(|badge| { + self.available_ut.resource_def().mint(self.ut_mint_size, badge) + }); + self.available_ut.put(new_tokens); + self.total_minted += self.ut_mint_size; + } + self.collected_xrd.put(payment.take(self.ut_token_price * num)); + ut_bucket.put(self.available_ut.take(num)); + } + (payment, ut_bucket) + } + + #[auth(ut_minter_badge)] + pub fn show_bank(&self) { + let metadata = self.available_ut.resource_def().metadata(); + info!("Available {}: {}", metadata["symbol"], self.available_ut.amount()); + info!("Claimable XRD: {}", self.collected_xrd.amount()); + info!("Total XRD Claimed: {}", self.total_claimed); + info!("Total {} Minted: {}", metadata["symbol"], self.total_minted); + info!("Total {} Redeemed: {}", metadata["symbol"], self.total_redeemed); + } + + #[auth(ut_minter_badge)] + pub fn claim(&mut self) -> Bucket { + self.total_claimed += self.collected_xrd.amount(); + self.collected_xrd.take_all() + } + + pub fn redeem(&mut self, used_tokens: Bucket) { + if used_tokens.amount() > 0.into() { + scrypto_assert!(used_tokens.resource_def() == self.available_ut.resource_def(), "You can only redeem the expected utility tokens."); + self.total_redeemed += used_tokens.amount(); + self.ut_minter_vault.authorize(|badge| { + used_tokens.burn(badge); + }) + } + } + } +} diff --git a/basic/utility_token_service/tests/lib.rs b/basic/utility_token_service/tests/lib.rs new file mode 100644 index 00000000..8a75f6c6 --- /dev/null +++ b/basic/utility_token_service/tests/lib.rs @@ -0,0 +1,34 @@ +use radix_engine::ledger::*; +use radix_engine::transaction::*; +use scrypto::prelude::*; + +#[test] +fn test_hello() { + // Set up environment. + let mut ledger = InMemoryLedger::with_bootstrap(); + let mut executor = TransactionExecutor::new(&mut ledger, 0, 0); + let key = executor.new_public_key(); + let account = executor.new_account(key); + let package = executor.publish_package(include_code!()); + + // Test the `new` function. + let transaction1 = TransactionBuilder::new(&executor) + .call_function(package, "Hello", "new", vec![], None) + .build(vec![key]) + .unwrap(); + let receipt1 = executor.run(transaction1, false).unwrap(); + println!("{:?}\n", receipt1); + assert!(receipt1.success); + + // Test the `free_token` method. + let component = receipt1.component(0).unwrap(); + let transaction2 = TransactionBuilder::new(&executor) + .call_method(component, "free_token", vec![], Some(account)) + .drop_all_bucket_refs() + .deposit_all_buckets(account) + .build(vec![key]) + .unwrap(); + let receipt2 = executor.run(transaction2, false).unwrap(); + println!("{:?}\n", receipt2); + assert!(receipt2.success); +} diff --git a/toys/rocks-candy-shop/Cargo.toml b/toys/rocks-candy-shop/Cargo.toml new file mode 100644 index 00000000..6de65558 --- /dev/null +++ b/toys/rocks-candy-shop/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rocks-candy-shop" +version = "0.1.0" +edition = "2018" + +[dependencies] +sbor = { path = "../../../sbor" } +scrypto = { path = "../../../scrypto" } + +[dev-dependencies] +radix-engine = { path = "../../../radix-engine" } + +[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. + +[lib] +crate-type = ["cdylib", "lib"] +name = "out" diff --git a/toys/rocks-candy-shop/revup_initial_supply.json b/toys/rocks-candy-shop/revup_initial_supply.json new file mode 100644 index 00000000..9729b6e9 --- /dev/null +++ b/toys/rocks-candy-shop/revup_initial_supply.json @@ -0,0 +1,79 @@ +{ + "commands": [ + { + "cmd": "reset", + "args": [], + "envs": [] + }, + { + "cmd": "new-account", + "args": [], + "envs": [ + "account", + "pubkey" + ] + }, + { + "cmd": "new-account", + "args": [], + "envs": [ + "account2", + "pubkey2" + ] + }, + { + "cmd": "new-token-fixed", + "args": [ + "10000", + "--name", + "emunie", + "--symbol", + "EMT" + ], + "envs": [ + "tokenEMT" + ] + }, + { + "cmd": "new-token-fixed", + "args": [ + "10000", + "--name", + "gmunie", + "--symbol", + "GMT" + ], + "envs": [ + "tokenGMT" + ] + }, + { + "cmd": "publish", + "args": [ + "." + ], + "envs": [ + "package" + ] + }, + { + "cmd": "call-function", + "args": [ + "$package", + "CandyShop", + "initial_supply", + "500" + ], + "envs": [ + "tokenGUM", + "tokenJAWB", + "tokenLPOP", + "tokenCANE", + "tokenJELLY", + "tokenMINT", + "tokenBEAR", + "component" + ] + } + ] +} \ No newline at end of file diff --git a/toys/rocks-candy-shop/revup_new.json b/toys/rocks-candy-shop/revup_new.json new file mode 100644 index 00000000..747c6a28 --- /dev/null +++ b/toys/rocks-candy-shop/revup_new.json @@ -0,0 +1,71 @@ +{ + "commands": [ + { + "cmd": "reset", + "args": [], + "envs": [] + }, + { + "cmd": "new-account", + "args": [], + "envs": [ + "account", + "pubkey" + ] + }, + { + "cmd": "new-account", + "args": [], + "envs": [ + "account2", + "pubkey2" + ] + }, + { + "cmd": "new-token-fixed", + "args": [ + "10000", + "--name", + "emunie", + "--symbol", + "EMT" + ], + "envs": [ + "tokenEMT" + ] + }, + { + "cmd": "new-token-fixed", + "args": [ + "10000", + "--name", + "gmunie", + "--symbol", + "GMT" + ], + "envs": [ + "tokenGMT" + ] + }, + { + "cmd": "publish", + "args": [ + "." + ], + "envs": [ + "package" + ] + }, + { + "cmd": "call-function", + "args": [ + "$package", + "CandyShop", + "new" + ], + "envs": [ + "component" + ] + } + ] +} \ No newline at end of file diff --git a/toys/rocks-candy-shop/src/lib.rs b/toys/rocks-candy-shop/src/lib.rs new file mode 100644 index 00000000..4bdd4576 --- /dev/null +++ b/toys/rocks-candy-shop/src/lib.rs @@ -0,0 +1,119 @@ +use scrypto::prelude::*; + +/* This a non-trivial example that shows off a blueprint that operates with + with an adjustable number of vaults. My main discover was that you can + return Vec and the resim methd call can handle it and update + the account correctly. + + If someone wants to do so there are many possible extensions: + - add purchase method(s) via XRD + - have the purchase method(s) also return change + - add badges to control who can get the collected XRD + - allow for different prices for each type of candy + - allow candy restocking by a badged account (hint: mutable supply) + - tag the candy_vaults with the rri instead of the token symbol + - many more... +*/ + +blueprint! { + struct CandyShop { + // The different kinds of candies are kept here with each in a tuple + // with a unique tag string that doubles as the candies' token symbol. + candy_vaults: Vec<(String,Vault)> + } + + impl CandyShop { + + pub fn new() -> Component { + // This constructor sets up an empty candy shop + let tagged_vaults: Vec<(String,Vault)> = Vec::new(); + Self { + candy_vaults: tagged_vaults + } + .instantiate() + } + + pub fn initial_supply( supply_size: u32 ) -> Component { + // This constructor sets up a variety of candies with the specified amount. + let mut tagged_vaults: Vec<(String,Vault)> = Vec::new(); + // Now define the meta data for each type of candy. + let mut metas = vec![("Gumball", "GUM", "The best gumball in the world.")]; + metas.push(("Jawbreaker", "JAWB", "Jawbreakers teach patience.")); + metas.push(("Lollipop","LPOP","You can't lick Lollipops!")); + metas.push(("Candy Cane", "CANE", "Striped candy rules!")); + metas.push(("Jelly Bean", "JELLY", "Jelly Beans are best!")); + metas.push(("Mint Candy", "MINT", "Mints are wonderful!")); + metas.push(("Gummy Bear", "BEAR", "Gummy Bears rules!")); + // Create a supply + for tup in metas { + let bucket = ResourceBuilder::new() + .metadata("name", tup.0.to_string()) + .metadata("symbol", tup.1.to_string()) + .metadata("description", tup.2.to_string()) + .new_token_fixed(supply_size); + tagged_vaults.push((tup.1.to_string(), Vault::with_bucket(bucket))); + } + Self { + candy_vaults: tagged_vaults + } + .instantiate() + } + + fn take_from_vault(&self, symbol: String, quantity: Decimal) -> Bucket { + // private function returns a bucket with the specified number and type of candy (or an empty bucket) + for c in &self.candy_vaults[..] { + if c.0 == symbol { + let v = &c.1; + if v.amount() >= quantity { + return v.take(quantity) + } else { + break; + } + } + } + let empty_bucket: Bucket = Bucket::new(RADIX_TOKEN); // canonical way to make an empty_bucket + return empty_bucket + } + + pub fn free_gum(&self) -> Bucket { + // Return a gumball if we have at least one available. + // If there is no GUM vault or the GUM vaut is empty, this method will fail. + self.take_from_vault("GUM".to_string(), 1.into()) + } + + pub fn free_samples(&mut self) -> Vec { + let mut buckets = Vec::new(); + for c in &self.candy_vaults[..] { + if c.1.amount() > 0.into() { + buckets.push(c.1.take(1)); + } + } + return buckets + } + + fn contains(&self, symbol: &str) -> bool { + // return True if the symbol is found in the candy_vaults based on the token symbol + let mut found: bool = false; + let symbol_string = symbol.to_string(); + for c in &self.candy_vaults[..] { + if c.0 == symbol_string { + found = true; + break; + } + } + return found + } + + pub fn add_candy(&mut self, name: String, symbol: String, description: String, supply_size: Decimal) { + scrypto_assert!(supply_size >= 1.into(), "Not enough initial candy"); + scrypto_assert!(self.contains(&symbol) == false, "That type of candy is already available."); + // Add a new kind of candy to the CandyShop + let bucket = ResourceBuilder::new() + .metadata("name", name) + .metadata("symbol", symbol.to_string()) + .metadata("description", description) + .new_token_fixed(supply_size); + self.candy_vaults.push((symbol, Vault::with_bucket(bucket))); + } + } +} diff --git a/toys/rocks-candy-shop/tests/lib.rs b/toys/rocks-candy-shop/tests/lib.rs new file mode 100644 index 00000000..8a75f6c6 --- /dev/null +++ b/toys/rocks-candy-shop/tests/lib.rs @@ -0,0 +1,34 @@ +use radix_engine::ledger::*; +use radix_engine::transaction::*; +use scrypto::prelude::*; + +#[test] +fn test_hello() { + // Set up environment. + let mut ledger = InMemoryLedger::with_bootstrap(); + let mut executor = TransactionExecutor::new(&mut ledger, 0, 0); + let key = executor.new_public_key(); + let account = executor.new_account(key); + let package = executor.publish_package(include_code!()); + + // Test the `new` function. + let transaction1 = TransactionBuilder::new(&executor) + .call_function(package, "Hello", "new", vec![], None) + .build(vec![key]) + .unwrap(); + let receipt1 = executor.run(transaction1, false).unwrap(); + println!("{:?}\n", receipt1); + assert!(receipt1.success); + + // Test the `free_token` method. + let component = receipt1.component(0).unwrap(); + let transaction2 = TransactionBuilder::new(&executor) + .call_method(component, "free_token", vec![], Some(account)) + .drop_all_bucket_refs() + .deposit_all_buckets(account) + .build(vec![key]) + .unwrap(); + let receipt2 = executor.run(transaction2, false).unwrap(); + println!("{:?}\n", receipt2); + assert!(receipt2.success); +}