From 421ca77dc4419c6e19201fa9969e90640ecf01dd Mon Sep 17 00:00:00 2001 From: Tony Lee Date: Wed, 27 Nov 2024 15:31:56 -0500 Subject: [PATCH] format coupons and discounts (#215) --- packages/coupons/sources/constants.move | 26 +- packages/coupons/sources/coupon.move | 91 +-- packages/coupons/sources/coupon_house.move | 493 ++++++------ packages/coupons/sources/data.move | 77 +- packages/coupons/sources/range.move | 55 +- packages/coupons/sources/rules.move | 335 +++++---- .../coupons/tests/authorization_tests.move | 108 ++- packages/coupons/tests/coupons_tests.move | 704 ++++++++++++------ packages/coupons/tests/setup.move | 493 ++++++------ packages/discounts/sources/discounts.move | 298 ++++---- packages/discounts/sources/free_claims.move | 378 +++++----- packages/discounts/sources/house.move | 177 +++-- packages/discounts/tests/discount_tests.move | 423 ++++++----- .../discounts/tests/free_claims_test.move | 410 +++++----- 14 files changed, 2266 insertions(+), 1802 deletions(-) diff --git a/packages/coupons/sources/constants.move b/packages/coupons/sources/constants.move index c765788e..ca44ddec 100644 --- a/packages/coupons/sources/constants.move +++ b/packages/coupons/sources/constants.move @@ -2,19 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 // Some constants used in coupons. -module coupons::constants { - /// discount types - /// Percentage discount (0,100] - const PERCENTAGE_DISCOUNT: u8 = 0; - /// Fixed MIST discount (e.g. -5 SUI) - const FIXED_PRICE_DISCOUNT: u8 = 1; +module coupons::constants; - /// A getter for the percentage discount type. - public fun percentage_discount_type(): u8 { PERCENTAGE_DISCOUNT } +/// discount types +/// Percentage discount (0,100] +const PERCENTAGE_DISCOUNT: u8 = 0; +/// Fixed MIST discount (e.g. -5 SUI) +const FIXED_PRICE_DISCOUNT: u8 = 1; - /// A getter for the fixed price discount type. - public fun fixed_price_discount_type(): u8 { FIXED_PRICE_DISCOUNT } +/// A getter for the percentage discount type. +public fun percentage_discount_type(): u8 { PERCENTAGE_DISCOUNT } - /// A vector with all the discount rule types. - public fun discount_rule_types(): vector { vector[PERCENTAGE_DISCOUNT, FIXED_PRICE_DISCOUNT] } +/// A getter for the fixed price discount type. +public fun fixed_price_discount_type(): u8 { FIXED_PRICE_DISCOUNT } + +/// A vector with all the discount rule types. +public fun discount_rule_types(): vector { + vector[PERCENTAGE_DISCOUNT, FIXED_PRICE_DISCOUNT] } diff --git a/packages/coupons/sources/coupon.move b/packages/coupons/sources/coupon.move index 8f8cd6f7..cfb12835 100644 --- a/packages/coupons/sources/coupon.move +++ b/packages/coupons/sources/coupon.move @@ -1,52 +1,55 @@ -module coupons::coupon { - use coupons::{ - rules::{Self, CouponRules}, - constants - }; +module coupons::coupon; - /// A Coupon has a type, a value and a ruleset. - /// - `Rules` are defined on the module `rules`, and covers a variety of everything we needed for the service. - /// - `kind` is a u8 constant, defined on `constants` which makes a coupon fixed price or discount percentage - /// - `value` is a u64 constant, which can be in the range of (0,100] for discount percentage, or any value > 0 for fixed price. - public struct Coupon has copy, store, drop { - kind: u8, // 0 -> Percentage Discount | 1 -> Fixed Discount - amount: u64, // if type == 0, we need it to be between 0, 100. We only allow int stlye (not 0.5% discount). - rules: CouponRules, // A list of base Rules for the coupon. - } +use coupons::constants; +use coupons::rules::{Self, CouponRules}; - /// An internal function to create a coupon object. - public(package) fun new( - kind: u8, - amount: u64, - rules: CouponRules, - _ctx: &mut TxContext - ): Coupon { - rules::assert_is_valid_amount(kind, amount); - rules::assert_is_valid_discount_type(kind); - Coupon { - kind, amount, rules - } - } +/// A Coupon has a type, a value and a ruleset. +/// - `Rules` are defined on the module `rules`, and covers a variety of +/// everything we needed for the service. +/// - `kind` is a u8 constant, defined on `constants` which makes a coupon fixed +/// price or discount percentage +/// - `value` is a u64 constant, which can be in the range of (0,100] for +/// discount percentage, or any value > 0 for fixed price. +public struct Coupon has copy, store, drop { + kind: u8, // 0 -> Percentage Discount | 1 -> Fixed Discount + amount: u64, // if type == 0, we need it to be between 0, 100. We only allow int stlye (not 0.5% discount). + rules: CouponRules, // A list of base Rules for the coupon. +} - public(package) fun rules(coupon: &Coupon): &CouponRules { - &coupon.rules +/// An internal function to create a coupon object. +public(package) fun new( + kind: u8, + amount: u64, + rules: CouponRules, + _ctx: &mut TxContext, +): Coupon { + rules::assert_is_valid_amount(kind, amount); + rules::assert_is_valid_discount_type(kind); + Coupon { + kind, + amount, + rules, } +} - public(package) fun rules_mut(coupon: &mut Coupon): &mut CouponRules { - &mut coupon.rules - } +public(package) fun rules(coupon: &Coupon): &CouponRules { + &coupon.rules +} - /// A helper to calculate the final price after the discount. - public(package) fun calculate_sale_price(coupon: &Coupon, price: u64): u64 { - // If it's fixed price, we just deduce the amount. - if(coupon.kind == constants::fixed_price_discount_type()){ - if(coupon.amount > price) return 0; // protect underflow case. - return price - coupon.amount - }; +public(package) fun rules_mut(coupon: &mut Coupon): &mut CouponRules { + &mut coupon.rules +} - // If it's discount price, we calculate the discount - let discount = (((price as u128) * (coupon.amount as u128) / 100) as u64); - // then remove it from the sale price. - price - discount - } +/// A helper to calculate the final price after the discount. +public(package) fun calculate_sale_price(coupon: &Coupon, price: u64): u64 { + // If it's fixed price, we just deduce the amount. + if (coupon.kind == constants::fixed_price_discount_type()) { + if (coupon.amount > price) return 0; // protect underflow case. + return price - coupon.amount + }; + + // If it's discount price, we calculate the discount + let discount = (((price as u128) * (coupon.amount as u128) / 100) as u64); + // then remove it from the sale price. + price - discount } diff --git a/packages/coupons/sources/coupon_house.move b/packages/coupons/sources/coupon_house.move index 434cb064..4b2d5925 100644 --- a/packages/coupons/sources/coupon_house.move +++ b/packages/coupons/sources/coupon_house.move @@ -2,240 +2,267 @@ // SPDX-License-Identifier: Apache-2.0 /// A module to support coupons for SuiNS. -/// This module allows secondary modules (e.g. Discord) to add or remove coupons too. -/// This allows for separation of logic & ease of de-authorization in case we don't want some functionality anymore. -/// -/// Coupons are unique string codes, that can be used (based on the business rules) to claim discounts in the app. -/// Each coupon is validated towards a list of rules. View `rules` module for explanation. -/// The app is authorized on `SuiNS` to be able to claim names and add earnings to the registry. -module coupons::coupon_house { - use std::string::String; - use sui::{ - dynamic_field::{Self as df}, - clock::Clock, - sui::SUI, - coin::Coin - }; - use coupons::{ - rules::CouponRules, - data::{Self, Data}, - coupon::{Self, Coupon} - }; - use suins::{ - domain, - suins::{Self, AdminCap, SuiNS}, - suins_registration::SuinsRegistration, - config::{Self, Config}, - registry::Registry - }; +/// This module allows secondary modules (e.g. Discord) to add or remove coupons +/// too. +/// This allows for separation of logic & ease of de-authorization in case we +/// don't want some functionality anymore. +/// +/// Coupons are unique string codes, that can be used (based on the business +/// rules) to claim discounts in the app. +/// Each coupon is validated towards a list of rules. View `rules` module for +/// explanation. +/// The app is authorized on `SuiNS` to be able to claim names and add earnings +/// to the registry. +module coupons::coupon_house; + +use coupons::coupon::{Self, Coupon}; +use coupons::data::{Self, Data}; +use coupons::rules::CouponRules; +use std::string::String; +use sui::clock::Clock; +use sui::coin::Coin; +use sui::dynamic_field as df; +use sui::sui::SUI; +use suins::config::{Self, Config}; +use suins::domain; +use suins::registry::Registry; +use suins::suins::{Self, AdminCap, SuiNS}; +use suins::suins_registration::SuinsRegistration; + +/// An app that's not authorized tries to access private data. +const EAppNotAuthorized: u64 = 1; +/// Tries to use app on an invalid version. +const EInvalidVersion: u64 = 2; + +/// These errors are claim errors. +/// Number of years passed is not within [1-5] interval. +const EInvalidYearsArgument: u64 = 3; +/// The payment does not match the price for the domain. +const EIncorrectAmount: u64 = 4; +/// Coupon doesn't exist. +const ECouponNotExists: u64 = 5; + +/// Our versioning of the coupons package. +const VERSION: u8 = 1; + +// Authorization for the Coupons on SuiNS, to be able to register names on the +// app. +public struct CouponsApp has drop {} + +/// Authorization Key for secondary apps (e.g. Discord) connected to this +/// module. +public struct AppKey has copy, store, drop {} + +/// The CouponHouse Shared Object which holds a table of coupon codes available +/// for claim. +public struct CouponHouse has store { + data: Data, + version: u8, + storage: UID, +} - /// An app that's not authorized tries to access private data. - const EAppNotAuthorized: u64 = 1; - /// Tries to use app on an invalid version. - const EInvalidVersion: u64 = 2; - - /// These errors are claim errors. - /// Number of years passed is not within [1-5] interval. - const EInvalidYearsArgument: u64 = 3; - /// The payment does not match the price for the domain. - const EIncorrectAmount: u64 = 4; - /// Coupon doesn't exist. - const ECouponNotExists: u64 = 5; - - /// Our versioning of the coupons package. - const VERSION: u8 = 1; - - // Authorization for the Coupons on SuiNS, to be able to register names on the app. - public struct CouponsApp has drop {} - - /// Authorization Key for secondary apps (e.g. Discord) connected to this module. - public struct AppKey has copy, store, drop {} - - /// The CouponHouse Shared Object which holds a table of coupon codes available for claim. - public struct CouponHouse has store { - data: Data, - version: u8, - storage: UID - } - - /// Called once to setup the CouponHouse on SuiNS. - public fun setup(suins: &mut SuiNS, cap: &AdminCap, ctx: &mut TxContext) { - cap.add_registry(suins, CouponHouse { +/// Called once to setup the CouponHouse on SuiNS. +public fun setup(suins: &mut SuiNS, cap: &AdminCap, ctx: &mut TxContext) { + cap.add_registry( + suins, + CouponHouse { storage: object::new(ctx), data: data::new(ctx), - version: VERSION - }); - } - - /// Register a name using a coupon code. - public fun register_with_coupon( - suins: &mut SuiNS, - coupon_code: String, - domain_name: String, - no_years: u8, - payment: Coin, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - // Validate registration years are in [0,5] range. - assert!(no_years > 0 && no_years <= 5, EInvalidYearsArgument); - - let config = suins.get_config(); - let domain = domain::new(domain_name); - let label = domain.sld(); - let domain_length = (label.length() as u8); - let original_price = config.calculate_price(domain_length, no_years); - // Validate name can be registered (is main domain (no subdomain) and length is valid) - config::assert_valid_user_registerable_domain(&domain); - - // Verify coupon house is authorized to get the registry / register names. - let coupon_house = coupon_house_mut(suins); - - // Validate that specified coupon is valid. - assert!(coupon_house.data.coupons().contains(coupon_code), ECouponNotExists); - - // Borrow coupon from the table. - let coupon: &mut Coupon = &mut coupon_house.data.coupons_mut()[coupon_code]; - - // We need to do a total of 5 checks, based on `CouponRules` - // Our checks work with `AND`, all of the conditions must pass for a coupon to be used. - // 1. Validate domain size. - coupon.rules().assert_coupon_valid_for_domain_size(domain_length); - // 2. Decrease available claims. Will ABORT if the coupon doesn't have enough available claims. - coupon.rules_mut().decrease_available_claims(); - // 3. Validate the coupon is valid for the specified user. - coupon.rules().assert_coupon_valid_for_address(ctx.sender()); - // 4. Validate the coupon hasn't expired (Based on clock) - coupon.rules().assert_coupon_is_not_expired(clock); - // 5. Validate years are valid for the coupon. - coupon.rules().assert_coupon_valid_for_domain_years(no_years); - - let sale_price = coupon.calculate_sale_price(original_price); - assert!(payment.value() == sale_price, EIncorrectAmount); - - // Clean up our registry by removing the coupon if no more available claims! - if(!coupon.rules().has_available_claims()){ - // remove the coupon, since it's no longer usable. - coupon_house.data.remove_coupon(coupon_code); - }; - - suins::app_add_balance(CouponsApp {}, suins, payment.into_balance()); - let registry: &mut Registry = suins::app_registry_mut(CouponsApp {}, suins); - registry.add_record(domain, no_years, clock, ctx) - } - - // A convenient helper to calculate the price in a PTB. - // Important: This function doesn't check the validity of the coupon (Whether the user can indeed use it) - // Nor does it calculate the original price. This is part of the Frontend anyways. - public fun calculate_sale_price(suins: &SuiNS, price: u64, coupon_code: String): u64 { - let coupon_house = coupon_house(suins); - // Validate that specified coupon is valid. - assert!(coupon_house.data.coupons().contains(coupon_code), ECouponNotExists); - - // Borrow coupon from the table. - let coupon: &Coupon = &coupon_house.data.coupons()[coupon_code]; - - coupon.calculate_sale_price(price) - } - - // Get `Data` as an authorized app. - public fun app_data_mut(suins: &mut SuiNS, _: A): &mut Data { - let coupon_house_mut = coupon_house_mut(suins); - coupon_house_mut.assert_version_is_valid(); - // verify app is authorized to get a mutable reference. - coupon_house_mut.assert_app_is_authorized(); - &mut coupon_house_mut.data - } - - /// Authorize an app on the coupon house. This allows to a secondary module to add/remove coupons. - public fun authorize_app(_: &AdminCap, suins: &mut SuiNS) { - df::add(&mut coupon_house_mut(suins).storage, AppKey{}, true); - } - - /// De-authorize an app. The app can no longer add or remove - public fun deauthorize_app(_: &AdminCap, suins: &mut SuiNS): bool { - df::remove(&mut coupon_house_mut(suins).storage, AppKey{}) - } - - /// An admin helper to set the version of the shared object. - /// Registrations are only possible if the latest version is being used. - public fun set_version(_: &AdminCap, suins: &mut SuiNS, version: u8) { - coupon_house_mut(suins).version = version; - } - - /// Validate that the version of the app is the latest. - public fun assert_version_is_valid(self: &CouponHouse) { - assert!(self.version == VERSION, EInvalidVersion); - } - - // Add a coupon as an admin. - /// To create a coupon, you have to call the PTB in the specific order - /// 1. (Optional) Call rules::new_domain_length_rule(type, length) // generate a length specific rule (e.g. only domains of size 5) - /// 2. Call rules::coupon_rules(...) to create the coupon's ruleset. - public fun admin_add_coupon( - _: &AdminCap, - suins: &mut SuiNS, - code: String, - kind: u8, - amount: u64, - rules: CouponRules, - ctx: &mut TxContext - ) { - let coupon_house = coupon_house_mut(suins); - coupon_house.assert_version_is_valid(); - coupon_house.data.save_coupon(code, coupon::new(kind, amount, rules, ctx)); - } - - // Remove a coupon as a system's admin. - public fun admin_remove_coupon(_: &AdminCap, suins: &mut SuiNS, code: String){ - let coupon_house = coupon_house_mut(suins); - coupon_house.assert_version_is_valid(); - coupon_house.data.remove_coupon(code); - } - - // Add coupon as a registered app. - public fun app_add_coupon( - data: &mut Data, - code: String, - kind: u8, - amount: u64, - rules: CouponRules, - ctx: &mut TxContext - ){ - data.save_coupon(code, coupon::new(kind, amount, rules, ctx)); - } - - // Remove a coupon as a registered app. - public fun app_remove_coupon(data: &mut Data, code: String) { - data.remove_coupon(code); - } - - /// Check if an application is authorized to access protected features of the Coupon House. - fun is_app_authorized(coupon_house: &CouponHouse): bool { - df::exists_(&coupon_house.storage, AppKey{}) - } - - /// Assert that an application is authorized to access protected features of Coupon House. - /// Aborts with `EAppNotAuthorized` if not. - fun assert_app_is_authorized(coupon_house: &CouponHouse) { - assert!(coupon_house.is_app_authorized(), EAppNotAuthorized); - } - - /// local helper to get the `coupon house` object from the SuiNS object. - fun coupon_house(suins: &SuiNS): &CouponHouse { - // Verify coupon house is authorized to get the registry / register names. - suins.assert_app_is_authorized(); - let coupons = suins.registry(); - coupons.assert_version_is_valid(); - coupons - } - - /// Gets a mutable reference to the coupon's house - fun coupon_house_mut(suins: &mut SuiNS): &mut CouponHouse { - // Verify coupon house is authorized to get the registry / register names. - suins.assert_app_is_authorized(); - let coupons = suins::app_registry_mut(CouponsApp {}, suins); - coupons.assert_version_is_valid(); - coupons - } + version: VERSION, + }, + ); +} + +/// Register a name using a coupon code. +public fun register_with_coupon( + suins: &mut SuiNS, + coupon_code: String, + domain_name: String, + no_years: u8, + payment: Coin, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + // Validate registration years are in [0,5] range. + assert!(no_years > 0 && no_years <= 5, EInvalidYearsArgument); + + let config = suins.get_config(); + let domain = domain::new(domain_name); + let label = domain.sld(); + let domain_length = (label.length() as u8); + let original_price = config.calculate_price(domain_length, no_years); + // Validate name can be registered (is main domain (no subdomain) and length + // is valid) + config::assert_valid_user_registerable_domain(&domain); + + // Verify coupon house is authorized to get the registry / register names. + let coupon_house = coupon_house_mut(suins); + + // Validate that specified coupon is valid. + assert!( + coupon_house.data.coupons().contains(coupon_code), + ECouponNotExists, + ); + + // Borrow coupon from the table. + let coupon: &mut Coupon = &mut coupon_house.data.coupons_mut()[coupon_code]; + + // We need to do a total of 5 checks, based on `CouponRules` + // Our checks work with `AND`, all of the conditions must pass for a coupon + // to be used. + // 1. Validate domain size. + coupon.rules().assert_coupon_valid_for_domain_size(domain_length); + // 2. Decrease available claims. Will ABORT if the coupon doesn't have + // enough available claims. + coupon.rules_mut().decrease_available_claims(); + // 3. Validate the coupon is valid for the specified user. + coupon.rules().assert_coupon_valid_for_address(ctx.sender()); + // 4. Validate the coupon hasn't expired (Based on clock) + coupon.rules().assert_coupon_is_not_expired(clock); + // 5. Validate years are valid for the coupon. + coupon.rules().assert_coupon_valid_for_domain_years(no_years); + + let sale_price = coupon.calculate_sale_price(original_price); + assert!(payment.value() == sale_price, EIncorrectAmount); + + // Clean up our registry by removing the coupon if no more available claims! + if (!coupon.rules().has_available_claims()) { + // remove the coupon, since it's no longer usable. + coupon_house.data.remove_coupon(coupon_code); + }; + + suins::app_add_balance(CouponsApp {}, suins, payment.into_balance()); + let registry: &mut Registry = suins::app_registry_mut(CouponsApp {}, suins); + registry.add_record(domain, no_years, clock, ctx) +} + +// A convenient helper to calculate the price in a PTB. +// Important: This function doesn't check the validity of the coupon (Whether +// the user can indeed use it) +// Nor does it calculate the original price. This is part of the Frontend +// anyways. +public fun calculate_sale_price( + suins: &SuiNS, + price: u64, + coupon_code: String, +): u64 { + let coupon_house = coupon_house(suins); + // Validate that specified coupon is valid. + assert!( + coupon_house.data.coupons().contains(coupon_code), + ECouponNotExists, + ); + + // Borrow coupon from the table. + let coupon: &Coupon = &coupon_house.data.coupons()[coupon_code]; + + coupon.calculate_sale_price(price) +} + +// Get `Data` as an authorized app. +public fun app_data_mut(suins: &mut SuiNS, _: A): &mut Data { + let coupon_house_mut = coupon_house_mut(suins); + coupon_house_mut.assert_version_is_valid(); + // verify app is authorized to get a mutable reference. + coupon_house_mut.assert_app_is_authorized(); + &mut coupon_house_mut.data +} + +/// Authorize an app on the coupon house. This allows to a secondary module to +/// add/remove coupons. +public fun authorize_app(_: &AdminCap, suins: &mut SuiNS) { + df::add(&mut coupon_house_mut(suins).storage, AppKey {}, true); +} + +/// De-authorize an app. The app can no longer add or remove +public fun deauthorize_app(_: &AdminCap, suins: &mut SuiNS): bool { + df::remove(&mut coupon_house_mut(suins).storage, AppKey {}) +} + +/// An admin helper to set the version of the shared object. +/// Registrations are only possible if the latest version is being used. +public fun set_version(_: &AdminCap, suins: &mut SuiNS, version: u8) { + coupon_house_mut(suins).version = version; +} + +/// Validate that the version of the app is the latest. +public fun assert_version_is_valid(self: &CouponHouse) { + assert!(self.version == VERSION, EInvalidVersion); +} + +// Add a coupon as an admin. +/// To create a coupon, you have to call the PTB in the specific order +/// 1. (Optional) Call rules::new_domain_length_rule(type, length) // generate a +/// length specific rule (e.g. only domains of size 5) +/// 2. Call rules::coupon_rules(...) to create the coupon's ruleset. +public fun admin_add_coupon( + _: &AdminCap, + suins: &mut SuiNS, + code: String, + kind: u8, + amount: u64, + rules: CouponRules, + ctx: &mut TxContext, +) { + let coupon_house = coupon_house_mut(suins); + coupon_house.assert_version_is_valid(); + coupon_house.data.save_coupon(code, coupon::new(kind, amount, rules, ctx)); +} + +// Remove a coupon as a system's admin. +public fun admin_remove_coupon(_: &AdminCap, suins: &mut SuiNS, code: String) { + let coupon_house = coupon_house_mut(suins); + coupon_house.assert_version_is_valid(); + coupon_house.data.remove_coupon(code); +} + +// Add coupon as a registered app. +public fun app_add_coupon( + data: &mut Data, + code: String, + kind: u8, + amount: u64, + rules: CouponRules, + ctx: &mut TxContext, +) { + data.save_coupon(code, coupon::new(kind, amount, rules, ctx)); +} + +// Remove a coupon as a registered app. +public fun app_remove_coupon(data: &mut Data, code: String) { + data.remove_coupon(code); +} + +/// Check if an application is authorized to access protected features of the +/// Coupon House. +fun is_app_authorized(coupon_house: &CouponHouse): bool { + df::exists_(&coupon_house.storage, AppKey {}) +} + +/// Assert that an application is authorized to access protected features of +/// Coupon House. +/// Aborts with `EAppNotAuthorized` if not. +fun assert_app_is_authorized(coupon_house: &CouponHouse) { + assert!(coupon_house.is_app_authorized(), EAppNotAuthorized); +} + +/// local helper to get the `coupon house` object from the SuiNS object. +fun coupon_house(suins: &SuiNS): &CouponHouse { + // Verify coupon house is authorized to get the registry / register names. + suins.assert_app_is_authorized(); + let coupons = suins.registry(); + coupons.assert_version_is_valid(); + coupons +} + +/// Gets a mutable reference to the coupon's house +fun coupon_house_mut(suins: &mut SuiNS): &mut CouponHouse { + // Verify coupon house is authorized to get the registry / register names. + suins.assert_app_is_authorized(); + let coupons = suins::app_registry_mut( + CouponsApp {}, + suins, + ); + coupons.assert_version_is_valid(); + coupons } diff --git a/packages/coupons/sources/data.move b/packages/coupons/sources/data.move index b820bc44..aeaa6e39 100644 --- a/packages/coupons/sources/data.move +++ b/packages/coupons/sources/data.move @@ -1,47 +1,44 @@ -module coupons::data { - use std::string::String; - use sui::bag::{Self, Bag}; - use coupons::coupon::Coupon; - - const ECouponAlreadyExists: u64 = 1; - const ECouponDoesNotExist: u64 = 2; - - /// Create a `Data` struct that only authorized apps can get mutable access to. - /// We don't save the coupon's table directly on the shared object, because we want authorized apps to only perform - /// certain actions with the table (and not give full `mut` access to it). - public struct Data has store { - // hold a list of all coupons in the system. - coupons: Bag - } +module coupons::data; - public(package) fun new(ctx: &mut TxContext): Data { - Data { - coupons: bag::new(ctx) - } - } +use coupons::coupon::Coupon; +use std::string::String; +use sui::bag::{Self, Bag}; - /// Private internal functions - /// An internal function to save the coupon in the shared object's config. - public(package) fun save_coupon( - self: &mut Data, - code: String, - coupon: Coupon - ) { - assert!(!self.coupons.contains(code), ECouponAlreadyExists); - self.coupons.add(code, coupon); - } +const ECouponAlreadyExists: u64 = 1; +const ECouponDoesNotExist: u64 = 2; - // A function to remove a coupon from the system. - public(package) fun remove_coupon(self: &mut Data, code: String) { - assert!(self.coupons.contains(code), ECouponDoesNotExist); - let _: Coupon = self.coupons.remove(code); - } +/// Create a `Data` struct that only authorized apps can get mutable access to. +/// We don't save the coupon's table directly on the shared object, because we +/// want authorized apps to only perform +/// certain actions with the table (and not give full `mut` access to it). +public struct Data has store { + // hold a list of all coupons in the system. + coupons: Bag, +} - public(package) fun coupons(data: &Data): &Bag { - &data.coupons +public(package) fun new(ctx: &mut TxContext): Data { + Data { + coupons: bag::new(ctx), } +} - public(package) fun coupons_mut(data: &mut Data): &mut Bag { - &mut data.coupons - } +/// Private internal functions +/// An internal function to save the coupon in the shared object's config. +public(package) fun save_coupon(self: &mut Data, code: String, coupon: Coupon) { + assert!(!self.coupons.contains(code), ECouponAlreadyExists); + self.coupons.add(code, coupon); +} + +// A function to remove a coupon from the system. +public(package) fun remove_coupon(self: &mut Data, code: String) { + assert!(self.coupons.contains(code), ECouponDoesNotExist); + let _: Coupon = self.coupons.remove(code); +} + +public(package) fun coupons(data: &Data): &Bag { + &data.coupons +} + +public(package) fun coupons_mut(data: &mut Data): &mut Bag { + &mut data.coupons } diff --git a/packages/coupons/sources/range.move b/packages/coupons/sources/range.move index 5427d561..f84078d9 100644 --- a/packages/coupons/sources/range.move +++ b/packages/coupons/sources/range.move @@ -2,38 +2,37 @@ // SPDX-License-Identifier: Apache-2.0 /// A module to introduce `range` checks for the rules. -module coupons::range { - /// Invalid [from, to] setup in the range! - /// `to` parameter has to be >= `from` - const EInvalidRange: u64 = 0; - - /// A Range for u8 helper - public struct Range has copy, store, drop { - vec: vector - } +module coupons::range; - /// a new Range constructor[from, to] - public fun new(from: u8, to: u8): Range { - assert!(to >= from, EInvalidRange); +/// Invalid [from, to] setup in the range! +/// `to` parameter has to be >= `from` +const EInvalidRange: u64 = 0; - Range { - vec: vector[from, to] - } - } +/// A Range for u8 helper +public struct Range has copy, store, drop { + vec: vector, +} - // Verify that the number is in the range (includes from, to) - public fun is_in_range(range: &Range, number: u8): bool { - number >= range.from() && number <= range.to() - } +/// a new Range constructor[from, to] +public fun new(from: u8, to: u8): Range { + assert!(to >= from, EInvalidRange); - /// Get floor limit for the range. - public fun from(range: &Range): u8 { - range.vec[0] - } - - /// Get upper limit for the range. - public fun to(range: &Range): u8 { - range.vec[1] + Range { + vec: vector[from, to], } +} + +// Verify that the number is in the range (includes from, to) +public fun is_in_range(range: &Range, number: u8): bool { + number >= range.from() && number <= range.to() +} + +/// Get floor limit for the range. +public fun from(range: &Range): u8 { + range.vec[0] +} +/// Get upper limit for the range. +public fun to(range: &Range): u8 { + range.vec[1] } diff --git a/packages/coupons/sources/rules.move b/packages/coupons/sources/rules.move index 5e0be1cd..101e5934 100644 --- a/packages/coupons/sources/rules.move +++ b/packages/coupons/sources/rules.move @@ -3,179 +3,208 @@ // A module with a couple of helpers for validation of coupons // validation of names etc. -module coupons::rules { - use sui::clock::Clock; - - use coupons::{constants, range::Range}; - - use suins::constants::{Self as suins_constants}; - // Errors - /// Error when you try to create a DomainLengthRule with invalid type. - const EInvalidLengthRule: u64 = 0; - /// Error when you try to use a coupon that isn't valid for these years. - const ENotValidYears: u64 = 1; - /// Error when you try to use a coupon which doesn't match to the domain's size. - const EInvalidForDomainLength: u64 = 2; - /// Error when you try to use a domain that has used all it's available claims. - const ENoMoreAvailableClaims: u64 = 3; - /// Error when you try to create a percentage discount coupon with invalid percentage amount. - const EInvalidAmount: u64 = 4; - /// Error when you try to create a coupon with invalid type. - const EInvalidType: u64 = 5; - /// Error when you try to use a coupon without the matching address - const EInvalidUser: u64 = 6; - /// Error when coupon has expired - const ECouponExpired: u64 = 7; - /// Error when creating years range. - const EInvalidYears: u64 = 8; - /// Available claims can't be 0. - const EInvalidAvailableClaims: u64 = 9; - - /// The Struct that holds the coupon's rules. - /// All rules are combined in `AND` fashion. - /// All of the checks have to pass for a coupon to be used. - public struct CouponRules has copy, store, drop { - length: Option, - available_claims: Option, - user: Option
, - expiration: Option, - years: Option - } +module coupons::rules; + +use coupons::constants; +use coupons::range::Range; +use sui::clock::Clock; +use suins::constants as suins_constants; + +// Errors +/// Error when you try to create a DomainLengthRule with invalid type. +const EInvalidLengthRule: u64 = 0; +/// Error when you try to use a coupon that isn't valid for these years. +const ENotValidYears: u64 = 1; +/// Error when you try to use a coupon which doesn't match to the domain's size. +const EInvalidForDomainLength: u64 = 2; +/// Error when you try to use a domain that has used all it's available claims. +const ENoMoreAvailableClaims: u64 = 3; +/// Error when you try to create a percentage discount coupon with invalid +/// percentage amount. +const EInvalidAmount: u64 = 4; +/// Error when you try to create a coupon with invalid type. +const EInvalidType: u64 = 5; +/// Error when you try to use a coupon without the matching address +const EInvalidUser: u64 = 6; +/// Error when coupon has expired +const ECouponExpired: u64 = 7; +/// Error when creating years range. +const EInvalidYears: u64 = 8; +/// Available claims can't be 0. +const EInvalidAvailableClaims: u64 = 9; + +/// The Struct that holds the coupon's rules. +/// All rules are combined in `AND` fashion. +/// All of the checks have to pass for a coupon to be used. +public struct CouponRules has copy, store, drop { + length: Option, + available_claims: Option, + user: Option
, + expiration: Option, + years: Option, +} - /// This is used in a PTB when creating a coupon. - /// Creates a CouponRules object to be used to create a coupon. - /// All rules are optional, and can be chained (`AND`) format. - /// 1. Length: The name has to be in range [from, to] - /// 2. Max available claims - /// 3. Only for a specific address - /// 4. Might have an expiration date. - /// 5. Might be valid only for registrations in a range [from, to] - public fun new_coupon_rules( - length: Option, - available_claims: Option, - user: Option
, - expiration: Option, - years: Option - ): CouponRules { - assert!(is_valid_years_range(&years), EInvalidYears); - assert!(is_valid_length_range(&length), EInvalidLengthRule); - assert!(available_claims.is_none() || (*available_claims.borrow() > 0), EInvalidAvailableClaims); - CouponRules { - length, available_claims, user, expiration, years - } +/// This is used in a PTB when creating a coupon. +/// Creates a CouponRules object to be used to create a coupon. +/// All rules are optional, and can be chained (`AND`) format. +/// 1. Length: The name has to be in range [from, to] +/// 2. Max available claims +/// 3. Only for a specific address +/// 4. Might have an expiration date. +/// 5. Might be valid only for registrations in a range [from, to] +public fun new_coupon_rules( + length: Option, + available_claims: Option, + user: Option
, + expiration: Option, + years: Option, +): CouponRules { + assert!(is_valid_years_range(&years), EInvalidYears); + assert!(is_valid_length_range(&length), EInvalidLengthRule); + assert!( + available_claims.is_none() || (*available_claims.borrow() > 0), + EInvalidAvailableClaims, + ); + CouponRules { + length, + available_claims, + user, + expiration, + years, } +} - // A convenient helper to create a zero rule `CouponRules` object. - // This helps generate a coupon that can be used without any of the restrictions. - public fun new_empty_rules(): CouponRules { - CouponRules { - length: option::none(), - available_claims: option::none(), - user: option::none(), - expiration: option::none(), - years: option::none() - } +// A convenient helper to create a zero rule `CouponRules` object. +// This helps generate a coupon that can be used without any of the +// restrictions. +public fun new_empty_rules(): CouponRules { + CouponRules { + length: option::none(), + available_claims: option::none(), + user: option::none(), + expiration: option::none(), + years: option::none(), } +} - /// If the rules count `available_claims`, we decrease it. - /// Aborts if there are no more available claims on that coupon. - /// We shouldn't get here ever, as we're checking this on the coupon creation, but - /// keeping it as a sanity check (e.g. created a coupon with 0 available claims). - public fun decrease_available_claims(rules: &mut CouponRules) { - if(rules.available_claims.is_some()){ - assert!(has_available_claims(rules), ENoMoreAvailableClaims); - // Decrease available claims by 1. - let available_claims = *rules.available_claims.borrow(); - rules.available_claims.swap(available_claims - 1); - } +/// If the rules count `available_claims`, we decrease it. +/// Aborts if there are no more available claims on that coupon. +/// We shouldn't get here ever, as we're checking this on the coupon creation, +/// but +/// keeping it as a sanity check (e.g. created a coupon with 0 available +/// claims). +public fun decrease_available_claims(rules: &mut CouponRules) { + if (rules.available_claims.is_some()) { + assert!(has_available_claims(rules), ENoMoreAvailableClaims); + // Decrease available claims by 1. + let available_claims = *rules.available_claims.borrow(); + rules.available_claims.swap(available_claims - 1); } +} - // Checks whether a coupon has available claims. - // Returns true if the rule is not set OR it has used all the available claims. - public fun has_available_claims(rules: &CouponRules): bool { - if(rules.available_claims.is_none()) return true; - *rules.available_claims.borrow() > 0 - } +// Checks whether a coupon has available claims. +// Returns true if the rule is not set OR it has used all the available claims. +public fun has_available_claims(rules: &CouponRules): bool { + if (rules.available_claims.is_none()) return true; + *rules.available_claims.borrow() > 0 +} - // Assertion helper for the validity of years. - public fun assert_coupon_valid_for_domain_years(rules: &CouponRules, target: u8) { - assert!(is_coupon_valid_for_domain_years(rules, target), ENotValidYears); - } +// Assertion helper for the validity of years. +public fun assert_coupon_valid_for_domain_years( + rules: &CouponRules, + target: u8, +) { + assert!(is_coupon_valid_for_domain_years(rules, target), ENotValidYears); +} - // Checks if a target amount of years is valid for claim. - // Our years is either empty, or a vector [from, to] (e.g. [1,2]) - // That means we can create a combination of: - // 1. Exact years (e.g. 2 years, by passing [2,2]) - // 2. Range of years (e.g. [1,3]) - public fun is_coupon_valid_for_domain_years(rules: &CouponRules, target: u8): bool { - if(rules.years.is_none()) return true; +// Checks if a target amount of years is valid for claim. +// Our years is either empty, or a vector [from, to] (e.g. [1,2]) +// That means we can create a combination of: +// 1. Exact years (e.g. 2 years, by passing [2,2]) +// 2. Range of years (e.g. [1,3]) +public fun is_coupon_valid_for_domain_years( + rules: &CouponRules, + target: u8, +): bool { + if (rules.years.is_none()) return true; + + rules.years.borrow().is_in_range(target) +} - rules.years.borrow().is_in_range(target) - } +public fun assert_is_valid_discount_type(`type`: u8) { + assert!(constants::discount_rule_types().contains(&`type`), EInvalidType); +} - public fun assert_is_valid_discount_type(`type`: u8) { - assert!(constants::discount_rule_types().contains(&`type`), EInvalidType); - } - - // verify that we are creating the coupons correctly (based on amount & type). - // for amounts, if we have a percentage discount, our max num is 100. - public fun assert_is_valid_amount(`type`: u8, amount: u64) { - assert!(amount > 0, EInvalidAmount); // protect from division by 0. 0 doesn't make sense in any scenario. - if(`type` == constants::percentage_discount_type()){ - assert!(amount<=100, EInvalidAmount) - } +// verify that we are creating the coupons correctly (based on amount & type). +// for amounts, if we have a percentage discount, our max num is 100. +public fun assert_is_valid_amount(`type`: u8, amount: u64) { + assert!(amount > 0, EInvalidAmount); // protect from division by 0. 0 doesn't make sense in any scenario. + if (`type` == constants::percentage_discount_type()) { + assert!(amount<=100, EInvalidAmount) } +} - // We check a DomainSize Rule against the length of a domain. - // We return if the length is valid based on that. - public fun assert_coupon_valid_for_domain_size(rules: &CouponRules, length: u8) { - assert!(is_coupon_valid_for_domain_size(rules, length), EInvalidForDomainLength) - } - /// We check the length of the name based on the domain length rule - public fun is_coupon_valid_for_domain_size(rules: &CouponRules, length: u8): bool { - // If the vec is not set, we pass this rule test. - if(rules.length.is_none()) return true; +// We check a DomainSize Rule against the length of a domain. +// We return if the length is valid based on that. +public fun assert_coupon_valid_for_domain_size( + rules: &CouponRules, + length: u8, +) { + assert!( + is_coupon_valid_for_domain_size(rules, length), + EInvalidForDomainLength, + ) +} - rules.length.borrow().is_in_range(length) - } +/// We check the length of the name based on the domain length rule +public fun is_coupon_valid_for_domain_size( + rules: &CouponRules, + length: u8, +): bool { + // If the vec is not set, we pass this rule test. + if (rules.length.is_none()) return true; + rules.length.borrow().is_in_range(length) +} - // We check that the coupon is valid for the specified address. - /// Throws `EInvalidUser` error if it has expired. - public fun assert_coupon_valid_for_address(rules: &CouponRules, user: address) { - assert!(is_coupon_valid_for_address(rules, user), EInvalidUser); - } - /// Check that the domain is valid for the specified address - public fun is_coupon_valid_for_address(rules: &CouponRules, user: address): bool { - if(rules.user.is_none()) return true; - rules.user.borrow() == user - } +// We check that the coupon is valid for the specified address. +/// Throws `EInvalidUser` error if it has expired. +public fun assert_coupon_valid_for_address(rules: &CouponRules, user: address) { + assert!(is_coupon_valid_for_address(rules, user), EInvalidUser); +} - /// Simple assertion for the coupon expiration. - /// Throws `ECouponExpired` error if it has expired. - public fun assert_coupon_is_not_expired(rules: &CouponRules, clock: &Clock) { - assert!(!is_coupon_expired(rules, clock), ECouponExpired); - } +/// Check that the domain is valid for the specified address +public fun is_coupon_valid_for_address( + rules: &CouponRules, + user: address, +): bool { + if (rules.user.is_none()) return true; + rules.user.borrow() == user +} - /// Check whether a coupon has expired - public fun is_coupon_expired(rules: &CouponRules, clock: &Clock): bool { - if(rules.expiration.is_none()){ - return false - }; +/// Simple assertion for the coupon expiration. +/// Throws `ECouponExpired` error if it has expired. +public fun assert_coupon_is_not_expired(rules: &CouponRules, clock: &Clock) { + assert!(!is_coupon_expired(rules, clock), ECouponExpired); +} - clock.timestamp_ms() > *rules.expiration.borrow() - } +/// Check whether a coupon has expired +public fun is_coupon_expired(rules: &CouponRules, clock: &Clock): bool { + if (rules.expiration.is_none()) { + return false + }; + clock.timestamp_ms() > *rules.expiration.borrow() +} - fun is_valid_years_range(range: &Option): bool { - if(range.is_none()) return true; - let range = range.borrow(); - range.from() >= 1 && range.to() <= 5 - } +fun is_valid_years_range(range: &Option): bool { + if (range.is_none()) return true; + let range = range.borrow(); + range.from() >= 1 && range.to() <= 5 +} - fun is_valid_length_range(range: &Option): bool { - if(range.is_none()) return true; - let range = range.borrow(); - range.from() >= suins_constants::min_domain_length() && range.to() <= suins_constants::max_domain_length() - } +fun is_valid_length_range(range: &Option): bool { + if (range.is_none()) return true; + let range = range.borrow(); + range.from() >= suins_constants::min_domain_length() && range.to() <= suins_constants::max_domain_length() } diff --git a/packages/coupons/tests/authorization_tests.move b/packages/coupons/tests/authorization_tests.move index d7979d30..60f3d57b 100644 --- a/packages/coupons/tests/authorization_tests.move +++ b/packages/coupons/tests/authorization_tests.move @@ -3,70 +3,64 @@ // A set of tests for the authorization of different apps in the CouponHouse. #[test_only] -module coupons::app_authorization_tests { +module coupons::app_authorization_tests; - use sui::test_scenario::{return_shared, return_to_sender, end}; +use coupons::coupon_house::{Self, deauthorize_app}; +use coupons::setup::{Self, TestApp, admin, user, test_init}; +use sui::test_scenario::{return_shared, return_to_sender, end}; +use suins::suins::SuiNS; - use coupons::{ - coupon_house::{Self, deauthorize_app}, - setup::{ - Self, - TestApp, - admin, - user, - test_init, - } +#[test] +fun admin_get_app_success() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + // auth style as authorized app + { + scenario.next_tx(user()); + let mut suins = scenario.take_shared(); + coupon_house::app_data_mut(&mut suins, setup::test_app()); + return_shared(suins); }; - use suins::suins::SuiNS; - #[test] - fun admin_get_app_success() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - // auth style as authorized app - { - scenario.next_tx(user()); - let mut suins = scenario.take_shared(); - coupon_house::app_data_mut(&mut suins, setup::test_app()); - return_shared(suins); - }; + end(scenario_val); +} - end(scenario_val); - } +#[test] +fun authorized_app_get_app_success() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + { + scenario.next_tx(admin()); - #[test] - fun authorized_app_get_app_success(){ - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - { - scenario.next_tx(admin()); + let mut coupon_house = scenario.take_shared(); + let admin_cap = scenario.take_from_sender(); - let mut coupon_house = scenario.take_shared(); - let admin_cap = scenario.take_from_sender(); - - // test app deauthorization. - deauthorize_app(&admin_cap, &mut coupon_house); + // test app deauthorization. + deauthorize_app(&admin_cap, &mut coupon_house); - // test that the app is indeed non authorized - assert!(!coupon_house.is_app_authorized(), 0); - - return_to_sender(scenario, admin_cap); - return_shared(coupon_house); - }; - end(scenario_val); - } + // test that the app is indeed non authorized + assert!(!coupon_house.is_app_authorized(), 0); - #[test, expected_failure(abort_code=::coupons::coupon_house::EAppNotAuthorized)] - fun unauthorized_app_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - { - scenario.next_tx(user()); - let mut suins = scenario.take_shared(); - coupon_house::app_data_mut(&mut suins, setup::unauthorized_test_app()); - return_shared(suins); - }; - end(scenario_val); - } - + return_to_sender(scenario, admin_cap); + return_shared(coupon_house); + }; + end(scenario_val); +} + +#[ + test, + expected_failure( + abort_code = ::coupons::coupon_house::EAppNotAuthorized, + ), +] +fun unauthorized_app_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + { + scenario.next_tx(user()); + let mut suins = scenario.take_shared(); + coupon_house::app_data_mut(&mut suins, setup::unauthorized_test_app()); + return_shared(suins); + }; + end(scenario_val); } diff --git a/packages/coupons/tests/coupons_tests.move b/packages/coupons/tests/coupons_tests.move index f5a7f9f9..9623d50d 100644 --- a/packages/coupons/tests/coupons_tests.move +++ b/packages/coupons/tests/coupons_tests.move @@ -2,267 +2,493 @@ // SPDX-License-Identifier: Apache-2.0 #[test_only] -module coupons::coupon_tests { - use sui::{ - test_scenario::{Self, Scenario, return_shared}, - test_utils - }; - use suins::suins::SuiNS; +module coupons::coupon_tests; - // test dependencies. - use coupons::{ - setup::{Self, TestApp, user, user_two, mist_per_sui, test_app, admin_add_coupon, register_with_coupon, test_init}, - coupon_house, - data, - constants::{Self}, - rules, - range - }; +use coupons::constants; +use coupons::coupon_house; +use coupons::data; +use coupons::range; +use coupons::rules; +use coupons::setup::{ + Self, + TestApp, + user, + user_two, + mist_per_sui, + test_app, + admin_add_coupon, + register_with_coupon, + test_init +}; +use sui::test_scenario::{Self, Scenario, return_shared}; +use sui::test_utils; +use suins::suins::SuiNS; - // populate a lot of coupons with different cases. - // This populates the coupon as an authorized app - fun populate_coupons(scenario: &mut Scenario) { - scenario.next_tx(user()); - let mut suins = scenario.take_shared(); +// populate a lot of coupons with different cases. +// This populates the coupon as an authorized app +fun populate_coupons(scenario: &mut Scenario) { + scenario.next_tx(user()); + let mut suins = scenario.take_shared(); - let data_mut = coupon_house::app_data_mut(&mut suins, test_app()); - setup::populate_coupons(data_mut, scenario.ctx()); - return_shared(suins); - } + let data_mut = coupon_house::app_data_mut(&mut suins, test_app()); + setup::populate_coupons(data_mut, scenario.ctx()); + return_shared(suins); +} - // Please look up at `setup` file to see all the coupon names and their respective logic. - // Tests the e2e experience for coupons (a list of different coupons with different rules) - #[test] - fun test_e2e() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - // populate all coupons. - populate_coupons(scenario); +// Please look up at `setup` file to see all the coupon names and their +// respective logic. +// Tests the e2e experience for coupons (a list of different coupons with +// different rules) +#[test] +fun test_e2e() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + // populate all coupons. + populate_coupons(scenario); - // 5 SUI discount coupon. - register_with_coupon(b"5_SUI_DISCOUNT".to_string(), b"test.sui".to_string(), 1, 195 * mist_per_sui(), 0, user(), scenario); + // 5 SUI discount coupon. + register_with_coupon( + b"5_SUI_DISCOUNT".to_string(), + b"test.sui".to_string(), + 1, + 195 * mist_per_sui(), + 0, + user(), + scenario, + ); - // original price would be 400 (200*2 years). 25% discount should bring it down to 300. - register_with_coupon(b"25_PERCENT_DISCOUNT_MAX_2_YEARS".to_string(), b"jest.sui".to_string(), 2, 300 * mist_per_sui(), 0, user(), scenario); + // original price would be 400 (200*2 years). 25% discount should bring it + // down to 300. + register_with_coupon( + b"25_PERCENT_DISCOUNT_MAX_2_YEARS".to_string(), + b"jest.sui".to_string(), + 2, + 300 * mist_per_sui(), + 0, + user(), + scenario, + ); - // Test that this user-specific coupon works as expected - register_with_coupon(b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), b"fest.sui".to_string(), 2, 300 * mist_per_sui(), 0, user(), scenario); + // Test that this user-specific coupon works as expected + register_with_coupon( + b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), + b"fest.sui".to_string(), + 2, + 300 * mist_per_sui(), + 0, + user(), + scenario, + ); - // 50% discount only on names 5+ digits - register_with_coupon(b"50_PERCENT_5_PLUS_NAMES".to_string(), b"testo.sui".to_string(), 1, 25 * mist_per_sui(), 0, user(), scenario); + // 50% discount only on names 5+ digits + register_with_coupon( + b"50_PERCENT_5_PLUS_NAMES".to_string(), + b"testo.sui".to_string(), + 1, + 25 * mist_per_sui(), + 0, + user(), + scenario, + ); - // 50% discount only on names 3 digit names. - register_with_coupon(b"50_PERCENT_3_DIGITS".to_string(), b"tes.sui".to_string(), 1, 600 * mist_per_sui(), 0, user(), scenario); + // 50% discount only on names 3 digit names. + register_with_coupon( + b"50_PERCENT_3_DIGITS".to_string(), + b"tes.sui".to_string(), + 1, + 600 * mist_per_sui(), + 0, + user(), + scenario, + ); - // 50% DISCOUNT, with all possible rules involved. - register_with_coupon(b"50_DISCOUNT_SALAD".to_string(), b"teso.sui".to_string(), 1, 100 * mist_per_sui(), 0, user(), scenario); + // 50% DISCOUNT, with all possible rules involved. + register_with_coupon( + b"50_DISCOUNT_SALAD".to_string(), + b"teso.sui".to_string(), + 1, + 100 * mist_per_sui(), + 0, + user(), + scenario, + ); - scenario_val.end(); - } - #[test] - fun zero_fee_purchase(){ - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - // populate all coupons. - populate_coupons(scenario); - // 5 SUI discount coupon. - admin_add_coupon(b"100_SUI_OFF".to_string(), constants::fixed_price_discount_type(), 100 * mist_per_sui(), scenario); - // Buy a name for free using the 100 SUI OFF coupon! - register_with_coupon(b"100_SUI_OFF".to_string(), b"testo.sui".to_string(), 1, 0 * mist_per_sui(), 0, user(), scenario); - scenario_val.end(); - } - #[test] - fun specific_max_years(){ - rules::new_coupon_rules( - option::none(), - option::none(), - option::none(), - option::none(), - option::some(range::new(1,1)) - ); - } - #[test, expected_failure(abort_code=::coupons::rules::EInvalidYears)] - fun max_years_failure(){ - rules::new_coupon_rules( - option::none(), - option::none(), - option::none(), - option::none(), - option::some(range::new(0,1)) - ); - } - #[test, expected_failure(abort_code=::coupons::range::EInvalidRange)] - fun max_years_two_failure(){ - rules::new_coupon_rules( - option::none(), - option::none(), - option::none(), - option::none(), - option::some(range::new(5,4)) - ); - } + scenario_val.end(); +} +#[test] +fun zero_fee_purchase() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + // populate all coupons. + populate_coupons(scenario); + // 5 SUI discount coupon. + admin_add_coupon( + b"100_SUI_OFF".to_string(), + constants::fixed_price_discount_type(), + 100 * mist_per_sui(), + scenario, + ); + // Buy a name for free using the 100 SUI OFF coupon! + register_with_coupon( + b"100_SUI_OFF".to_string(), + b"testo.sui".to_string(), + 1, + 0 * mist_per_sui(), + 0, + user(), + scenario, + ); + scenario_val.end(); +} +#[test] +fun specific_max_years() { + rules::new_coupon_rules( + option::none(), + option::none(), + option::none(), + option::none(), + option::some(range::new(1, 1)), + ); +} +#[test, expected_failure(abort_code = ::coupons::rules::EInvalidYears)] +fun max_years_failure() { + rules::new_coupon_rules( + option::none(), + option::none(), + option::none(), + option::none(), + option::some(range::new(0, 1)), + ); +} +#[test, expected_failure(abort_code = ::coupons::range::EInvalidRange)] +fun max_years_two_failure() { + rules::new_coupon_rules( + option::none(), + option::none(), + option::none(), + option::none(), + option::some(range::new(5, 4)), + ); +} + +#[test] +fun test_price_calculation() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + { + test_scenario::next_tx(scenario, user()); + let suins = test_scenario::take_shared(scenario); - #[test] - fun test_price_calculation(){ - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - { - test_scenario::next_tx(scenario, user()); - let suins = test_scenario::take_shared(scenario); - - let sale_price = coupon_house::calculate_sale_price(&suins, 100, b"50_PERCENT_5_PLUS_NAMES".to_string()); - assert!(sale_price == 50, 1); - test_scenario::return_shared(suins); - }; - scenario_val.end(); - } - // Tests the e2e experience for coupons (a list of different coupons with different rules) - #[test, expected_failure(abort_code=::coupons::coupon_house::EIncorrectAmount)] - fun test_invalid_coin_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - // populate all coupons. - populate_coupons(scenario); - // 5 SUI discount coupon. - register_with_coupon(b"5_SUI_DISCOUNT".to_string(), b"test.sui".to_string(), 1, 200 * mist_per_sui(), 0, user(), scenario); - scenario_val.end(); - } - #[test, expected_failure(abort_code=::coupons::coupon_house::ECouponNotExists)] - fun no_more_available_claims_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - register_with_coupon(b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), b"test.sui".to_string(), 1, 150 * mist_per_sui(), 0, user(), scenario); - register_with_coupon(b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), b"tost.sui".to_string(), 1, 150 * mist_per_sui(), 0, user(), scenario); - scenario_val.end(); - } - #[test, expected_failure(abort_code=::coupons::coupon_house::EInvalidYearsArgument)] - fun invalid_years_claim_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - register_with_coupon(b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), b"test.sui".to_string(), 6, 150 * mist_per_sui(), 0, user(), scenario); - scenario_val.end(); - } - #[test, expected_failure(abort_code=::coupons::coupon_house::EInvalidYearsArgument)] - fun invalid_years_claim_1_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - register_with_coupon(b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), b"test.sui".to_string(), 0, 150 * mist_per_sui(), 0, user(), scenario); - scenario_val.end(); - } + let sale_price = coupon_house::calculate_sale_price( + &suins, + 100, + b"50_PERCENT_5_PLUS_NAMES".to_string(), + ); + assert!(sale_price == 50, 1); + test_scenario::return_shared(suins); + }; + scenario_val.end(); +} +// Tests the e2e experience for coupons (a list of different coupons with +// different rules) +#[ + test, + expected_failure( + abort_code = ::coupons::coupon_house::EIncorrectAmount, + ), +] +fun test_invalid_coin_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + // populate all coupons. + populate_coupons(scenario); + // 5 SUI discount coupon. + register_with_coupon( + b"5_SUI_DISCOUNT".to_string(), + b"test.sui".to_string(), + 1, + 200 * mist_per_sui(), + 0, + user(), + scenario, + ); + scenario_val.end(); +} +#[ + test, + expected_failure( + abort_code = ::coupons::coupon_house::ECouponNotExists, + ), +] +fun no_more_available_claims_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + register_with_coupon( + b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), + b"test.sui".to_string(), + 1, + 150 * mist_per_sui(), + 0, + user(), + scenario, + ); + register_with_coupon( + b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), + b"tost.sui".to_string(), + 1, + 150 * mist_per_sui(), + 0, + user(), + scenario, + ); + scenario_val.end(); +} +#[ + test, + expected_failure( + abort_code = ::coupons::coupon_house::EInvalidYearsArgument, + ), +] +fun invalid_years_claim_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + register_with_coupon( + b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), + b"test.sui".to_string(), + 6, + 150 * mist_per_sui(), + 0, + user(), + scenario, + ); + scenario_val.end(); +} +#[ + test, + expected_failure( + abort_code = ::coupons::coupon_house::EInvalidYearsArgument, + ), +] +fun invalid_years_claim_1_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + register_with_coupon( + b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), + b"test.sui".to_string(), + 0, + 150 * mist_per_sui(), + 0, + user(), + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::EInvalidUser)] - fun invalid_user_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - register_with_coupon(b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), b"test.sui".to_string(), 1, 150 * mist_per_sui(), 0, user_two(), scenario); - scenario_val.end(); - } +#[test, expected_failure(abort_code = ::coupons::rules::EInvalidUser)] +fun invalid_user_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + register_with_coupon( + b"25_PERCENT_DISCOUNT_USER_ONLY".to_string(), + b"test.sui".to_string(), + 1, + 150 * mist_per_sui(), + 0, + user_two(), + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::ECouponExpired)] - fun coupon_expired_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - register_with_coupon(b"50_PERCENT_3_DIGITS".to_string(), b"tes.sui".to_string(), 1, 150 * mist_per_sui(), 2, user_two(), scenario); - scenario_val.end(); - } +#[test, expected_failure(abort_code = ::coupons::rules::ECouponExpired)] +fun coupon_expired_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + register_with_coupon( + b"50_PERCENT_3_DIGITS".to_string(), + b"tes.sui".to_string(), + 1, + 150 * mist_per_sui(), + 2, + user_two(), + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::ENotValidYears)] - fun coupon_not_valid_for_years_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - register_with_coupon(b"50_DISCOUNT_SALAD".to_string(), b"tes.sui".to_string(), 3, 150 * mist_per_sui(), 0, user(), scenario); - scenario_val.end(); - } +#[test, expected_failure(abort_code = ::coupons::rules::ENotValidYears)] +fun coupon_not_valid_for_years_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + register_with_coupon( + b"50_DISCOUNT_SALAD".to_string(), + b"tes.sui".to_string(), + 3, + 150 * mist_per_sui(), + 0, + user(), + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::EInvalidForDomainLength)] - fun coupon_invalid_length_1_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - register_with_coupon(b"50_PERCENT_3_DIGITS".to_string(), b"test.sui".to_string(), 1, 150 * mist_per_sui(), 2, user_two(), scenario); - scenario_val.end(); - } +#[ + test, + expected_failure( + abort_code = ::coupons::rules::EInvalidForDomainLength, + ), +] +fun coupon_invalid_length_1_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + register_with_coupon( + b"50_PERCENT_3_DIGITS".to_string(), + b"test.sui".to_string(), + 1, + 150 * mist_per_sui(), + 2, + user_two(), + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::EInvalidForDomainLength)] - fun coupon_invalid_length_2_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - // Tries to use 5 digit name for a <=4 digit one. - register_with_coupon(b"50_DISCOUNT_SALAD".to_string(), b"testo.sui".to_string(), 1, 150 * mist_per_sui(), 2, user(), scenario); - scenario_val.end(); - } +#[ + test, + expected_failure( + abort_code = ::coupons::rules::EInvalidForDomainLength, + ), +] +fun coupon_invalid_length_2_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + // Tries to use 5 digit name for a <=4 digit one. + register_with_coupon( + b"50_DISCOUNT_SALAD".to_string(), + b"testo.sui".to_string(), + 1, + 150 * mist_per_sui(), + 2, + user(), + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::EInvalidForDomainLength)] - fun coupon_invalid_length_3_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - // Tries to use 4 digit name for a 5+ chars coupon. - register_with_coupon(b"50_PERCENT_5_PLUS_NAMES".to_string(), b"test.sui".to_string(), 1, 150 * mist_per_sui(), 2, user(), scenario); - scenario_val.end(); - } +#[ + test, + expected_failure( + abort_code = ::coupons::rules::EInvalidForDomainLength, + ), +] +fun coupon_invalid_length_3_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + // Tries to use 4 digit name for a 5+ chars coupon. + register_with_coupon( + b"50_PERCENT_5_PLUS_NAMES".to_string(), + b"test.sui".to_string(), + 1, + 150 * mist_per_sui(), + 2, + user(), + scenario, + ); + scenario_val.end(); +} - #[test] - fun add_coupon_as_admin() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - // add a no rule coupon as an admin - admin_add_coupon(b"TEST_SUCCESS_ADDITION".to_string(), constants::fixed_price_discount_type(), 100 * mist_per_sui(), scenario); - setup::admin_remove_coupon(b"TEST_SUCCESS_ADDITION".to_string(), scenario); +#[test] +fun add_coupon_as_admin() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + // add a no rule coupon as an admin + admin_add_coupon( + b"TEST_SUCCESS_ADDITION".to_string(), + constants::fixed_price_discount_type(), + 100 * mist_per_sui(), + scenario, + ); + setup::admin_remove_coupon(b"TEST_SUCCESS_ADDITION".to_string(), scenario); - scenario_val.end(); - } + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::EInvalidType)] - fun add_coupon_invalid_type_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - admin_add_coupon(b"TEST_SUCCESS_ADDITION".to_string(), 5, 100 * mist_per_sui(), scenario); - scenario_val.end(); - } +#[test, expected_failure(abort_code = ::coupons::rules::EInvalidType)] +fun add_coupon_invalid_type_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + admin_add_coupon( + b"TEST_SUCCESS_ADDITION".to_string(), + 5, + 100 * mist_per_sui(), + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::rules::EInvalidAmount)] - fun add_coupon_invalid_amount_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - admin_add_coupon(b"TEST_SUCCESS_ADDITION".to_string(), constants::percentage_discount_type(), 101, scenario); - scenario_val.end(); - } - #[test, expected_failure(abort_code=::coupons::rules::EInvalidAmount)] - fun add_coupon_invalid_amount_2_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - admin_add_coupon(b"TEST_SUCCESS_ADDITION".to_string(), constants::percentage_discount_type(), 0, scenario); - scenario_val.end(); - } +#[test, expected_failure(abort_code = ::coupons::rules::EInvalidAmount)] +fun add_coupon_invalid_amount_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + admin_add_coupon( + b"TEST_SUCCESS_ADDITION".to_string(), + constants::percentage_discount_type(), + 101, + scenario, + ); + scenario_val.end(); +} +#[test, expected_failure(abort_code = ::coupons::rules::EInvalidAmount)] +fun add_coupon_invalid_amount_2_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + admin_add_coupon( + b"TEST_SUCCESS_ADDITION".to_string(), + constants::percentage_discount_type(), + 0, + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::data::ECouponAlreadyExists)] - fun add_coupon_twice_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - populate_coupons(scenario); - admin_add_coupon(b"TEST_SUCCESS_ADDITION".to_string(), constants::percentage_discount_type(), 100, scenario); - admin_add_coupon(b"TEST_SUCCESS_ADDITION".to_string(), constants::percentage_discount_type(), 100, scenario); - scenario_val.end(); - } +#[test, expected_failure(abort_code = ::coupons::data::ECouponAlreadyExists)] +fun add_coupon_twice_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + populate_coupons(scenario); + admin_add_coupon( + b"TEST_SUCCESS_ADDITION".to_string(), + constants::percentage_discount_type(), + 100, + scenario, + ); + admin_add_coupon( + b"TEST_SUCCESS_ADDITION".to_string(), + constants::percentage_discount_type(), + 100, + scenario, + ); + scenario_val.end(); +} - #[test, expected_failure(abort_code=::coupons::data::ECouponDoesNotExist)] - fun remove_non_existing_coupon() { - let mut ctx = tx_context::dummy(); - let mut data = data::new(&mut ctx); - data.remove_coupon(b"TEST_SUCCESS_ADDITION".to_string()); - test_utils::destroy(data); - } +#[test, expected_failure(abort_code = ::coupons::data::ECouponDoesNotExist)] +fun remove_non_existing_coupon() { + let mut ctx = tx_context::dummy(); + let mut data = data::new(&mut ctx); + data.remove_coupon(b"TEST_SUCCESS_ADDITION".to_string()); + test_utils::destroy(data); } diff --git a/packages/coupons/tests/setup.move b/packages/coupons/tests/setup.move index 2d8f2cdd..4f3beca7 100644 --- a/packages/coupons/tests/setup.move +++ b/packages/coupons/tests/setup.move @@ -2,242 +2,269 @@ // SPDX-License-Identifier: Apache-2.0 #[test_only] -module coupons::setup { - use std::string::{utf8, String}; - use sui::{clock::{Self, Clock}, test_scenario::{Self, Scenario, ctx}, sui::SUI, coin::{Self}}; - use coupons::{coupon_house::{Self, CouponsApp}, data::Data, rules::{Self}, constants, range}; - - public struct TestApp has drop {} - - public struct UnauthorizedTestApp has drop {} - - const MIST_PER_SUI: u64 = 1_000_000_000; - - const ADMIN_ADDRESS: address = @0xA001; - const USER_ADDRESS: address = @0xA002; - const USER_2_ADDRESS: address = @0xA003; - - use suins::suins::{Self, AdminCap, SuiNS}; - use suins::registry; - - public fun test_init(): Scenario { - let mut scenario_val = test_scenario::begin(ADMIN_ADDRESS); - let scenario = &mut scenario_val; - initialize_coupon_house(scenario); - scenario_val - } - - public fun initialize_coupon_house(scenario: &mut Scenario) { - { - let mut suins = suins::init_for_testing(scenario.ctx()); - suins::authorize_app_for_testing(&mut suins); - suins::share_for_testing(suins); - let clock = clock::create_for_testing(scenario.ctx()); - clock::share_for_testing(clock); - }; - { - scenario.next_tx(ADMIN_ADDRESS); - // get admin cap - let admin_cap = scenario.take_from_sender(); - let mut suins = scenario.take_shared(); - // initialize coupon data. - coupon_house::setup(&mut suins, &admin_cap, scenario.ctx()); - registry::init_for_testing(&admin_cap, &mut suins, scenario.ctx()); - // authorize TestApp to CouponHouse. - coupon_house::authorize_app(&admin_cap, &mut suins); - test_scenario::return_to_sender(scenario, admin_cap); - test_scenario::return_shared(suins); - }; - } - - public fun admin(): address { - ADMIN_ADDRESS - } - public fun user(): address { - USER_ADDRESS - } - public fun user_two(): address { - USER_2_ADDRESS - } - public fun mist_per_sui(): u64 { - MIST_PER_SUI - } - - // global getters. - - public fun test_app(): TestApp { - TestApp {} - } - - public fun unauthorized_test_app(): UnauthorizedTestApp { - UnauthorizedTestApp {} - } - - /// A helper to add a bunch of coupons (with different setups) that we can use on the coupon tests. - public fun populate_coupons(data_mut: &mut Data, ctx: &mut TxContext) { - - // 5 SUI DISCOUNT, ONLY CLAIMABLE TWICE - coupon_house::app_add_coupon( - data_mut, - utf8(b"5_SUI_DISCOUNT"), - constants::fixed_price_discount_type(), - 5 * MIST_PER_SUI, - rules::new_coupon_rules( - option::none(), // domain length rule, - option::some(2), // available claims - option::none(), // user specific - option::none(), // expiration timestamp - option::none() // available years - ), - ctx - ); - - // 25% DISCOUNT, ONLY FOR 2 YEARS OR LESS REGISTRATIONS - coupon_house::app_add_coupon( - data_mut, - utf8(b"25_PERCENT_DISCOUNT_MAX_2_YEARS"), - constants::percentage_discount_type(), - 25, // 25% - rules::new_coupon_rules( - option::none(), // domain length rule, - option::none(), // claimable as many times as needed - option::none(), // user specific - option::none(), // expiration timestamp - option::some(range::new(1,2)) // Maximum of 2 years - ), - ctx - ); - - // 25% DISCOUNT, only claimable ONCE by a specific user - coupon_house::app_add_coupon( - data_mut, - utf8(b"25_PERCENT_DISCOUNT_USER_ONLY"), - constants::percentage_discount_type(), - 25, // 25% - rules::new_coupon_rules( - option::none(), // domain length rule, - option::some(1), // claimable once. - option::some(user()), // ONLY CLAIMABLE BY SPECIFIC USER - option::none(), // expiration timestamp - option::none() // any years - ), - ctx - ); - - // 50% DISCOUNT, only claimable only for names > 5 digits - coupon_house::app_add_coupon( - data_mut, - utf8(b"50_PERCENT_5_PLUS_NAMES"), - constants::percentage_discount_type(), - 50, // 25% - rules::new_coupon_rules( - // Only usable for domains with length >= 5. This discount wouldn't be applicable for others. - option::some(range::new(5, 63)), // domain length rule, - option::some(1), // claimable once. - option::none(), // claimable by anyone - option::none(), // expiration timestamp - option::none() // Maximum of 2 years - ), - ctx - ); - - // 50% DISCOUNT, only for 3 digit names - coupon_house::app_add_coupon( - data_mut, - utf8(b"50_PERCENT_3_DIGITS"), - constants::percentage_discount_type(), - 50, // 50% - rules::new_coupon_rules( - // Only usable for domains with fixed length of 3 digits. - option::some(range::new(3,3)), // domain length rule, - option::none(), // claimable once. - option::none(), // claimable by anyone - option::some(1), // expiration timestamp. - option::none() // no maximum set - ), - ctx - ); - - // 50% DISCOUNT, has all rules so we can test combinations! - coupon_house::app_add_coupon( - data_mut, - utf8(b"50_DISCOUNT_SALAD"), - constants::percentage_discount_type(), - 50, // 50% - rules::new_coupon_rules( - // Only usable for 3 or 4 digit names (max char = 4) - option::some(range::new(3,4)), // domain length rule, - option::some(1), // claimable once. - option::some(user()), // claimable a specific address - option::some(1), // expires at 1 clock tick - option::some(range::new(1, 2)) // Maximum of 2 years - ), - ctx - ); - - // THESE last two are just for easy coverage. - // We just add + remove the coupon immediately. - coupon_house::app_add_coupon(data_mut, utf8(b"REMOVE_FOR_COVERAGE"), constants::percentage_discount_type(), 50, rules::new_empty_rules(), ctx); - coupon_house::app_remove_coupon(data_mut, utf8(b"REMOVE_FOR_COVERAGE")); - } - - // Adds a 0 rule coupon that gives 15% discount to test admin additions. - public fun admin_add_coupon(code_name: String, kind: u8, value: u64, scenario: &mut Scenario) { - scenario.next_tx(admin()); +module coupons::setup; + +use coupons::constants; +use coupons::coupon_house::{Self, CouponsApp}; +use coupons::data::Data; +use coupons::range; +use coupons::rules; +use std::string::{utf8, String}; +use sui::clock::{Self, Clock}; +use sui::coin; +use sui::sui::SUI; +use sui::test_scenario::{Self, Scenario, ctx}; +use suins::registry; +use suins::suins::{Self, AdminCap, SuiNS}; + +public struct TestApp has drop {} + +public struct UnauthorizedTestApp has drop {} + +const MIST_PER_SUI: u64 = 1_000_000_000; + +const ADMIN_ADDRESS: address = @0xA001; +const USER_ADDRESS: address = @0xA002; +const USER_2_ADDRESS: address = @0xA003; + +public fun test_init(): Scenario { + let mut scenario_val = test_scenario::begin(ADMIN_ADDRESS); + let scenario = &mut scenario_val; + initialize_coupon_house(scenario); + scenario_val +} + +public fun initialize_coupon_house(scenario: &mut Scenario) { + { + let mut suins = suins::init_for_testing(scenario.ctx()); + suins::authorize_app_for_testing(&mut suins); + suins::share_for_testing(suins); + let clock = clock::create_for_testing(scenario.ctx()); + clock::share_for_testing(clock); + }; + { + scenario.next_tx(ADMIN_ADDRESS); + // get admin cap + let admin_cap = scenario.take_from_sender(); let mut suins = scenario.take_shared(); - let cap = scenario.take_from_sender(); - coupon_house::admin_add_coupon( - &cap, - &mut suins, - code_name, - kind, - value, - rules::new_empty_rules(), - scenario.ctx() - ); - scenario.return_to_sender(cap); + // initialize coupon data. + coupon_house::setup(&mut suins, &admin_cap, scenario.ctx()); + registry::init_for_testing(&admin_cap, &mut suins, scenario.ctx()); + // authorize TestApp to CouponHouse. + coupon_house::authorize_app(&admin_cap, &mut suins); + test_scenario::return_to_sender(scenario, admin_cap); test_scenario::return_shared(suins); - } - // Adds a 0 rule coupon that gives 15% discount to test admin additions. - public fun admin_remove_coupon(code_name: String, scenario: &mut Scenario) { - scenario.next_tx(admin()); - let mut suins = scenario.take_shared(); - let cap = scenario.take_from_sender(); - coupon_house::admin_remove_coupon( - &cap, - &mut suins, - code_name - ); - scenario.return_to_sender(cap); - test_scenario::return_shared(suins); - } - - // Internal helper that tries to claim a name using a coupon. - // Test prices are: - // 3 digit -> 1200 - // 4 digit -> 200 - // 5 digit -> 50 - // A helper to easily register a name with a coupon code. - public fun register_with_coupon(coupon_code: String, domain_name: String, no_years: u8, amount: u64, clock_value: u64, user: address, scenario: &mut Scenario) { - scenario.next_tx(user); - let mut clock = scenario.take_shared(); - clock.increment_for_testing(clock_value); - let mut suins = scenario.take_shared(); + }; +} - let payment = coin::mint_for_testing(amount, scenario.ctx()); +public fun admin(): address { + ADMIN_ADDRESS +} - let nft = coupon_house::register_with_coupon( - &mut suins, - coupon_code, - domain_name, - no_years, - payment, - &clock, - scenario.ctx() - ); +public fun user(): address { + USER_ADDRESS +} - transfer::public_transfer(nft, user); - test_scenario::return_shared(suins); - test_scenario::return_shared(clock); - } +public fun user_two(): address { + USER_2_ADDRESS +} + +public fun mist_per_sui(): u64 { + MIST_PER_SUI +} + +// global getters. + +public fun test_app(): TestApp { + TestApp {} +} + +public fun unauthorized_test_app(): UnauthorizedTestApp { + UnauthorizedTestApp {} +} + +/// A helper to add a bunch of coupons (with different setups) that we can use +/// on the coupon tests. +public fun populate_coupons(data_mut: &mut Data, ctx: &mut TxContext) { + // 5 SUI DISCOUNT, ONLY CLAIMABLE TWICE + coupon_house::app_add_coupon( + data_mut, + utf8(b"5_SUI_DISCOUNT"), + constants::fixed_price_discount_type(), + 5 * MIST_PER_SUI, + rules::new_coupon_rules( + option::none(), + option::some(2), + option::none(), + option::none(), + option::none(), + ), + ctx, + ); + + // 25% DISCOUNT, ONLY FOR 2 YEARS OR LESS REGISTRATIONS + coupon_house::app_add_coupon( + data_mut, + utf8(b"25_PERCENT_DISCOUNT_MAX_2_YEARS"), + constants::percentage_discount_type(), + 25, // 25% + rules::new_coupon_rules( + option::none(), + option::none(), + option::none(), + option::none(), + option::some(range::new(1, 2)), + ), + ctx, + ); + + // 25% DISCOUNT, only claimable ONCE by a specific user + coupon_house::app_add_coupon( + data_mut, + utf8(b"25_PERCENT_DISCOUNT_USER_ONLY"), + constants::percentage_discount_type(), + 25, // 25% + rules::new_coupon_rules( + option::none(), + option::some(1), + option::some(user()), + option::none(), + option::none(), + ), + ctx, + ); + + // 50% DISCOUNT, only claimable only for names > 5 digits + coupon_house::app_add_coupon( + data_mut, + utf8(b"50_PERCENT_5_PLUS_NAMES"), + constants::percentage_discount_type(), + 50, // 25% + rules::new_coupon_rules( + option::some(range::new(5, 63)), + option::some(1), + option::none(), + option::none(), + option::none(), + ), + ctx, + ); + + // 50% DISCOUNT, only for 3 digit names + coupon_house::app_add_coupon( + data_mut, + utf8(b"50_PERCENT_3_DIGITS"), + constants::percentage_discount_type(), + 50, // 50% + rules::new_coupon_rules( + option::some(range::new(3, 3)), + option::none(), + option::none(), + option::some(1), + option::none(), + ), + ctx, + ); + + // 50% DISCOUNT, has all rules so we can test combinations! + coupon_house::app_add_coupon( + data_mut, + utf8(b"50_DISCOUNT_SALAD"), + constants::percentage_discount_type(), + 50, // 50% + rules::new_coupon_rules( + option::some(range::new(3, 4)), + option::some(1), + option::some(user()), + option::some(1), + option::some(range::new(1, 2)), + ), + ctx, + ); + + // THESE last two are just for easy coverage. + // We just add + remove the coupon immediately. + coupon_house::app_add_coupon( + data_mut, + utf8(b"REMOVE_FOR_COVERAGE"), + constants::percentage_discount_type(), + 50, + rules::new_empty_rules(), + ctx, + ); + coupon_house::app_remove_coupon(data_mut, utf8(b"REMOVE_FOR_COVERAGE")); +} + +// Adds a 0 rule coupon that gives 15% discount to test admin additions. +public fun admin_add_coupon( + code_name: String, + kind: u8, + value: u64, + scenario: &mut Scenario, +) { + scenario.next_tx(admin()); + let mut suins = scenario.take_shared(); + let cap = scenario.take_from_sender(); + coupon_house::admin_add_coupon( + &cap, + &mut suins, + code_name, + kind, + value, + rules::new_empty_rules(), + scenario.ctx(), + ); + scenario.return_to_sender(cap); + test_scenario::return_shared(suins); +} + +// Adds a 0 rule coupon that gives 15% discount to test admin additions. +public fun admin_remove_coupon(code_name: String, scenario: &mut Scenario) { + scenario.next_tx(admin()); + let mut suins = scenario.take_shared(); + let cap = scenario.take_from_sender(); + coupon_house::admin_remove_coupon( + &cap, + &mut suins, + code_name, + ); + scenario.return_to_sender(cap); + test_scenario::return_shared(suins); +} + +// Internal helper that tries to claim a name using a coupon. +// Test prices are: +// 3 digit -> 1200 +// 4 digit -> 200 +// 5 digit -> 50 +// A helper to easily register a name with a coupon code. +public fun register_with_coupon( + coupon_code: String, + domain_name: String, + no_years: u8, + amount: u64, + clock_value: u64, + user: address, + scenario: &mut Scenario, +) { + scenario.next_tx(user); + let mut clock = scenario.take_shared(); + clock.increment_for_testing(clock_value); + let mut suins = scenario.take_shared(); + + let payment = coin::mint_for_testing(amount, scenario.ctx()); + + let nft = coupon_house::register_with_coupon( + &mut suins, + coupon_code, + domain_name, + no_years, + payment, + &clock, + scenario.ctx(), + ); + + transfer::public_transfer(nft, user); + test_scenario::return_shared(suins); + test_scenario::return_shared(clock); } diff --git a/packages/discounts/sources/discounts.move b/packages/discounts/sources/discounts.move index caf77eb7..1ad5e091 100644 --- a/packages/discounts/sources/discounts.move +++ b/packages/discounts/sources/discounts.move @@ -1,142 +1,178 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -/// A module that allows purchasing names in a different price by presenting a reference of type T. +/// A module that allows purchasing names in a different price by presenting a +/// reference of type T. /// Each `T` can have a separate configuration for a discount percentage. /// If a `T` doesn't exist, registration will fail. /// /// Can be called only when promotions are active for a specific type T. /// Activation / deactivation happens through PTBs. -module discounts::discounts { - use std::{string::String, type_name::{Self as `type`}}; - - use sui::{dynamic_field::{Self as df}, clock::{Clock}, coin::Coin, sui::SUI}; - - use suins::{domain::{Self}, suins::{Self, AdminCap, SuiNS}, suins_registration::SuinsRegistration}; - - // The base shared object. - use discounts::house::{Self, DiscountHouse}; - - use day_one::day_one::{DayOne, is_active}; - - /// A configuration already exists - const EConfigExists: u64 = 1; - /// A configuration doesn't exist - const EConfigNotExists: u64 = 2; - /// Invalid payment value - const EIncorrectAmount: u64 = 3; - /// Tries to use DayOne on regular register flow. - const ENotValidForDayOne: u64 = 4; - /// Tries to claim with a non active DayOne - const ENotActiveDayOne: u64 = 5; - - /// A key that opens up discounts for type T. - public struct DiscountKey has copy, store, drop {} - - /// The Discount config for type T. - /// We save the sale price for each letter configuration (3 chars, 4 chars, 5+ chars) - public struct DiscountConfig has copy, store, drop { - three_char_price: u64, - four_char_price: u64, - five_plus_char_price: u64, - } - - /// A function to register a name with a discount using type `T`. - public fun register( - self: &mut DiscountHouse, - suins: &mut SuiNS, - _: &T, - domain_name: String, - payment: Coin, - clock: &Clock, - _reseller: Option, - ctx: &mut TxContext - ): SuinsRegistration { - // For normal flow, we do not allow DayOne to be used. - // DayOne can only be used on `register_with_day_one` function. - assert!(`type`::into_string(`type`::get()) != `type`::into_string(`type`::get()), ENotValidForDayOne); - internal_register_name(self, suins, domain_name, payment, clock, ctx) - } - - /// A special function for DayOne registration. - /// We separate it from the normal registration flow because we only want it to be usable - /// for activated DayOnes. - public fun register_with_day_one( - self: &mut DiscountHouse, - suins: &mut SuiNS, - day_one: &DayOne, - domain_name: String, - payment: Coin, - clock: &Clock, - _reseller: Option, - ctx: &mut TxContext - ): SuinsRegistration { - assert!(is_active(day_one), ENotActiveDayOne); - internal_register_name(self, suins, domain_name, payment, clock, ctx) - } - - /// Calculate the price of a label. - public fun calculate_price(self: &DiscountConfig, length: u8): u64 { - - let price = if (length == 3) { - self.three_char_price - } else if (length == 4) { - self.four_char_price - } else { - self.five_plus_char_price - }; - - price - } - - /// An admin action to authorize a type T for special pricing. - public fun authorize_type( - _: &AdminCap, - self: &mut DiscountHouse, - three_char_price: u64, - four_char_price: u64, - five_plus_char_price: u64 - ) { - self.assert_version_is_valid(); - assert!(!df::exists_(house::uid_mut(self), DiscountKey {}), EConfigExists); - - df::add(house::uid_mut(self), DiscountKey{}, DiscountConfig { +module discounts::discounts; + +use day_one::day_one::{DayOne, is_active}; +use discounts::house::{Self, DiscountHouse}; +use std::string::String; +use std::type_name as `type`; +use sui::clock::Clock; +use sui::coin::Coin; +use sui::dynamic_field as df; +use sui::sui::SUI; +use suins::domain; +use suins::suins::{Self, AdminCap, SuiNS}; +use suins::suins_registration::SuinsRegistration; + +/// A configuration already exists +const EConfigExists: u64 = 1; +/// A configuration doesn't exist +const EConfigNotExists: u64 = 2; +/// Invalid payment value +const EIncorrectAmount: u64 = 3; +/// Tries to use DayOne on regular register flow. +const ENotValidForDayOne: u64 = 4; +/// Tries to claim with a non active DayOne +const ENotActiveDayOne: u64 = 5; + +/// A key that opens up discounts for type T. +public struct DiscountKey has copy, store, drop {} + +/// The Discount config for type T. +/// We save the sale price for each letter configuration (3 chars, 4 chars, 5+ +/// chars) +public struct DiscountConfig has copy, store, drop { + three_char_price: u64, + four_char_price: u64, + five_plus_char_price: u64, +} + +/// A function to register a name with a discount using type `T`. +public fun register( + self: &mut DiscountHouse, + suins: &mut SuiNS, + _: &T, + domain_name: String, + payment: Coin, + clock: &Clock, + _reseller: Option, + ctx: &mut TxContext, +): SuinsRegistration { + // For normal flow, we do not allow DayOne to be used. + // DayOne can only be used on `register_with_day_one` function. + assert!( + `type`::into_string(`type`::get()) != `type`::into_string(`type`::get()), + ENotValidForDayOne, + ); + internal_register_name(self, suins, domain_name, payment, clock, ctx) +} + +/// A special function for DayOne registration. +/// We separate it from the normal registration flow because we only want it to +/// be usable +/// for activated DayOnes. +public fun register_with_day_one( + self: &mut DiscountHouse, + suins: &mut SuiNS, + day_one: &DayOne, + domain_name: String, + payment: Coin, + clock: &Clock, + _reseller: Option, + ctx: &mut TxContext, +): SuinsRegistration { + assert!(is_active(day_one), ENotActiveDayOne); + internal_register_name( + self, + suins, + domain_name, + payment, + clock, + ctx, + ) +} + +/// Calculate the price of a label. +public fun calculate_price(self: &DiscountConfig, length: u8): u64 { + let price = if (length == 3) { + self.three_char_price + } else if (length == 4) { + self.four_char_price + } else { + self.five_plus_char_price + }; + + price +} + +/// An admin action to authorize a type T for special pricing. +public fun authorize_type( + _: &AdminCap, + self: &mut DiscountHouse, + three_char_price: u64, + four_char_price: u64, + five_plus_char_price: u64, +) { + self.assert_version_is_valid(); + assert!( + !df::exists_(house::uid_mut(self), DiscountKey {}), + EConfigExists, + ); + + df::add( + house::uid_mut(self), + DiscountKey {}, + DiscountConfig { three_char_price, four_char_price, - five_plus_char_price - }); - } - - /// An admin action to deauthorize type T from getting discounts. - public fun deauthorize_type(_: &AdminCap, self: &mut DiscountHouse) { - self.assert_version_is_valid(); - assert_config_exists(self); - df::remove, DiscountConfig>(self.uid_mut(), DiscountKey{}); - } - - /// Internal helper to handle the registration process - fun internal_register_name( - self: &mut DiscountHouse, - suins: &mut SuiNS, - domain_name: String, - payment: Coin, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - self.assert_version_is_valid(); - // validate that there's a configuration for type T. - assert_config_exists(self); - - let domain = domain::new(domain_name); - let price = calculate_price(df::borrow(self.uid_mut(), DiscountKey{}), (domain.sld().length() as u8)); - - assert!(payment.value() == price, EIncorrectAmount); - suins::app_add_balance(house::suins_app_auth(), suins, payment.into_balance()); - - house::friend_add_registry_entry(suins, domain, clock, ctx) - } - - fun assert_config_exists(self: &mut DiscountHouse) { - assert!(df::exists_with_type, DiscountConfig>(house::uid_mut(self), DiscountKey {}), EConfigNotExists); - } + five_plus_char_price, + }, + ); +} + +/// An admin action to deauthorize type T from getting discounts. +public fun deauthorize_type(_: &AdminCap, self: &mut DiscountHouse) { + self.assert_version_is_valid(); + assert_config_exists(self); + df::remove, DiscountConfig>( + self.uid_mut(), + DiscountKey {}, + ); +} + +/// Internal helper to handle the registration process +fun internal_register_name( + self: &mut DiscountHouse, + suins: &mut SuiNS, + domain_name: String, + payment: Coin, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + self.assert_version_is_valid(); + // validate that there's a configuration for type T. + assert_config_exists(self); + + let domain = domain::new(domain_name); + let price = calculate_price( + df::borrow(self.uid_mut(), DiscountKey {}), + (domain.sld().length() as u8), + ); + + assert!(payment.value() == price, EIncorrectAmount); + suins::app_add_balance( + house::suins_app_auth(), + suins, + payment.into_balance(), + ); + + house::friend_add_registry_entry(suins, domain, clock, ctx) +} + +fun assert_config_exists(self: &mut DiscountHouse) { + assert!( + df::exists_with_type, DiscountConfig>( + house::uid_mut(self), + DiscountKey {}, + ), + EConfigNotExists, + ); } diff --git a/packages/discounts/sources/free_claims.move b/packages/discounts/sources/free_claims.move index 154889a6..fe12135c 100644 --- a/packages/discounts/sources/free_claims.move +++ b/packages/discounts/sources/free_claims.move @@ -1,179 +1,219 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -/// A module that allows claiming names of a set length for free by presenting an object T. +/// A module that allows claiming names of a set length for free by presenting +/// an object T. /// Each `T` can have a separate configuration for a discount percentage. /// If a `T` doesn't exist, registration will fail. /// /// Can be called only when promotions are active for a specific type T. /// Activation / deactivation happens through PTBs. -module discounts::free_claims { - - use std::{string::{String}, type_name::{Self as `type`}}; - use sui::{dynamic_field::{Self as df}, clock::{Clock}, linked_table::{Self, LinkedTable}}; - - use suins::{domain::{Self, Domain}, suins::{AdminCap, SuiNS}, suins_registration::SuinsRegistration}; - - use discounts::house::{Self, DiscountHouse}; - - use day_one::day_one::{DayOne, is_active}; - - /// A configuration already exists - const EConfigExists: u64 = 1; - /// A configuration doesn't exist - const EConfigNotExists: u64 = 2; - /// Invalid length array - const EInvalidCharacterRange: u64 = 3; - /// Object has already been used in this promotion. - const EAlreadyClaimed: u64 = 4; - /// Tries to use DayOne on regular register flow. - const ENotValidForDayOne: u64 = 5; - /// Tries to claim with a non active DayOne - const ENotActiveDayOne: u64 = 6; - - /// A key to authorize DiscountHouse to register names on SuiNS. - public struct FreeClaimsApp has drop {} - - /// A key that opens up free claims for type T. - public struct FreeClaimsKey has copy, store, drop {} - - /// We hold the configuration for the promotion - /// We only allow 1 claim / per configuration / per promotion. - /// We keep the used ids as a LinkedTable so we can get our rebates when closing the promotion. - public struct FreeClaimsConfig has store { - domain_length_range: vector, - used_objects: LinkedTable - } - - /// A function to register a name with a discount using type `T`. - public fun free_claim( - self: &mut DiscountHouse, - suins: &mut SuiNS, - object: &T, - domain_name: String, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - // For normal flow, we do not allow DayOne to be used. - // DayOne can only be used on `register_with_day_one` function. - assert!(`type`::into_string(`type`::get()) != `type`::into_string(`type`::get()), ENotValidForDayOne); - - internal_claim_free_name(self, suins, domain_name, clock, object, ctx) - } - - // A function to register a free name using `DayOne`. - public fun free_claim_with_day_one( - self: &mut DiscountHouse, - suins: &mut SuiNS, - day_one: &DayOne, - domain_name: String, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - assert!(is_active(day_one), ENotActiveDayOne); - internal_claim_free_name(self, suins, domain_name, clock, day_one, ctx) - } - - - /// Internal helper that checks if there's a valid configuration for T, - /// validates that the domain name is of vlaid length, and then does the registration. - fun internal_claim_free_name( - self: &mut DiscountHouse, - suins: &mut SuiNS, - domain_name: String, - clock: &Clock, - object: &T, - ctx: &mut TxContext - ): SuinsRegistration { - self.assert_version_is_valid(); - // validate that there's a configuration for type T. - assert_config_exists(self); - - // We only allow one free registration per object. - // We shall check the id hasn't been used before first. - let id = object::id(object); - - // validate that the supplied object hasn't been used to claim a free name. - let config = df::borrow_mut, FreeClaimsConfig>(self.uid_mut(), FreeClaimsKey{}); - assert!(!config.used_objects.contains(id), EAlreadyClaimed); - - // add the supplied object's id to the used objects list. - config.used_objects.push_back(id, true); - - // Now validate the domain, and that the rule applies here. - let domain = domain::new(domain_name); - assert_domain_length_eligible(&domain, config); - - house::friend_add_registry_entry(suins, domain, clock, ctx) - } - - /// An admin action to authorize a type T for free claiming of names by presenting - /// an object of type `T`. - public fun authorize_type( - _: &AdminCap, - self: &mut DiscountHouse, - domain_length_range: vector, - ctx: &mut TxContext - ) { - self.assert_version_is_valid(); - assert!(!df::exists_(self.uid_mut(), FreeClaimsKey {}), EConfigExists); - - // validate the range is valid. - assert_valid_length_setup(&domain_length_range); - - df::add(self.uid_mut(), FreeClaimsKey{}, FreeClaimsConfig { +module discounts::free_claims; + +use day_one::day_one::{DayOne, is_active}; +use discounts::house::{Self, DiscountHouse}; +use std::string::String; +use std::type_name as `type`; +use sui::clock::Clock; +use sui::dynamic_field as df; +use sui::linked_table::{Self, LinkedTable}; +use suins::domain::{Self, Domain}; +use suins::suins::{AdminCap, SuiNS}; +use suins::suins_registration::SuinsRegistration; + +/// A configuration already exists +const EConfigExists: u64 = 1; +/// A configuration doesn't exist +const EConfigNotExists: u64 = 2; +/// Invalid length array +const EInvalidCharacterRange: u64 = 3; +/// Object has already been used in this promotion. +const EAlreadyClaimed: u64 = 4; +/// Tries to use DayOne on regular register flow. +const ENotValidForDayOne: u64 = 5; +/// Tries to claim with a non active DayOne +const ENotActiveDayOne: u64 = 6; + +/// A key to authorize DiscountHouse to register names on SuiNS. +public struct FreeClaimsApp has drop {} + +/// A key that opens up free claims for type T. +public struct FreeClaimsKey has copy, store, drop {} + +/// We hold the configuration for the promotion +/// We only allow 1 claim / per configuration / per promotion. +/// We keep the used ids as a LinkedTable so we can get our rebates when closing +/// the promotion. +public struct FreeClaimsConfig has store { + domain_length_range: vector, + used_objects: LinkedTable, +} + +/// A function to register a name with a discount using type `T`. +public fun free_claim( + self: &mut DiscountHouse, + suins: &mut SuiNS, + object: &T, + domain_name: String, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + // For normal flow, we do not allow DayOne to be used. + // DayOne can only be used on `register_with_day_one` function. + assert!( + `type`::into_string(`type`::get()) != `type`::into_string(`type`::get()), + ENotValidForDayOne, + ); + + internal_claim_free_name(self, suins, domain_name, clock, object, ctx) +} + +// A function to register a free name using `DayOne`. +public fun free_claim_with_day_one( + self: &mut DiscountHouse, + suins: &mut SuiNS, + day_one: &DayOne, + domain_name: String, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + assert!(is_active(day_one), ENotActiveDayOne); + internal_claim_free_name( + self, + suins, + domain_name, + clock, + day_one, + ctx, + ) +} + +/// Internal helper that checks if there's a valid configuration for T, +/// validates that the domain name is of vlaid length, and then does the +/// registration. +fun internal_claim_free_name( + self: &mut DiscountHouse, + suins: &mut SuiNS, + domain_name: String, + clock: &Clock, + object: &T, + ctx: &mut TxContext, +): SuinsRegistration { + self.assert_version_is_valid(); + // validate that there's a configuration for type T. + assert_config_exists(self); + + // We only allow one free registration per object. + // We shall check the id hasn't been used before first. + let id = object::id(object); + + // validate that the supplied object hasn't been used to claim a free name. + let config = df::borrow_mut, FreeClaimsConfig>( + self.uid_mut(), + FreeClaimsKey {}, + ); + assert!(!config.used_objects.contains(id), EAlreadyClaimed); + + // add the supplied object's id to the used objects list. + config.used_objects.push_back(id, true); + + // Now validate the domain, and that the rule applies here. + let domain = domain::new(domain_name); + assert_domain_length_eligible(&domain, config); + + house::friend_add_registry_entry(suins, domain, clock, ctx) +} + +/// An admin action to authorize a type T for free claiming of names by +/// presenting +/// an object of type `T`. +public fun authorize_type( + _: &AdminCap, + self: &mut DiscountHouse, + domain_length_range: vector, + ctx: &mut TxContext, +) { + self.assert_version_is_valid(); + assert!(!df::exists_(self.uid_mut(), FreeClaimsKey {}), EConfigExists); + + // validate the range is valid. + assert_valid_length_setup(&domain_length_range); + + df::add( + self.uid_mut(), + FreeClaimsKey {}, + FreeClaimsConfig { domain_length_range, - used_objects: linked_table::new(ctx) - }); - } - - /// An admin action to deauthorize type T from getting discounts. - /// Deauthorization also brings storage rebates by destroying the table of used objects. - /// If we re-authorize a type, objects can be re-used, but that's considered a separate promotion. - public fun deauthorize_type(_: &AdminCap, self: &mut DiscountHouse) { - self.assert_version_is_valid(); - assert_config_exists(self); - let FreeClaimsConfig { mut used_objects, domain_length_range: _ } = df::remove, FreeClaimsConfig>(self.uid_mut(), FreeClaimsKey{}); - - // parse each entry and remove it. Gives us storage rebates. - while(used_objects.length() > 0) { - used_objects.pop_front(); - }; - - used_objects.destroy_empty(); - } - - /// Worried by the 1000 DFs load limit, I introduce a `drop_type` function now - /// to make sure we can force-finish a promotion for type `T`. - public fun force_deauthorize_type(_: &AdminCap, self: &mut DiscountHouse) { - self.assert_version_is_valid(); - assert_config_exists(self); - let FreeClaimsConfig { used_objects, domain_length_range: _ } = df::remove, FreeClaimsConfig>(self.uid_mut(), FreeClaimsKey{}); - used_objects.drop(); - } - - // Validate that there is a config for `T` - fun assert_config_exists(self: &mut DiscountHouse) { - assert!(df::exists_with_type, FreeClaimsConfig>(self.uid_mut(), FreeClaimsKey {}), EConfigNotExists); - } - - /// Validate that the domain length is valid for the passed configuration. - fun assert_domain_length_eligible(domain: &Domain, config: &FreeClaimsConfig) { - let domain_length = (domain.sld().length() as u8); - let from = config.domain_length_range[0]; - let to = config.domain_length_range[1]; - - assert!(domain_length >= from && domain_length <= to, EInvalidCharacterRange); - } - - - // Validate that our range setup is right. - fun assert_valid_length_setup(domain_length_range: &vector) { - assert!(domain_length_range.length() == 2, EInvalidCharacterRange); - - let from = domain_length_range[0]; - let to = domain_length_range[1]; - - assert!(to >= from, EInvalidCharacterRange); - } + used_objects: linked_table::new(ctx), + }, + ); +} + +/// An admin action to deauthorize type T from getting discounts. +/// Deauthorization also brings storage rebates by destroying the table of used +/// objects. +/// If we re-authorize a type, objects can be re-used, but that's considered a +/// separate promotion. +public fun deauthorize_type(_: &AdminCap, self: &mut DiscountHouse) { + self.assert_version_is_valid(); + assert_config_exists(self); + let FreeClaimsConfig { + mut used_objects, + domain_length_range: _, + } = df::remove, FreeClaimsConfig>( + self.uid_mut(), + FreeClaimsKey {}, + ); + + // parse each entry and remove it. Gives us storage rebates. + while (used_objects.length() > 0) { + used_objects.pop_front(); + }; + + used_objects.destroy_empty(); +} + +/// Worried by the 1000 DFs load limit, I introduce a `drop_type` function now +/// to make sure we can force-finish a promotion for type `T`. +public fun force_deauthorize_type(_: &AdminCap, self: &mut DiscountHouse) { + self.assert_version_is_valid(); + assert_config_exists(self); + let FreeClaimsConfig { used_objects, domain_length_range: _ } = df::remove< + FreeClaimsKey, + FreeClaimsConfig, + >(self.uid_mut(), FreeClaimsKey {}); + used_objects.drop(); +} + +// Validate that there is a config for `T` +fun assert_config_exists(self: &mut DiscountHouse) { + assert!( + df::exists_with_type, FreeClaimsConfig>( + self.uid_mut(), + FreeClaimsKey {}, + ), + EConfigNotExists, + ); +} + +/// Validate that the domain length is valid for the passed configuration. +fun assert_domain_length_eligible(domain: &Domain, config: &FreeClaimsConfig) { + let domain_length = (domain.sld().length() as u8); + let from = config.domain_length_range[0]; + let to = config.domain_length_range[1]; + + assert!( + domain_length >= from && domain_length <= to, + EInvalidCharacterRange, + ); +} + +// Validate that our range setup is right. +fun assert_valid_length_setup(domain_length_range: &vector) { + assert!(domain_length_range.length() == 2, EInvalidCharacterRange); + + let from = domain_length_range[0]; + let to = domain_length_range[1]; + + assert!(to >= from, EInvalidCharacterRange); } diff --git a/packages/discounts/sources/house.move b/packages/discounts/sources/house.move index 1af58811..bd00e58f 100644 --- a/packages/discounts/sources/house.move +++ b/packages/discounts/sources/house.move @@ -1,95 +1,94 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -/// A base module that holds a shared object for the configuration of the package +/// A base module that holds a shared object for the configuration of the +/// package /// and exports some package utilities for the 2 systems to use. -module discounts::house { - - use sui::clock::{Clock}; - - use suins::{ - domain::Domain, - registry::Registry, - suins::{Self, AdminCap, SuiNS}, - config, - suins_registration::SuinsRegistration - }; - - // The `free_claims` module can use the shared object to attach configuration & claim names. - /* friend discounts::free_claims; */ - // The `discounts` module can use the shared object to attach configuration & claim names. - /* friend discounts::discounts; */ - - /// Tries to register with invalid version of the app - const ENotValidVersion: u64 = 1; - - /// A version handler that allows us to upgrade the app in the future. - const VERSION: u8 = 1; - - /// All promotions in this package are valid only for 1 year - const REGISTRATION_YEARS: u8 = 1; - - /// A key to authorize DiscountHouse to register names on SuiNS. - public struct DiscountHouseApp has drop {} - - // The Shared object responsible for the discounts. - public struct DiscountHouse has key, store { - id: UID, - version: u8 - } - - /// Share the house. - /// This will hold DFs with the configuration for different types. - fun init(ctx: &mut TxContext){ - transfer::public_share_object(DiscountHouse { - id: object::new(ctx), - version: VERSION - }) - } - - /// An admin helper to set the version of the shared object. - /// Registrations are only possible if the latest version is being used. - public fun set_version(_: &AdminCap, self: &mut DiscountHouse, version: u8) { - self.version = version; - } - - /// Validate that the version of the app is the latest. - public fun assert_version_is_valid(self: &DiscountHouse) { - assert!(self.version == VERSION, ENotValidVersion); - } - - /// A function to save a new SuiNS name in the registry. - /// Helps re-use the same code for all discounts based on type T of the package. - public(package) fun friend_add_registry_entry( - suins: &mut SuiNS, - domain: Domain, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - // Verify that app is authorized to register names. - suins.assert_app_is_authorized(); - - // Validate that the name can be registered. - config::assert_valid_user_registerable_domain(&domain); - - let registry = suins::app_registry_mut(DiscountHouseApp {}, suins); - registry.add_record(domain, REGISTRATION_YEARS, clock, ctx) - } - - /// Returns the UID of the shared object so we can add custom configuration. - /// from different modules we have. but keep using the same shared object. - public(package) fun uid_mut(self: &mut DiscountHouse): &mut UID { - &mut self.id - } - - /// Allows the friend modules to call functions to the SuiNS registry. - public(package) fun suins_app_auth(): DiscountHouseApp { - DiscountHouseApp {} - } - - #[test_only] - public fun init_for_testing(ctx: &mut TxContext) { - init(ctx); - } +module discounts::house; +use sui::clock::Clock; +use suins::config; +use suins::domain::Domain; +use suins::registry::Registry; +use suins::suins::{Self, AdminCap, SuiNS}; +use suins::suins_registration::SuinsRegistration; + +// The `free_claims` module can use the shared object to attach configuration & claim names. +/* friend discounts::free_claims; */ +// The `discounts` module can use the shared object to attach configuration & claim names. +/* friend discounts::discounts; */ + +/// Tries to register with invalid version of the app +const ENotValidVersion: u64 = 1; + +/// A version handler that allows us to upgrade the app in the future. +const VERSION: u8 = 1; + +/// All promotions in this package are valid only for 1 year +const REGISTRATION_YEARS: u8 = 1; + +/// A key to authorize DiscountHouse to register names on SuiNS. +public struct DiscountHouseApp has drop {} + +// The Shared object responsible for the discounts. +public struct DiscountHouse has key, store { + id: UID, + version: u8, +} + +/// Share the house. +/// This will hold DFs with the configuration for different types. +fun init(ctx: &mut TxContext) { + transfer::public_share_object(DiscountHouse { + id: object::new(ctx), + version: VERSION, + }) +} + +/// An admin helper to set the version of the shared object. +/// Registrations are only possible if the latest version is being used. +public fun set_version(_: &AdminCap, self: &mut DiscountHouse, version: u8) { + self.version = version; +} + +/// Validate that the version of the app is the latest. +public fun assert_version_is_valid(self: &DiscountHouse) { + assert!(self.version == VERSION, ENotValidVersion); +} + +/// A function to save a new SuiNS name in the registry. +/// Helps re-use the same code for all discounts based on type T of the package. +public(package) fun friend_add_registry_entry( + suins: &mut SuiNS, + domain: Domain, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + // Verify that app is authorized to register names. + suins.assert_app_is_authorized(); + + // Validate that the name can be registered. + config::assert_valid_user_registerable_domain(&domain); + + let registry = suins::app_registry_mut( + DiscountHouseApp {}, + suins, + ); + registry.add_record(domain, REGISTRATION_YEARS, clock, ctx) +} + +/// Returns the UID of the shared object so we can add custom configuration. +/// from different modules we have. but keep using the same shared object. +public(package) fun uid_mut(self: &mut DiscountHouse): &mut UID { + &mut self.id +} + +/// Allows the friend modules to call functions to the SuiNS registry. +public(package) fun suins_app_auth(): DiscountHouseApp { + DiscountHouseApp {} +} + +#[test_only] +public fun init_for_testing(ctx: &mut TxContext) { + init(ctx); } diff --git a/packages/discounts/tests/discount_tests.move b/packages/discounts/tests/discount_tests.move index 5a42c274..2c8e71fd 100644 --- a/packages/discounts/tests/discount_tests.move +++ b/packages/discounts/tests/discount_tests.move @@ -2,201 +2,258 @@ // SPDX-License-Identifier: Apache-2.0 #[test_only] -module discounts::discount_tests { - use std::string::{utf8, String}; - - use sui::{test_scenario::{Self as ts, Scenario, ctx}, clock::{Self, Clock}, coin::{Self, Coin}, sui::SUI}; - - use suins::{suins::{Self, SuiNS, AdminCap}, registry}; - - use discounts::{house::{Self, DiscountHouse, DiscountHouseApp}, discounts}; - - use day_one::day_one::{Self, DayOne}; - - // an authorized type to test. - public struct TestAuthorized has copy, store, drop {} - - // another authorized type to test. - public struct AnotherAuthorized has copy, store, drop {} - - // an unauthorized type to test. - public struct TestUnauthorized has copy, store, drop {} - - const SUINS_ADDRESS: address = @0xA001; - const USER_ADDRESS: address = @0xA002; - - const MIST_PER_SUI: u64 = 1_000_000_000; - - fun test_init(): Scenario { - let mut scenario_val = ts::begin(SUINS_ADDRESS); - let scenario = &mut scenario_val; - { - let mut suins = suins::init_for_testing(scenario.ctx()); - suins.authorize_app_for_testing(); - suins.share_for_testing(); - house::init_for_testing(scenario.ctx()); - let clock = clock::create_for_testing(scenario.ctx()); - clock.share_for_testing(); - }; - { - scenario.next_tx(SUINS_ADDRESS); - let admin_cap = scenario.take_from_sender(); - let mut suins = scenario.take_shared(); - let mut discount_house = scenario.take_shared(); - - // a more expensive alternative. - discounts::authorize_type(&admin_cap, &mut discount_house, 3*MIST_PER_SUI, 2*MIST_PER_SUI, 1*MIST_PER_SUI); - // a much cheaper price for another type. - discounts::authorize_type(&admin_cap, &mut discount_house, MIST_PER_SUI / 20, MIST_PER_SUI / 10, MIST_PER_SUI / 5); - discounts::authorize_type(&admin_cap, &mut discount_house, MIST_PER_SUI, MIST_PER_SUI, MIST_PER_SUI); - - registry::init_for_testing(&admin_cap, &mut suins, scenario.ctx()); - - ts::return_shared(discount_house); - ts::return_shared(suins); - ts::return_to_sender(scenario, admin_cap); - }; - scenario_val - } - - fun register_with_type( - item: &T, - scenario: &mut Scenario, - domain_name: String, - payment: Coin, - user: address - ) { - scenario.next_tx(user); +module discounts::discount_tests; + +use day_one::day_one::{Self, DayOne}; +use discounts::discounts; +use discounts::house::{Self, DiscountHouse, DiscountHouseApp}; +use std::string::{utf8, String}; +use sui::clock::{Self, Clock}; +use sui::coin::{Self, Coin}; +use sui::sui::SUI; +use sui::test_scenario::{Self as ts, Scenario, ctx}; +use suins::registry; +use suins::suins::{Self, SuiNS, AdminCap}; + +// an authorized type to test. +public struct TestAuthorized has copy, store, drop {} + +// another authorized type to test. +public struct AnotherAuthorized has copy, store, drop {} + +// an unauthorized type to test. +public struct TestUnauthorized has copy, store, drop {} + +const SUINS_ADDRESS: address = @0xA001; +const USER_ADDRESS: address = @0xA002; + +const MIST_PER_SUI: u64 = 1_000_000_000; + +fun test_init(): Scenario { + let mut scenario_val = ts::begin(SUINS_ADDRESS); + let scenario = &mut scenario_val; + { + let mut suins = suins::init_for_testing(scenario.ctx()); + suins.authorize_app_for_testing(); + suins.share_for_testing(); + house::init_for_testing(scenario.ctx()); + let clock = clock::create_for_testing(scenario.ctx()); + clock.share_for_testing(); + }; + { + scenario.next_tx(SUINS_ADDRESS); + let admin_cap = scenario.take_from_sender(); let mut suins = scenario.take_shared(); let mut discount_house = scenario.take_shared(); - let clock = scenario.take_shared(); - let name = discounts::register(&mut discount_house, &mut suins, item, domain_name, payment, &clock, option::none(), scenario.ctx()); - - transfer::public_transfer(name, user); - - ts::return_shared(discount_house); - ts::return_shared(suins); - ts::return_shared(clock); - } - - fun register_with_day_one( - item: &DayOne, - scenario: &mut Scenario, - domain_name: String, - payment: Coin, - user: address - ) { - scenario.next_tx(user); - let mut suins = scenario.take_shared(); - let mut discount_house = scenario.take_shared(); - let clock = scenario.take_shared(); - - let name = discounts::register_with_day_one(&mut discount_house, &mut suins, item, domain_name, payment, &clock, option::none(), scenario.ctx()); + // a more expensive alternative. + discounts::authorize_type( + &admin_cap, + &mut discount_house, + 3*MIST_PER_SUI, + 2*MIST_PER_SUI, + 1*MIST_PER_SUI, + ); + // a much cheaper price for another type. + discounts::authorize_type( + &admin_cap, + &mut discount_house, + MIST_PER_SUI / 20, + MIST_PER_SUI / 10, + MIST_PER_SUI / 5, + ); + discounts::authorize_type( + &admin_cap, + &mut discount_house, + MIST_PER_SUI, + MIST_PER_SUI, + MIST_PER_SUI, + ); - transfer::public_transfer(name, user); + registry::init_for_testing(&admin_cap, &mut suins, scenario.ctx()); ts::return_shared(discount_house); ts::return_shared(suins); - ts::return_shared(clock); - } - - #[test] - fun test_e2e() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - - let test_item = TestAuthorized {}; - let payment: Coin = coin::mint_for_testing(2*MIST_PER_SUI, scenario.ctx()); - - register_with_type( - &test_item, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS - ); - - scenario_val.end(); - } - - #[test, expected_failure(abort_code = ::discounts::discounts::EConfigNotExists)] - fun register_with_unauthorized_type() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - - let test_item = TestUnauthorized {}; - let payment: Coin = coin::mint_for_testing(2*MIST_PER_SUI, scenario.ctx()); + ts::return_to_sender(scenario, admin_cap); + }; + scenario_val +} - register_with_type( - &test_item, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS - ); - scenario_val.end(); - } - - #[test] - fun use_day_one(){ - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - - let mut day_one = day_one::mint_for_testing(scenario.ctx()); - day_one::set_is_active_for_testing(&mut day_one, true); - let payment: Coin = coin::mint_for_testing(MIST_PER_SUI, scenario.ctx()); - - register_with_day_one( - &day_one, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS - ); +fun register_with_type( + item: &T, + scenario: &mut Scenario, + domain_name: String, + payment: Coin, + user: address, +) { + scenario.next_tx(user); + let mut suins = scenario.take_shared(); + let mut discount_house = scenario.take_shared(); + let clock = scenario.take_shared(); + + let name = discounts::register( + &mut discount_house, + &mut suins, + item, + domain_name, + payment, + &clock, + option::none(), + scenario.ctx(), + ); + + transfer::public_transfer(name, user); + + ts::return_shared(discount_house); + ts::return_shared(suins); + ts::return_shared(clock); +} - day_one.burn_for_testing(); - scenario_val.end(); - } - - #[test, expected_failure(abort_code = ::discounts::discounts::ENotValidForDayOne)] - fun use_day_one_for_casual_flow_failure(){ - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - - let mut day_one = day_one::mint_for_testing(scenario.ctx()); - day_one::set_is_active_for_testing(&mut day_one, true); - let payment: Coin = coin::mint_for_testing(MIST_PER_SUI, scenario.ctx()); - - register_with_type( - &day_one, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS - ); +fun register_with_day_one( + item: &DayOne, + scenario: &mut Scenario, + domain_name: String, + payment: Coin, + user: address, +) { + scenario.next_tx(user); + let mut suins = scenario.take_shared(); + let mut discount_house = scenario.take_shared(); + let clock = scenario.take_shared(); + + let name = discounts::register_with_day_one( + &mut discount_house, + &mut suins, + item, + domain_name, + payment, + &clock, + option::none(), + scenario.ctx(), + ); + + transfer::public_transfer(name, user); + + ts::return_shared(discount_house); + ts::return_shared(suins); + ts::return_shared(clock); +} - day_one.burn_for_testing(); - scenario_val.end(); - } +#[test] +fun test_e2e() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + + let test_item = TestAuthorized {}; + let payment: Coin = coin::mint_for_testing( + 2*MIST_PER_SUI, + scenario.ctx(), + ); + + register_with_type( + &test_item, + scenario, + utf8(b"test.sui"), + payment, + USER_ADDRESS, + ); + + scenario_val.end(); +} - #[test, expected_failure(abort_code = ::discounts::discounts::ENotActiveDayOne)] - fun use_inactive_day_one_failure(){ - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; +#[test, expected_failure(abort_code = ::discounts::discounts::EConfigNotExists)] +fun register_with_unauthorized_type() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + + let test_item = TestUnauthorized {}; + let payment: Coin = coin::mint_for_testing( + 2*MIST_PER_SUI, + scenario.ctx(), + ); + + register_with_type( + &test_item, + scenario, + utf8(b"test.sui"), + payment, + USER_ADDRESS, + ); + scenario_val.end(); +} - let day_one = day_one::mint_for_testing(scenario.ctx()); - let payment: Coin = coin::mint_for_testing(MIST_PER_SUI, scenario.ctx()); +#[test] +fun use_day_one() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + day_one::set_is_active_for_testing(&mut day_one, true); + let payment: Coin = coin::mint_for_testing( + MIST_PER_SUI, + scenario.ctx(), + ); + + register_with_day_one( + &day_one, + scenario, + utf8(b"test.sui"), + payment, + USER_ADDRESS, + ); + + day_one.burn_for_testing(); + scenario_val.end(); +} - register_with_day_one( - &day_one, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS - ); +#[ + test, + expected_failure( + abort_code = ::discounts::discounts::ENotValidForDayOne, + ), +] +fun use_day_one_for_casual_flow_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + day_one::set_is_active_for_testing(&mut day_one, true); + let payment: Coin = coin::mint_for_testing( + MIST_PER_SUI, + scenario.ctx(), + ); + + register_with_type( + &day_one, + scenario, + utf8(b"test.sui"), + payment, + USER_ADDRESS, + ); + + day_one.burn_for_testing(); + scenario_val.end(); +} - day_one.burn_for_testing(); - scenario_val.end(); - } +#[test, expected_failure(abort_code = ::discounts::discounts::ENotActiveDayOne)] +fun use_inactive_day_one_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + + let day_one = day_one::mint_for_testing(scenario.ctx()); + let payment: Coin = coin::mint_for_testing( + MIST_PER_SUI, + scenario.ctx(), + ); + + register_with_day_one( + &day_one, + scenario, + utf8(b"test.sui"), + payment, + USER_ADDRESS, + ); + + day_one.burn_for_testing(); + scenario_val.end(); } diff --git a/packages/discounts/tests/free_claims_test.move b/packages/discounts/tests/free_claims_test.move index a5f051c1..c495a8c7 100644 --- a/packages/discounts/tests/free_claims_test.move +++ b/packages/discounts/tests/free_claims_test.move @@ -2,220 +2,248 @@ // SPDX-License-Identifier: Apache-2.0 #[test_only] -module discounts::free_claims_tests { - - use std::string::{utf8, String}; - - use sui::{test_scenario::{Self as ts, Scenario, ctx}, clock::{Self, Clock}}; - - use suins::{suins::{Self, SuiNS, AdminCap}, registry}; - - use discounts::house::{Self, DiscountHouse, DiscountHouseApp}; - use discounts::free_claims; - - use day_one::day_one::{Self, DayOne}; - - // An authorized type to test. - public struct TestAuthorized has key, store { id: UID } - - // An unauthorized type to test. - public struct TestUnauthorized has key { id: UID } - - const SUINS_ADDRESS: address = @0xA001; - const USER_ADDRESS: address = @0xA002; - - fun test_init(): Scenario { - let mut scenario_val = ts::begin(SUINS_ADDRESS); - let scenario = &mut scenario_val; - { - let mut suins = suins::init_for_testing(scenario.ctx()); - suins.authorize_app_for_testing(); - suins.share_for_testing(); - house::init_for_testing(scenario.ctx()); - let clock = clock::create_for_testing(scenario.ctx()); - clock.share_for_testing(); - }; - { - ts::next_tx(scenario, SUINS_ADDRESS); - let admin_cap = scenario.take_from_sender(); - let mut suins = scenario.take_shared(); - let mut discount_house = scenario.take_shared(); - - // a more expensive alternative. - free_claims::authorize_type(&admin_cap, &mut discount_house, vector[10,63], scenario.ctx()); - free_claims::authorize_type(&admin_cap, &mut discount_house, vector[10,63], scenario.ctx()); - registry::init_for_testing(&admin_cap, &mut suins, scenario.ctx()); - - ts::return_shared(discount_house); - ts::return_shared(suins); - ts::return_to_sender(scenario, admin_cap); - }; - scenario_val - } - - fun test_end(mut scenario_val: Scenario) { - let scenario = &mut scenario_val; - { - ts::next_tx(scenario, SUINS_ADDRESS); - let admin_cap = scenario.take_from_sender(); - let mut discount_house = scenario.take_shared(); - free_claims::deauthorize_type(&admin_cap, &mut discount_house); - free_claims::deauthorize_type(&admin_cap, &mut discount_house); - ts::return_shared(discount_house); - ts::return_to_sender(scenario, admin_cap); - - }; - ts::end(scenario_val); - } - - fun burn_authorized(authorized: TestAuthorized) { - let TestAuthorized { id } = authorized; - id.delete(); - } - - fun free_claim_with_type( - item: &T, - scenario: &mut Scenario, - domain_name: String, - user: address - ) { - ts::next_tx(scenario, user); +module discounts::free_claims_tests; + +use day_one::day_one::{Self, DayOne}; +use discounts::free_claims; +use discounts::house::{Self, DiscountHouse, DiscountHouseApp}; +use std::string::{utf8, String}; +use sui::clock::{Self, Clock}; +use sui::test_scenario::{Self as ts, Scenario, ctx}; +use suins::registry; +use suins::suins::{Self, SuiNS, AdminCap}; + +// An authorized type to test. +public struct TestAuthorized has key, store { id: UID } + +// An unauthorized type to test. +public struct TestUnauthorized has key { id: UID } + +const SUINS_ADDRESS: address = @0xA001; +const USER_ADDRESS: address = @0xA002; + +fun test_init(): Scenario { + let mut scenario_val = ts::begin(SUINS_ADDRESS); + let scenario = &mut scenario_val; + { + let mut suins = suins::init_for_testing(scenario.ctx()); + suins.authorize_app_for_testing(); + suins.share_for_testing(); + house::init_for_testing(scenario.ctx()); + let clock = clock::create_for_testing(scenario.ctx()); + clock.share_for_testing(); + }; + { + ts::next_tx(scenario, SUINS_ADDRESS); + let admin_cap = scenario.take_from_sender(); let mut suins = scenario.take_shared(); let mut discount_house = scenario.take_shared(); - let clock = scenario.take_shared(); - let name = free_claims::free_claim(&mut discount_house, &mut suins, item, domain_name, &clock, scenario.ctx()); - - transfer::public_transfer(name, user); + // a more expensive alternative. + free_claims::authorize_type( + &admin_cap, + &mut discount_house, + vector[10, 63], + scenario.ctx(), + ); + free_claims::authorize_type( + &admin_cap, + &mut discount_house, + vector[10, 63], + scenario.ctx(), + ); + registry::init_for_testing(&admin_cap, &mut suins, scenario.ctx()); ts::return_shared(discount_house); ts::return_shared(suins); - ts::return_shared(clock); - } - - fun free_claim_with_day_one( - item: &DayOne, - scenario: &mut Scenario, - domain_name: String, - user: address - ) { - ts::next_tx(scenario, user); - let mut suins = ts::take_shared(scenario); - let mut discount_house = ts::take_shared(scenario); - let clock = ts::take_shared(scenario); - - let name = free_claims::free_claim_with_day_one(&mut discount_house, &mut suins, item, domain_name, &clock, scenario.ctx()); - - transfer::public_transfer(name, user); + ts::return_to_sender(scenario, admin_cap); + }; + scenario_val +} - ts::return_shared(discount_house); - ts::return_shared(suins); - ts::return_shared(clock); - } - - #[test] - fun test_e2e() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; - - let test_item = TestAuthorized { - id: object::new(scenario.ctx()) - }; - - free_claim_with_type( - &test_item, - scenario, - utf8(b"01234567890.sui"), - USER_ADDRESS +fun test_end(mut scenario_val: Scenario) { + let scenario = &mut scenario_val; + { + ts::next_tx(scenario, SUINS_ADDRESS); + let admin_cap = scenario.take_from_sender(); + let mut discount_house = scenario.take_shared(); + free_claims::deauthorize_type( + &admin_cap, + &mut discount_house, ); + free_claims::deauthorize_type(&admin_cap, &mut discount_house); + ts::return_shared(discount_house); + ts::return_to_sender(scenario, admin_cap); + }; + ts::end(scenario_val); +} - burn_authorized(test_item); - test_end(scenario_val); - } - - #[test] - fun use_day_one(){ - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; +fun burn_authorized(authorized: TestAuthorized) { + let TestAuthorized { id } = authorized; + id.delete(); +} - let mut day_one = day_one::mint_for_testing(scenario.ctx()); - day_one.set_is_active_for_testing(true); +fun free_claim_with_type( + item: &T, + scenario: &mut Scenario, + domain_name: String, + user: address, +) { + ts::next_tx(scenario, user); + let mut suins = scenario.take_shared(); + let mut discount_house = scenario.take_shared(); + let clock = scenario.take_shared(); + + let name = free_claims::free_claim( + &mut discount_house, + &mut suins, + item, + domain_name, + &clock, + scenario.ctx(), + ); + + transfer::public_transfer(name, user); + + ts::return_shared(discount_house); + ts::return_shared(suins); + ts::return_shared(clock); +} - free_claim_with_day_one( - &day_one, - scenario, - utf8(b"0123456789.sui"), - USER_ADDRESS - ); +fun free_claim_with_day_one( + item: &DayOne, + scenario: &mut Scenario, + domain_name: String, + user: address, +) { + ts::next_tx(scenario, user); + let mut suins = ts::take_shared(scenario); + let mut discount_house = ts::take_shared(scenario); + let clock = ts::take_shared(scenario); + + let name = free_claims::free_claim_with_day_one( + &mut discount_house, + &mut suins, + item, + domain_name, + &clock, + scenario.ctx(), + ); + + transfer::public_transfer(name, user); + + ts::return_shared(discount_house); + ts::return_shared(suins); + ts::return_shared(clock); +} - day_one.burn_for_testing(); - test_end(scenario_val); - } +#[test] +fun test_e2e() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; - #[test, expected_failure(abort_code = discounts::free_claims::EAlreadyClaimed)] - fun test_tries_to_claim_again_with_same_object_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; + let test_item = TestAuthorized { + id: object::new(scenario.ctx()), + }; - let test_item = TestAuthorized { - id: object::new(scenario.ctx()) - }; + free_claim_with_type( + &test_item, + scenario, + utf8(b"01234567890.sui"), + USER_ADDRESS, + ); - free_claim_with_type( - &test_item, - scenario, - utf8(b"01234567890.sui"), - USER_ADDRESS - ); + burn_authorized(test_item); + test_end(scenario_val); +} - // tries to claim again using the same test_item. - free_claim_with_type( - &test_item, - scenario, - utf8(b"01234567891.sui"), - USER_ADDRESS - ); +#[test] +fun use_day_one() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; - burn_authorized(test_item); - test_end(scenario_val); - } + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + day_one.set_is_active_for_testing(true); - #[test, expected_failure(abort_code = discounts::free_claims::EInvalidCharacterRange)] - fun test_invalid_size_failure() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; + free_claim_with_day_one( + &day_one, + scenario, + utf8(b"0123456789.sui"), + USER_ADDRESS, + ); - let test_item = TestAuthorized { - id: object::new(scenario.ctx()) - }; + day_one.burn_for_testing(); + test_end(scenario_val); +} - free_claim_with_type( - &test_item, - scenario, - utf8(b"012345678.sui"), - USER_ADDRESS - ); +#[test, expected_failure(abort_code = discounts::free_claims::EAlreadyClaimed)] +fun test_tries_to_claim_again_with_same_object_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + + let test_item = TestAuthorized { + id: object::new(scenario.ctx()), + }; + + free_claim_with_type( + &test_item, + scenario, + utf8(b"01234567890.sui"), + USER_ADDRESS, + ); + + // tries to claim again using the same test_item. + free_claim_with_type( + &test_item, + scenario, + utf8(b"01234567891.sui"), + USER_ADDRESS, + ); + + burn_authorized(test_item); + test_end(scenario_val); +} - burn_authorized(test_item); - test_end(scenario_val); - } +#[ + test, + expected_failure( + abort_code = discounts::free_claims::EInvalidCharacterRange, + ), +] +fun test_invalid_size_failure() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; + + let test_item = TestAuthorized { + id: object::new(scenario.ctx()), + }; + + free_claim_with_type( + &test_item, + scenario, + utf8(b"012345678.sui"), + USER_ADDRESS, + ); + + burn_authorized(test_item); + test_end(scenario_val); +} - #[test, expected_failure(abort_code = discounts::free_claims::EConfigNotExists)] - fun register_with_unauthorized_type() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; +#[test, expected_failure(abort_code = discounts::free_claims::EConfigNotExists)] +fun register_with_unauthorized_type() { + let mut scenario_val = test_init(); + let scenario = &mut scenario_val; - let test_item = TestUnauthorized { - id: object::new(scenario.ctx()) - }; + let test_item = TestUnauthorized { + id: object::new(scenario.ctx()), + }; - free_claim_with_type( - &test_item, - scenario, - utf8(b"test.sui"), - USER_ADDRESS - ); + free_claim_with_type( + &test_item, + scenario, + utf8(b"test.sui"), + USER_ADDRESS, + ); - abort 1337 - } + abort 1337 }