Skip to content

Commit

Permalink
Merge pull request #17 from MystenLabs/ml/coupons
Browse files Browse the repository at this point in the history
Coupons
  • Loading branch information
manolisliolios authored Oct 2, 2023
2 parents f6a2d31 + 8540a7a commit fb955c3
Show file tree
Hide file tree
Showing 12 changed files with 1,159 additions and 12 deletions.
9 changes: 0 additions & 9 deletions packages/bogo/Move.toml

This file was deleted.

3 changes: 0 additions & 3 deletions packages/bogo/README.md

This file was deleted.

Empty file removed packages/bogo/sources/promo.move
Empty file.
29 changes: 29 additions & 0 deletions packages/coupons/Move.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# @generated by Move, please check-in and do not edit manually.

[move]
version = 0

dependencies = [
{ name = "Sui" },
{ name = "suins" },
]

[[move.package]]
name = "MoveStdlib"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet", subdir = "crates/sui-framework/packages/move-stdlib" }

[[move.package]]
name = "Sui"
source = { git = "https://github.com/MystenLabs/sui.git", rev = "testnet", subdir = "crates/sui-framework/packages/sui-framework" }

dependencies = [
{ name = "MoveStdlib" },
]

[[move.package]]
name = "suins"
source = { local = "../suins" }

dependencies = [
{ name = "Sui" },
]
10 changes: 10 additions & 0 deletions packages/coupons/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "coupons"
version = "0.0.1"

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet", override=true }
suins = { local = "../suins" }

[addresses]
coupons = "0x0"
21 changes: 21 additions & 0 deletions packages/coupons/sources/constants.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Mysten Labs, Inc.
// 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;

/// A getter for the percentage discount type.
public fun percentage_discount_type(): u8 { PERCENTAGE_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<u8> { vector[PERCENTAGE_DISCOUNT, FIXED_PRICE_DISCOUNT] }
}
293 changes: 293 additions & 0 deletions packages/coupons/sources/coupons.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
// Copyright (c) Mysten Labs, Inc.
// 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::coupons {
use std::string::{Self, String};

use sui::table::{Self, Table};
use sui::tx_context::{TxContext, sender};
use sui::object::{Self, UID};
use sui::transfer;
use sui::dynamic_field::{Self as df};
use sui::clock::Clock;
use sui::sui::SUI;
use sui::coin::{Self, Coin};

use coupons::rules::{Self, CouponRules};
use coupons::constants;

/// Coupon already exists
const ECouponAlreadyExists: u64 = 0;
/// 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;

// use suins::config;
use suins::domain;
use suins::suins::{Self, AdminCap, SuiNS}; // re-use AdminCap for creating new coupons.
use suins::suins_registration::SuinsRegistration;
use suins::config::{Self, Config};
use suins::registry::{Self, Registry};

// Authorization for the Coupons on SuiNS, to be able to register names on the app.
struct CouponsApp has drop {}
/// Authorization Key for secondary apps (e.g. Discord) connected to this module.
struct AppKey<phantom App: drop> has copy, store, drop {}

/// 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).
struct Data has store {
// hold a list of all coupons in the system.
coupons: Table<String, Coupon>
}

/// The CouponHouse Shared Object which holds a table of coupon codes available for claim.
struct CouponHouse has key, store {
id: UID,
data: Data,
version: u8
}

/// 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.
/// - `type` 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.
struct Coupon has copy, store, drop {
type: 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.
}

// Initialization function.
// Share the CouponHouse.
fun init(ctx: &mut TxContext){
transfer::share_object(CouponHouse {
id: object::new(ctx),
data: Data { coupons: table::new(ctx) },
version: VERSION
});
}

