From 2dd5735af7ff9890129f3e7449cd68f7d38c9af0 Mon Sep 17 00:00:00 2001 From: Manolis Liolios Date: Tue, 7 Jan 2025 21:33:01 +0200 Subject: [PATCH] Our discounts revamped --- packages/discounts/sources/discounts.move | 198 ++++----- packages/discounts/sources/free_claims.move | 216 ++++------ packages/discounts/sources/house.move | 71 +--- packages/discounts/tests/discount_tests.move | 299 ++++++-------- .../discounts/tests/free_claims_test.move | 381 ++++++++++-------- 5 files changed, 527 insertions(+), 638 deletions(-) diff --git a/packages/discounts/sources/discounts.move b/packages/discounts/sources/discounts.move index 1ad5e091..599e07e7 100644 --- a/packages/discounts/sources/discounts.move +++ b/packages/discounts/sources/discounts.move @@ -12,167 +12,127 @@ 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 std::type_name; 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, -} +use suins::payment::PaymentIntent; +use suins::pricing_config::PricingConfig; +use suins::suins::{AdminCap, SuiNS}; + +use fun internal_apply_discount as DiscountHouse.internal_apply_discount; +use fun assert_config_exists as DiscountHouse.assert_config_exists; +use fun config as DiscountHouse.config; +use fun df::add as UID.add; +use fun df::exists_with_type as UID.exists_with_type; +use fun df::exists_ as UID.exists_; +use fun df::borrow as UID.borrow; + +#[error] +const EConfigAlreadyExists: vector = b"Config already exists"; +#[error] +const EConfigNotExists: vector = b"Config does not exist"; +#[error] +const EIncorrectAmount: vector = b"Incorrect amount"; +#[error] +const ENotActiveDayOne: vector = b"DayOne is not active"; +#[error] +const ENotValidForDayOne: vector = b"DayOne is not valid for this type"; + +/// A key allowing DiscountHouse to apply discounts. +public struct RegularDiscountsApp() has drop; + +/// A key that determins the discounts for a type `T`. +public struct DiscountKey() has copy, store, drop; /// A function to register a name with a discount using type `T`. -public fun register( +public fun apply_percentage_discount( self: &mut DiscountHouse, + intent: &mut PaymentIntent, suins: &mut SuiNS, - _: &T, - domain_name: String, - payment: Coin, - clock: &Clock, - _reseller: Option, + _: &mut T, // proof of owning the type T mutably. 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) +) { + // We can only use this discount for types other than DayOne, because we always check + // that the `DayOne` object is active. + assert!(type_name::get() != type_name::get(), ENotValidForDayOne); + + self.internal_apply_discount(intent, suins, 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( +public fun apply_day_one_discount( self: &mut DiscountHouse, + intent: &mut PaymentIntent, suins: &mut SuiNS, - day_one: &DayOne, - domain_name: String, - payment: Coin, - clock: &Clock, - _reseller: Option, + day_one: &mut DayOne, // proof of owning the type T mutably. 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 +) { + assert!(day_one.is_active(), ENotActiveDayOne); + self.internal_apply_discount(intent, suins, ctx); } /// An admin action to authorize a type T for special pricing. +/// +/// When authorizing, we reuse the core `PricingConfig` struct, +/// and only accept it if all the values are in the [0, 100] range. public fun authorize_type( - _: &AdminCap, self: &mut DiscountHouse, - three_char_price: u64, - four_char_price: u64, - five_plus_char_price: u64, + _: &AdminCap, + pricing_config: PricingConfig, ) { - self.assert_version_is_valid(); - assert!( - !df::exists_(house::uid_mut(self), DiscountKey {}), - EConfigExists, - ); + assert!(!self.uid_mut().exists_(DiscountKey()), EConfigAlreadyExists); + let (_, values) = (*pricing_config.pricing()).into_keys_values(); + // make sure that all the percentages are in the [0, 99] range. We can use + // `free_claims` to giveaway free names. + assert!(!values.any!(|percentage| *percentage > 99), EIncorrectAmount); - df::add( - house::uid_mut(self), - DiscountKey {}, - DiscountConfig { - three_char_price, - four_char_price, - five_plus_char_price, - }, - ); + self.uid_mut().add(DiscountKey(), pricing_config); } /// 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.assert_config_exists(); + df::remove<_, PricingConfig>( self.uid_mut(), - DiscountKey {}, + DiscountKey(), ); } -/// Internal helper to handle the registration process -fun internal_register_name( +fun internal_apply_discount( self: &mut DiscountHouse, + intent: &mut PaymentIntent, 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); + _ctx: &mut TxContext, +) { + let config = self.config(); - let domain = domain::new(domain_name); - let price = calculate_price( - df::borrow(self.uid_mut(), DiscountKey {}), - (domain.sld().length() as u8), - ); + let discount_percent = config.calculate_base_price(intent + .request_data() + .domain() + .sld() + .length()); - assert!(payment.value() == price, EIncorrectAmount); - suins::app_add_balance( - house::suins_app_auth(), + intent.apply_percentage_discount( suins, - payment.into_balance(), + RegularDiscountsApp(), + house::discount_house_key!(), + // SAFETY: We know that the discount percentage is in the [0, 99] range. + discount_percent as u8, + false, ); +} - house::friend_add_registry_entry(suins, domain, clock, ctx) +fun config(self: &mut DiscountHouse): &PricingConfig { + self.assert_config_exists(); + self.uid_mut().borrow<_, PricingConfig>(DiscountKey()) } fun assert_config_exists(self: &mut DiscountHouse) { assert!( - df::exists_with_type, DiscountConfig>( - house::uid_mut(self), - DiscountKey {}, - ), + self.uid_mut().exists_with_type<_, PricingConfig>(DiscountKey()), EConfigNotExists, ); } diff --git a/packages/discounts/sources/free_claims.move b/packages/discounts/sources/free_claims.move index fe12135c..bf943c1e 100644 --- a/packages/discounts/sources/free_claims.move +++ b/packages/discounts/sources/free_claims.move @@ -12,14 +12,21 @@ 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 std::type_name; use sui::dynamic_field as df; use sui::linked_table::{Self, LinkedTable}; -use suins::domain::{Self, Domain}; +use suins::payment::PaymentIntent; +use suins::pricing_config::Range; use suins::suins::{AdminCap, SuiNS}; -use suins::suins_registration::SuinsRegistration; + +use fun internal_apply_full_discount as DiscountHouse.internal_apply_full_discount; +use fun assert_config_exists as DiscountHouse.assert_config_exists; +use fun config_mut as DiscountHouse.config_mut; +use fun df::add as UID.add; +use fun df::exists_with_type as UID.exists_with_type; +use fun df::exists_ as UID.exists_; +use fun df::borrow_mut as UID.borrow_mut; +use fun df::remove as UID.remove; /// A configuration already exists const EConfigExists: u64 = 1; @@ -34,18 +41,18 @@ 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 allows DiscountHouse to apply free claims. +public struct FreeClaimsApp() has drop; /// A key that opens up free claims for type T. -public struct FreeClaimsKey has copy, store, drop {} +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, + domain_length_range: Range, used_objects: LinkedTable, } @@ -53,167 +60,112 @@ public struct FreeClaimsConfig has store { public fun free_claim( self: &mut DiscountHouse, suins: &mut SuiNS, + intent: &mut PaymentIntent, 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) + assert!(type_name::get() != type_name::get(), ENotValidForDayOne); + // Apply the discount. + self.internal_apply_full_discount(suins, intent, object::id(object), ctx); } // A function to register a free name using `DayOne`. public fun free_claim_with_day_one( self: &mut DiscountHouse, suins: &mut SuiNS, + intent: &mut PaymentIntent, 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, - ) +) { + assert!(day_one.is_active(), ENotActiveDayOne); + self.internal_apply_full_discount(suins, intent, object::id(day_one), 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( + self: &mut DiscountHouse, + _: &AdminCap, + domain_length_range: Range, + ctx: &mut TxContext, +) { + self.assert_version_is_valid(); + assert!(!self.uid_mut().exists_(FreeClaimsKey()), EConfigExists); + + self + .uid_mut() + .add( + FreeClaimsKey(), + FreeClaimsConfig { + domain_length_range, + used_objects: linked_table::new(ctx), + }, + ); +} + +/// Force-deauthorize type T from free claims. +/// Drops the linked_table. +public fun deauthorize_type(self: &mut DiscountHouse, _: &AdminCap): LinkedTable { + self.assert_version_is_valid(); + self.assert_config_exists(); + + let FreeClaimsConfig { used_objects, domain_length_range: _ } = self + .uid_mut() + .remove(FreeClaimsKey()); + + used_objects } /// 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( +fun internal_apply_full_discount( self: &mut DiscountHouse, suins: &mut SuiNS, - domain_name: String, - clock: &Clock, - object: &T, - ctx: &mut TxContext, -): SuinsRegistration { + intent: &mut PaymentIntent, + id: ID, + _ctx: &mut TxContext, +) { self.assert_version_is_valid(); - // validate that there's a configuration for type T. - assert_config_exists(self); + self.assert_config_exists(); + + let config = self.config_mut(); // 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), - }, + assert!( + config + .domain_length_range + .is_between_inclusive(intent.request_data().domain().sld().length()), + EInvalidCharacterRange, ); -} -/// 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 {}, + // applies 100% discount to the intent (so payment cost becomes 0). + intent.apply_percentage_discount( + suins, + FreeClaimsApp(), + house::discount_house_key!(), + 100, + false, ); - - // 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(); +fun config_mut(self: &mut DiscountHouse): &mut FreeClaimsConfig { + self.uid_mut().borrow_mut<_, FreeClaimsConfig>(FreeClaimsKey()) } // 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 {}, - ), + self.uid_mut().exists_with_type<_, FreeClaimsConfig>(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 bd00e58f..451cd9ff 100644 --- a/packages/discounts/sources/house.move +++ b/packages/discounts/sources/house.move @@ -6,30 +6,15 @@ /// and exports some package utilities for the 2 systems to use. 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; +use std::string::String; +use suins::suins::AdminCap; -// 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; */ +#[error] +const EInvalidVersion: vector = b"Invalid version"; -/// 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. +/// The version of the DiscountHouse. 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, @@ -45,50 +30,32 @@ fun init(ctx: &mut TxContext) { }) } -/// 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) { +public fun set_version(self: &mut DiscountHouse, _: &AdminCap, 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); +public(package) macro fun discount_house_key(): String { + b"object_discount".to_string() } -/// 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) +public(package) fun assert_version_is_valid(self: &DiscountHouse) { + assert!(self.version == VERSION, EInvalidVersion); } -/// 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. +/// A helper function to get a mutable reference to the UID. 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); } + +#[test_only] +public fun house_for_testing(ctx: &mut TxContext): DiscountHouse { + DiscountHouse { + id: object::new(ctx), + version: VERSION, + } +} diff --git a/packages/discounts/tests/discount_tests.move b/packages/discounts/tests/discount_tests.move index 2c8e71fd..8c4994c5 100644 --- a/packages/discounts/tests/discount_tests.move +++ b/packages/discounts/tests/discount_tests.move @@ -5,13 +5,14 @@ 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 discounts::discounts::{Self, RegularDiscountsApp}; +use discounts::house::{Self, DiscountHouse}; +use sui::clock; use sui::test_scenario::{Self as ts, Scenario, ctx}; +use sui::test_utils::{destroy, assert_eq}; +use suins::constants; +use suins::payment::{Self, PaymentIntent}; +use suins::pricing_config::{Self, PricingConfig}; use suins::registry; use suins::suins::{Self, SuiNS, AdminCap}; @@ -27,14 +28,12 @@ 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.authorize_app_for_testing(); suins.share_for_testing(); house::init_for_testing(scenario.ctx()); let clock = clock::create_for_testing(scenario.ctx()); @@ -48,212 +47,158 @@ fun test_init(): Scenario { // a more expensive alternative. discounts::authorize_type( - &admin_cap, &mut discount_house, - 3*MIST_PER_SUI, - 2*MIST_PER_SUI, - 1*MIST_PER_SUI, + &admin_cap, + test_config(false), // we get 5, 3, 2% discounts for 3, 4, 5+ chars. ); // 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, + &admin_cap, + test_config(true), // we get 50, 30, 20% discounts for 3, 4, 5+ chars. ); discounts::authorize_type( - &admin_cap, &mut discount_house, - MIST_PER_SUI, - MIST_PER_SUI, - MIST_PER_SUI, + &admin_cap, + test_config(true), // we get 50, 30, 20% discounts for 3, 4, 5+ chars. ); 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.return_to_sender(admin_cap); }; scenario_val } -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); -} - -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); +#[test] +fun test_e2e() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + assert_eq(intent.request_data().base_amount(), 50 * constants::mist_per_sui()); + + discounts::apply_percentage_discount( + discount_house, + intent, + suins, + &mut TestAuthorized {}, + scenario.ctx(), + ); - ts::return_shared(discount_house); - ts::return_shared(suins); - ts::return_shared(clock); + assert_eq(intent.request_data().base_amount(), 40 * constants::mist_per_sui()); + assert_eq(intent.request_data().discounts_applied().size(), 1); + assert_eq(intent.request_data().discount_applied(), true); + }); } #[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(), - ); +fun register_day_one() { + init_purchase!(USER_ADDRESS, b"wow.sui", |discount_house, suins, intent, scenario| { + assert_eq(intent.request_data().base_amount(), 1200 * constants::mist_per_sui()); + + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + day_one.set_is_active_for_testing(true); + + discounts::apply_day_one_discount( + discount_house, + intent, + suins, + &mut day_one, + scenario.ctx(), + ); - register_with_type( - &test_item, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS, - ); + assert_eq(intent.request_data().base_amount(), 840 * constants::mist_per_sui()); + assert_eq(intent.request_data().discounts_applied().size(), 1); + assert_eq(intent.request_data().discount_applied(), true); - scenario_val.end(); + day_one.burn_for_testing(); + }); } #[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(); + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + discounts::apply_percentage_discount( + discount_house, + intent, + suins, + &mut TestUnauthorized {}, + 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, - ); +#[test, expected_failure(abort_code = ::discounts::discounts::ENotValidForDayOne)] +fun use_day_one_for_casual_flow_failure() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + + discounts::apply_percentage_discount( + discount_house, + intent, + suins, + &mut day_one, + scenario.ctx(), + ); + day_one.burn_for_testing(); + }); +} - day_one.burn_for_testing(); - scenario_val.end(); +#[test, expected_failure(abort_code = ::discounts::discounts::ENotActiveDayOne)] +fun use_inactive_day_one_failure() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + day_one.set_is_active_for_testing(false); + + discounts::apply_day_one_discount( + discount_house, + intent, + suins, + &mut day_one, + scenario.ctx(), + ); + day_one.burn_for_testing(); + }); } -#[ - 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; +macro fun init_purchase( + $addr: address, + $domain_name: vector, + $f: |&mut DiscountHouse, &mut SuiNS, &mut PaymentIntent, &mut Scenario|, +) { + let addr = $addr; + let dm = $domain_name; - 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(), - ); + let mut scenario = test_init(); + scenario.next_tx(addr); - register_with_type( - &day_one, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS, - ); + // take the discount house + let mut discount_house = scenario.take_shared(); + let mut suins = scenario.take_shared(); + let mut intent = payment::init_registration(&mut suins, dm.to_string()); - day_one.burn_for_testing(); - scenario_val.end(); -} + $f(&mut discount_house, &mut suins, &mut intent, &mut scenario); -#[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; + destroy(intent); + destroy(discount_house); + destroy(suins); - let day_one = day_one::mint_for_testing(scenario.ctx()); - let payment: Coin = coin::mint_for_testing( - MIST_PER_SUI, - scenario.ctx(), - ); + scenario.end(); +} - register_with_day_one( - &day_one, - scenario, - utf8(b"test.sui"), - payment, - USER_ADDRESS, - ); +fun test_config(is_large: bool): PricingConfig { + let multiply = if (is_large) { + 3 + } else { + 1 + }; - day_one.burn_for_testing(); - scenario_val.end(); + pricing_config::new( + vector[ + pricing_config::new_range(vector[3, 3]), + pricing_config::new_range(vector[4, 4]), + pricing_config::new_range(vector[5, 63]), + ], + vector[10 * multiply, 15 * multiply, 20 * multiply], + ) } diff --git a/packages/discounts/tests/free_claims_test.move b/packages/discounts/tests/free_claims_test.move index c495a8c7..ca2dccde 100644 --- a/packages/discounts/tests/free_claims_test.move +++ b/packages/discounts/tests/free_claims_test.move @@ -2,22 +2,34 @@ // SPDX-License-Identifier: Apache-2.0 #[test_only] -module discounts::free_claims_tests; +module discounts::free_claims_test; 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 discounts::free_claims::{Self, FreeClaimsApp}; +use discounts::house::{Self, DiscountHouse}; +use sui::clock; use sui::test_scenario::{Self as ts, Scenario, ctx}; +use sui::test_utils::{destroy, assert_eq}; +use suins::constants; +use suins::payment::{Self, PaymentIntent}; +use suins::pricing_config; use suins::registry; use suins::suins::{Self, SuiNS, AdminCap}; -// An authorized type to test. -public struct TestAuthorized has key, store { id: UID } +// an authorized type to test. +public struct TestAuthorized has key, store { + id: UID +} + +// another authorized type to test. +public struct AnotherAuthorized has key { + id: UID, +} -// An unauthorized type to test. -public struct TestUnauthorized has key { id: UID } +// an unauthorized type to test. +public struct TestUnauthorized has key { + id: UID, +} const SUINS_ADDRESS: address = @0xA001; const USER_ADDRESS: address = @0xA002; @@ -27,223 +39,276 @@ fun test_init(): Scenario { let scenario = &mut scenario_val; { let mut suins = suins::init_for_testing(scenario.ctx()); - suins.authorize_app_for_testing(); + 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); + 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. free_claims::authorize_type( + &mut discount_house, &admin_cap, + pricing_config::new_range(vector[5,63]), // only 5+ letter names + scenario.ctx(), + ); + // a much cheaper price for another type. + free_claims::authorize_type( &mut discount_house, - vector[10, 63], + &admin_cap, + pricing_config::new_range(vector[3,4]), // only 3 and 4 letter names scenario.ctx(), ); + free_claims::authorize_type( - &admin_cap, &mut discount_house, - vector[10, 63], + &admin_cap, + pricing_config::new_range(vector[3,63]), // any actual 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.return_to_sender(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, +#[test] +fun test_e2e() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + assert_eq(intent.request_data().base_amount(), 50 * constants::mist_per_sui()); + + let obj = TestAuthorized { id: object::new(scenario.ctx()) }; + + free_claims::free_claim( + discount_house, + suins, + intent, + &obj, + scenario.ctx(), ); - 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); + + assert_eq(intent.request_data().base_amount(), 0); + assert_eq(intent.request_data().discounts_applied().size(), 1); + assert_eq(intent.request_data().discount_applied(), true); + destroy(obj); + }); } -fun burn_authorized(authorized: TestAuthorized) { - let TestAuthorized { id } = authorized; - id.delete(); +#[test] +fun register_day_one() { + init_purchase!(USER_ADDRESS, b"wow.sui", |discount_house, suins, intent, scenario| { + assert_eq(intent.request_data().base_amount(), 1200 * constants::mist_per_sui()); + + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + day_one.set_is_active_for_testing(true); + + free_claims::free_claim_with_day_one( + discount_house, + suins, + intent, + &day_one, + scenario.ctx(), + ); + + assert_eq(intent.request_data().base_amount(), 0); + assert_eq(intent.request_data().discounts_applied().size(), 1); + assert_eq(intent.request_data().discount_applied(), true); + + day_one.burn_for_testing(); + }); } -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(); +#[test] +fun test_deauthorize_discount() { + let mut scenario = test_init(); + scenario.next_tx(SUINS_ADDRESS); let mut discount_house = scenario.take_shared(); - let clock = scenario.take_shared(); + let admin_cap = scenario.take_from_sender(); - let name = free_claims::free_claim( + let table = free_claims::deauthorize_type( &mut discount_house, - &mut suins, - item, - domain_name, - &clock, - scenario.ctx(), + &admin_cap, ); - - transfer::public_transfer(name, user); + sui::transfer::public_transfer(table, scenario.ctx().sender()); ts::return_shared(discount_house); - ts::return_shared(suins); - ts::return_shared(clock); + scenario.return_to_sender(admin_cap); + + scenario.end(); } -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); +#[test, expected_failure(abort_code = ::discounts::free_claims::EConfigNotExists)] +fun register_with_unauthorized_type() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + + let unauthorized = TestUnauthorized { id: object::new(scenario.ctx()) }; + free_claims::free_claim( + discount_house, + suins, + intent, + &unauthorized, + scenario.ctx(), + ); + destroy(unauthorized); + }); +} - let name = free_claims::free_claim_with_day_one( - &mut discount_house, - &mut suins, - item, - domain_name, - &clock, - scenario.ctx(), - ); +#[test, expected_failure(abort_code = ::discounts::free_claims::EAlreadyClaimed)] +#[allow(dead_code)] +fun test_already_claimed() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + assert_eq(intent.request_data().base_amount(), 50 * constants::mist_per_sui()); - transfer::public_transfer(name, user); + let obj = TestAuthorized { id: object::new(scenario.ctx()) }; - ts::return_shared(discount_house); - ts::return_shared(suins); - ts::return_shared(clock); + free_claims::free_claim( + discount_house, + suins, + intent, + &obj, + scenario.ctx(), + ); + + free_claims::free_claim( + discount_house, + suins, + intent, + &obj, + scenario.ctx(), + ); + abort 1337 + }); } -#[test] -fun test_e2e() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; +#[test, expected_failure(abort_code = ::discounts::free_claims::EInvalidCharacterRange)] +#[allow(dead_code)] +fun test_domain_out_of_range() { + init_purchase!(USER_ADDRESS, b"fiv.sui", |discount_house, suins, intent, scenario| { + let obj = TestAuthorized { id: object::new(scenario.ctx()) }; + + free_claims::free_claim( + discount_house, + suins, + intent, + &obj, + scenario.ctx(), + ); - let test_item = TestAuthorized { - id: object::new(scenario.ctx()), - }; + abort 1337 + }); +} + +#[test, expected_failure(abort_code = ::discounts::free_claims::EConfigExists)] +fun test_authorize_config_twice() { + let mut scenario = test_init(); + scenario.next_tx(SUINS_ADDRESS); + let mut discount_house = scenario.take_shared(); + let admin_cap = scenario.take_from_sender(); - free_claim_with_type( - &test_item, - scenario, - utf8(b"01234567890.sui"), - USER_ADDRESS, + free_claims::authorize_type( + &mut discount_house, + &admin_cap, + pricing_config::new_range(vector[5,63]), + scenario.ctx(), ); - burn_authorized(test_item); - test_end(scenario_val); + abort 1337 } -#[test] -fun use_day_one() { - let mut scenario_val = test_init(); - let scenario = &mut scenario_val; +#[test, expected_failure(abort_code = ::discounts::house::EInvalidVersion)] +fun test_version_togge() { + let mut scenario = test_init(); + scenario.next_tx(SUINS_ADDRESS); + let mut discount_house = scenario.take_shared(); + let admin_cap = scenario.take_from_sender(); - let mut day_one = day_one::mint_for_testing(scenario.ctx()); - day_one.set_is_active_for_testing(true); + discount_house.set_version(&admin_cap, 255); - free_claim_with_day_one( - &day_one, - scenario, - utf8(b"0123456789.sui"), - USER_ADDRESS, - ); + discount_house.assert_version_is_valid(); - day_one.burn_for_testing(); - test_end(scenario_val); + abort 1337 } -#[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, - ); +#[test, expected_failure(abort_code = ::discounts::free_claims::EConfigNotExists)] +fun test_deauthorize_non_existing_config() { + let mut scenario = test_init(); + scenario.next_tx(SUINS_ADDRESS); + let mut discount_house = scenario.take_shared(); + let admin_cap = scenario.take_from_sender(); - // tries to claim again using the same test_item. - free_claim_with_type( - &test_item, - scenario, - utf8(b"01234567891.sui"), - USER_ADDRESS, + let _table = free_claims::deauthorize_type( + &mut discount_house, + &admin_cap, ); - burn_authorized(test_item); - test_end(scenario_val); + abort 1337 } -#[ - 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()), - }; +#[test, expected_failure(abort_code = ::discounts::free_claims::ENotValidForDayOne)] +fun use_day_one_for_casual_flow_failure() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + let day_one = day_one::mint_for_testing(scenario.ctx()); - free_claim_with_type( - &test_item, - scenario, - utf8(b"012345678.sui"), - USER_ADDRESS, - ); + free_claims::free_claim( + discount_house, + suins, + intent, + &day_one, + scenario.ctx(), + ); + day_one.burn_for_testing(); + }); +} - burn_authorized(test_item); - test_end(scenario_val); +#[test, expected_failure(abort_code = ::discounts::free_claims::ENotActiveDayOne)] +fun use_inactive_day_one_failure() { + init_purchase!(USER_ADDRESS, b"fivel.sui", |discount_house, suins, intent, scenario| { + let mut day_one = day_one::mint_for_testing(scenario.ctx()); + day_one.set_is_active_for_testing(false); + + free_claims::free_claim_with_day_one( + discount_house, + suins, + intent, + &day_one, + scenario.ctx(), + ); + day_one.burn_for_testing(); + }); } -#[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; +macro fun init_purchase( + $addr: address, + $domain_name: vector, + $f: |&mut DiscountHouse, &mut SuiNS, &mut PaymentIntent, &mut Scenario|, +) { + let addr = $addr; + let dm = $domain_name; - let test_item = TestUnauthorized { - id: object::new(scenario.ctx()), - }; + let mut scenario = test_init(); + scenario.next_tx(addr); - free_claim_with_type( - &test_item, - scenario, - utf8(b"test.sui"), - USER_ADDRESS, - ); + // take the discount house + let mut discount_house = scenario.take_shared(); + let mut suins = scenario.take_shared(); + let mut intent = payment::init_registration(&mut suins, dm.to_string()); - abort 1337 + $f(&mut discount_house, &mut suins, &mut intent, &mut scenario); + + destroy(intent); + destroy(discount_house); + destroy(suins); + + scenario.end(); }