/// Register a name using a coupon code.
public fun register_with_coupon(
self: &mut CouponHouse,
suins: &mut SuiNS,
coupon_code: String,
domain_name: String,
no_years: u8,
payment: Coin<SUI>,
clock: &Clock,
ctx: &mut TxContext
): SuinsRegistration {
assert_version_is_valid(self);
// Validate that specified coupon is valid.
assert!(table::contains(&mut self.data.coupons, coupon_code), ECouponNotExists);

// Verify coupon house is authorized to buy names.
suins::assert_app_is_authorized<CouponsApp>(suins);

// Validate registration years are in [0,5] range.
assert!(no_years > 0 && no_years <= 5, EInvalidYearsArgument);

let config = suins::get_config<Config>(suins);
let domain = domain::new(domain_name);
let label = domain::sld(&domain);

let domain_length = (string::length(label) as u8);

// Borrow coupon from the table.
let coupon = table::borrow_mut(&mut self.data.coupons, 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.
rules::assert_coupon_valid_for_domain_size(&coupon.rules, domain_length);
// 2. Decrease available claims. Will ABORT if the coupon doesn't have enough available claims.
rules::decrease_available_claims(&mut coupon.rules);
// 3. Validate the coupon is valid for the specified user.
rules::assert_coupon_valid_for_address(&coupon.rules, sender(ctx));
// 4. Validate the coupon hasn't expired (Based on clock)
rules::assert_coupon_is_not_expired(&coupon.rules, clock);
// 5. Validate years are valid for the coupon.
rules::assert_coupon_valid_for_domain_years(&coupon.rules, no_years);

// Validate name can be registered (is main domain (no subdomain) and length is valid)
config::assert_valid_user_registerable_domain(&domain);

let original_price = config::calculate_price(config, domain_length, no_years);
let sale_price = internal_calculate_sale_price(original_price, coupon);

assert!(coin::value(&payment) == sale_price, EIncorrectAmount);
suins::app_add_balance(CouponsApp {}, suins, coin::into_balance(payment));

// Clean up our registry by removing the coupon if no more available claims!
if(!rules::has_available_claims(&coupon.rules)){
// remove the coupon, since it's no longer usable.
internal_remove_coupon(&mut self.data, coupon_code);
};

let registry = suins::app_registry_mut<CouponsApp, Registry>(CouponsApp {}, suins);
registry::add_record(registry, 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(self: &mut CouponHouse, price: u64, coupon_code: String): u64 {
// Validate that specified coupon is valid.
assert!(table::contains(&mut self.data.coupons, coupon_code), ECouponNotExists);
// Borrow coupon from the table.
let coupon = table::borrow_mut(&mut self.data.coupons, coupon_code);
internal_calculate_sale_price(price, coupon)
}

// Get `Data` as an authorized app.
public fun app_data_mut<App: drop>(_: App, self: &mut CouponHouse): &mut Data {
assert_version_is_valid(self);
// verify app is authorized to get a mutable reference.
assert_app_is_authorized<App>(self);
&mut self.data
}

/// Check if an application is authorized to access protected features of the Coupon House.
public fun is_app_authorized<App: drop>(self: &CouponHouse): bool {
df::exists_(&self.id, AppKey<App>{})
}

/// Assert that an application is authorized to access protected features of Coupon House.
/// Aborts with `EAppNotAuthorized` if not.
public fun assert_app_is_authorized<App: drop>(self: &CouponHouse) {
assert!(is_app_authorized<App>(self), EAppNotAuthorized);
}

/// Authorize an app. This allows to a secondary module to add/remove coupons.
public fun authorize_app<App: drop>(_: &AdminCap, self: &mut CouponHouse) {
df::add(&mut self.id, AppKey<App>{}, true);
}

/// De-authorize an app. The app can no longer add or remove
public fun deauthorize_app<App: drop>(_: &AdminCap, self: &mut CouponHouse): bool {
df::remove(&mut self.id, AppKey<App>{})
}

/// 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 CouponHouse, version: u8) {
self.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,
self: &mut CouponHouse,
code: String,
type: u8,
amount: u64,
rules: CouponRules,
ctx: &mut TxContext
) {
assert_version_is_valid(self);
internal_save_coupon(&mut self.data, code, internal_create_coupon(type, amount, rules, ctx));
}

// Remove a coupon as a system's admin.
public fun admin_remove_coupon(_: &AdminCap, self: &mut CouponHouse, code: String){
internal_remove_coupon(&mut self.data, code)
}

// Add coupon as a registered app.
public fun app_add_coupon(
self: &mut Data,
code: String,
type: u8,
amount: u64,
rules: CouponRules,
ctx: &mut TxContext
){
internal_save_coupon(self, code, internal_create_coupon(type, amount, rules, ctx));
}

// Remove a coupon as a registered app.
public fun app_remove_coupon(self: &mut Data, code: String) {
internal_remove_coupon(self, code);
}


/// A helper to calculate the final price after the discount.
fun internal_calculate_sale_price(price: u64, coupon: &Coupon): u64{
// If it's fixed price, we just deduce the amount.
if(coupon.type == 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
}

/// Private internal functions
/// An internal function to save the coupon in the shared object's config.
fun internal_save_coupon(
self: &mut Data,
code: String,
coupon: Coupon
) {
assert!(!table::contains(&mut self.coupons, code), ECouponAlreadyExists);
table::add(&mut self.coupons, code, coupon);
}

/// An internal function to create a coupon object.
fun internal_create_coupon(
type: u8,
amount: u64,
rules: CouponRules,
_ctx: &mut TxContext
): Coupon {
rules::assert_is_valid_amount(type, amount);
rules::assert_is_valid_discount_type(type);
Coupon {
type, amount, rules
}
}

// A function to remove a coupon from the system.
fun internal_remove_coupon(self: &mut Data, code: String) {
table::remove(&mut self.coupons, code);
}

// test only functions.
#[test_only]
public fun init_for_testing(ctx: &mut TxContext){
init(ctx);
}
}
Loading

0 comments on commit fb955c3

Please sign in to comment.