From 4b11522cab4bab4c352b8abe0924c003981ef84f Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Fri, 22 Nov 2024 02:55:39 +0200 Subject: [PATCH] feat(examples/move/nft_marketplace): highlighted the rental extension separately from the marketplace extension and added some mock items for marketplace operations --- .../sources/item_for_market.move | 58 +++ .../sources/nft_marketplace.move | 410 ++--------------- .../sources/rental_extension.move | 417 ++++++++++++++++++ 3 files changed, 514 insertions(+), 371 deletions(-) create mode 100644 docs/examples/move/nft_marketplace/sources/item_for_market.move create mode 100644 docs/examples/move/nft_marketplace/sources/rental_extension.move diff --git a/docs/examples/move/nft_marketplace/sources/item_for_market.move b/docs/examples/move/nft_marketplace/sources/item_for_market.move new file mode 100644 index 00000000000..cfea0bfef5a --- /dev/null +++ b/docs/examples/move/nft_marketplace/sources/item_for_market.move @@ -0,0 +1,58 @@ +module nft_marketplace::market_items { + use iota::package; + /// One Time Witness. + public struct MARKET_ITEMS has drop {} + + + fun init(otw: MARKET_ITEMS, ctx: &mut TxContext) { + package::claim_and_keep(otw, ctx) + } + + public struct TShirt has key, store { + id: UID, + } + + public struct Jacket has key, store { + id: UID, + } + + public struct Shoes has key, store { + id: UID, + } + + public struct Jeans has key, store { + id: UID, + } + + public fun new_tshirt(ctx: &mut TxContext) { + let tshirt = TShirt { + id: object::new(ctx), + }; + + transfer::public_transfer(tshirt, ctx.sender()); + } + + public fun new_jeans(ctx: &mut TxContext) { + let jeans = Jeans { + id: object::new(ctx), + }; + + transfer::public_transfer(jeans, ctx.sender()); + } + + public fun new_shoes(ctx: &mut TxContext) { + let shoes = Shoes { + id: object::new(ctx), + }; + + transfer::public_transfer(shoes, ctx.sender()); + } + + public fun new_jacket(ctx: &mut TxContext) { + let jacket = Jacket { + id: object::new(ctx), + }; + + transfer::public_transfer(jacket, ctx.sender()); + } +} \ No newline at end of file diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move index f52c13437c4..b49b4979d53 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -1,111 +1,40 @@ module nft_marketplace::nft_marketplace { - // iota imports use iota::{ kiosk::{Kiosk, KioskOwnerCap, purchase}, kiosk_extension, bag, - transfer_policy::{Self, TransferPolicy, TransferRequest, TransferPolicyCap, has_rule, get_rule}, - clock::Clock, - coin::{Self, Coin}, - balance::{Self, Balance}, + transfer_policy::{Self, TransferPolicy, TransferPolicyCap, has_rule}, + coin::Coin, iota::IOTA, - package::Publisher }; // rules imports - use kiosk::kiosk_lock_rule::Rule as LockRule; // use kiosk::floor_price_rule::Rule as FloorPriceRule; // use kiosk::personal_kiosk_rule::Rule as PersonalRule; use kiosk::royalty_rule::Rule as RoyaltyRule; use kiosk::royalty_rule; // use kiosk::witness_rule::Rule as WitnessRule; + // === Errors === const EExtensionNotInstalled: u64 = 0; - const ENotOwner: u64 = 1; - const ENotEnoughCoins: u64 = 2; - const EInvalidKiosk: u64 = 3; - const ERentingPeriodNotOver: u64 = 4; - const EObjectNotExist: u64 = 5; - const ETotalPriceOverflow: u64 = 6; - const EInvalidRoyaltiesAmount: u64 = 7; + const EObjectNotExist: u64 = 1; // === Constants === const PERMISSIONS: u128 = 11; - const SECONDS_IN_A_DAY: u64 = 86400; - const MAX_BASIS_POINTS: u16 = 10_000; - const MAX_VALUE_U64: u64 = 0xff_ff_ff_ff__ff_ff_ff_ff; - - // === Structs === /// Extension Key for Kiosk Marketplace extension. public struct Marketplace has drop {} - /// Struct representing a rented item. - /// Used as a key for the Rentable that's placed in the Extension's Bag. - public struct Rented has store, copy, drop { id: ID } - - /// Struct representing a listed item. - /// Used as a key for the Rentable that's placed in the Extension's Bag. + /// Used as a key for the item that has been up for sale that's placed in the Extension's Bag. public struct Listed has store, copy, drop { id: ID } - - /// Promise struct for borrowing by value. - public struct Promise { - item: Rented, - duration: u64, - start_date: u64, - price_per_day: u64, - renter_kiosk: ID, - borrower_kiosk: ID, - } - - /// A wrapper object that holds an asset that is being rented. - /// Contains information relevant to the rental period, cost and renter. - public struct Rentable has store { - object: T, + + public struct ItemPrice has store { /// Total amount of time offered for renting in days. - duration: u64, - /// Initially undefined, is updated once someone rents it. - start_date: Option, - price_per_day: u64, - /// The kiosk id that the object was taken from. - kiosk_id: ID, - } - - /// A shared object that should be minted by every creator. - /// Defines the royalties the creator will receive from each rent invocation. - public struct RentalPolicy has key, store { - id: UID, - balance: Balance, - /// Note: Move does not support float numbers. - /// - /// If you need to represent a float, you need to determine the desired - /// precision and use a larger integer representation. - /// - /// For example, fpercentages can be represented using basis points: - /// 10000 basis points represent 100% and 100 basis points represent 1%. - amount_bp: u64 - } - - /// A shared object that should be minted by every creator. - /// Even for creators that do not wish to enforce royalties. Provides authorized access to an - /// empty TransferPolicy. - public struct ProtectedTP has key, store { - id: UID, - transfer_policy: TransferPolicy, - policy_cap: TransferPolicyCap + price: u64, } - /// A shared object that should be minted by every creator. - public struct RoyaltyTP has key, store { - id: UID, - transfer_policy: TransferPolicy, - policy_cap: TransferPolicyCap - } - - // === Public Functions === - /// Enables someone to install the Marketplace extension in their Kiosk. public fun install( kiosk: &mut Kiosk, @@ -115,335 +44,74 @@ module nft_marketplace::nft_marketplace { kiosk_extension::add(Marketplace {}, kiosk, cap, PERMISSIONS, ctx); } - /// Remove the extension from the Kiosk. Can only be performed by the owner, + /// Remove the extension from the Kiosk. Can only be performed by the owner, /// The extension storage must be empty for the transaction to succeed. public fun remove(kiosk: &mut Kiosk, cap: &KioskOwnerCap, _ctx: &mut TxContext) { kiosk_extension::remove(kiosk, cap); } - /// Mints and shares a ProtectedTP & a RentalPolicy object for type T. - /// Can only be performed by the publisher of type T. - public fun setup_renting(publisher: &Publisher, amount_bp: u64, ctx: &mut TxContext) { - // Creates an empty TP and shares a ProtectedTP object. - // This can be used to bypass the lock rule under specific conditions. - // Storing inside the cap the ProtectedTP with no way to access it - // as we do not want to modify this policy - let (transfer_policy, policy_cap) = transfer_policy::new(publisher, ctx); - - let protected_tp = ProtectedTP { - id: object::new(ctx), - transfer_policy, - policy_cap, - }; - - let rental_policy = RentalPolicy { - id: object::new(ctx), - balance: balance::zero(), - amount_bp, - }; - - transfer::share_object(protected_tp); - transfer::share_object(rental_policy); - } - public fun setup_royalties(policy: &mut TransferPolicy, cap: &TransferPolicyCap, amount_bp: u16, min_amount: u64, ctx: &mut TxContext) { - royalty_rule::add( policy, cap, amount_bp, min_amount); - } - /// Enables someone to list an asset within the Marketplace extension's Bag, - /// creating a Bag entry with the asset's ID as the key and a Rentable wrapper object as the value. - /// Requires the existance of a ProtectedTP which can only be created by the creator of type T. - /// Assumes item is already placed (& optionally locked) in a Kiosk. - public fun list( - kiosk: &mut Kiosk, - cap: &KioskOwnerCap, - protected_tp: &ProtectedTP, - item_id: ID, - duration: u64, - price_per_day: u64, - ctx: &mut TxContext, - ) { - assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); - - kiosk.set_owner(cap, ctx); - kiosk.list(cap, item_id, 0); - - let coin = coin::zero(ctx); - let (object, request) = kiosk.purchase(item_id, coin); - - let (_item, _paid, _from) = protected_tp.transfer_policy.confirm_request(request); - - let rentable = Rentable { - object, - duration, - start_date: option::none(), - price_per_day, - kiosk_id: object::id(kiosk), - }; - - place_in_bag(kiosk, Listed { id: item_id }, rentable); - } - - /// Allows the renter to delist an item, that is not currently being rented. - /// Places (or locks, if a lock rule is present) the object back to owner's Kiosk. - /// Creators should mint an empty TransferPolicy even if they don't want to apply any royalties. - /// If they wish at some point to enforce royalties, they can update the existing TransferPolicy. - public fun delist( - kiosk: &mut Kiosk, - cap: &KioskOwnerCap, - transfer_policy: &TransferPolicy, - item_id: ID, - _ctx: &mut TxContext, - ) { - assert!(kiosk.has_access(cap), ENotOwner); - - let rentable = take_from_bag(kiosk, Listed { id: item_id }); - - let Rentable { - object, - duration: _, - start_date: _, - price_per_day: _, - kiosk_id: _, - } = rentable; - - if (has_rule(transfer_policy)) { - kiosk.lock(cap, transfer_policy, object); - } else { - kiosk.place(cap, object); - }; + royalty_rule::add(policy, cap, amount_bp, min_amount); } /// Buy listed item and pay royalties if needed - public fun buy(kiosk: &mut Kiosk, policy: &mut TransferPolicy, item_id: object::ID, mut payment: Coin, mut royalties: Option>, ctx: &mut TxContext) { - let mut fee_amount = 0; - let payment_value = payment.value(); - let (item, mut transfer_request) = purchase(kiosk, item_id, payment); + public fun buy_item(kiosk: &mut Kiosk, policy: &mut TransferPolicy, item_id: object::ID, mut payment: Coin, ctx: &mut TxContext) { + assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); + let item_price = take_from_bag(kiosk, Listed { id: item_id }); + let ItemPrice { price } = item_price; + let payment_amount = payment.split(price, ctx); + let payment_amount_value = payment_amount.value(); + let (item, mut transfer_request) = purchase(kiosk, item_id, payment_amount); if (policy.has_rule()) { - let royalties = royalties.destroy_some(); - fee_amount = royalty_rule::fee_amount(policy, payment_value); - assert!(fee_amount == royalties.value(), EInvalidRoyaltiesAmount); - royalty_rule::pay(policy, &mut transfer_request, royalties); - } else{ - royalties.destroy_none(); + let royalties_value = royalty_rule::fee_amount(policy, payment_amount_value); + let royalties_coin = payment.split(royalties_value, ctx); + royalty_rule::pay(policy, &mut transfer_request, royalties_coin); }; transfer_policy::confirm_request(policy, transfer_request); transfer::public_transfer(item, ctx.sender()); + // Send a leftover back to buyer + transfer::public_transfer(payment, ctx.sender()); } - /// This enables individuals to rent a listed Rentable. - /// - /// It permits anyone to borrow an item on behalf of another user, provided they have the - /// Marketplace extension installed. - /// - /// The Rental Policy defines the portion of the coin that will be retained as fees and added to - /// the Rental Policy's balance. - public fun rent( - renter_kiosk: &mut Kiosk, - borrower_kiosk: &mut Kiosk, - rental_policy: &mut RentalPolicy, - item_id: ID, - mut coin: Coin, - clock: &Clock, - ctx: &mut TxContext, - ) { - assert!(kiosk_extension::is_installed(borrower_kiosk), EExtensionNotInstalled); - - let mut rentable = take_from_bag(renter_kiosk, Listed { id: item_id }); - - let max_price_per_day = MAX_VALUE_U64 / rentable.duration; - assert!(rentable.price_per_day <= max_price_per_day, ETotalPriceOverflow); - let total_price = rentable.price_per_day * rentable.duration; - - let coin_value = coin.value(); - assert!(coin_value == total_price, ENotEnoughCoins); - - // Calculate fees_amount using the given basis points amount (percentage), ensuring the - // result fits into a 64-bit unsigned integer. - let mut fees_amount = coin_value as u128; - fees_amount = fees_amount * (rental_policy.amount_bp as u128); - fees_amount = fees_amount / (MAX_BASIS_POINTS as u128); - let fees = coin.split(fees_amount as u64, ctx); - - coin::put(&mut rental_policy.balance, fees); - transfer::public_transfer(coin, renter_kiosk.owner()); - rentable.start_date.fill(clock.timestamp_ms()); - - place_in_bag(borrower_kiosk, Rented { id: item_id }, rentable); - } - - /// Enables the borrower to acquire the Rentable by reference from their bag. - public fun borrow( + public fun set_price( kiosk: &mut Kiosk, cap: &KioskOwnerCap, - item_id: ID, - _ctx: &mut TxContext, - ): &T { - assert!(kiosk.has_access(cap), ENotOwner); - let ext_storage_mut = kiosk_extension::storage_mut(Marketplace {}, kiosk); - let rentable: &Rentable = &ext_storage_mut[Rented { id: item_id }]; - &rentable.object - } - - /// Enables the borrower to temporarily acquire the Rentable with an agreement or promise to - /// return it. - /// - /// All the information about the Rentable is stored within the promise, facilitating the - /// reconstruction of the Rentable when the object is returned. - public fun borrow_val( - kiosk: &mut Kiosk, - cap: &KioskOwnerCap, - item_id: ID, - _ctx: &mut TxContext, - ): (T, Promise) { - assert!(kiosk.has_access(cap), ENotOwner); - let borrower_kiosk = object::id(kiosk); - - let rentable = take_from_bag(kiosk, Rented { id: item_id }); - - let promise = Promise { - item: Rented { id: item_id }, - duration: rentable.duration, - start_date: *option::borrow(&rentable.start_date), - price_per_day: rentable.price_per_day, - renter_kiosk: rentable.kiosk_id, - borrower_kiosk - }; - - let Rentable { - object, - duration: _, - start_date: _, - price_per_day: _, - kiosk_id: _, - } = rentable; - - (object, promise) - } - - /// Enables the borrower to return the borrowed item. - public fun return_val( - kiosk: &mut Kiosk, - object: T, - promise: Promise, - _ctx: &mut TxContext, - ) { + item: T, + price: u64) { assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); - let Promise { - item, - duration, - start_date, - price_per_day, - renter_kiosk, - borrower_kiosk, - } = promise; + let id = object::id(&item); + kiosk.place_and_list(cap, item, price); - let kiosk_id = object::id(kiosk); - assert!(kiosk_id == borrower_kiosk, EInvalidKiosk); - - let rentable = Rentable { - object, - duration, - start_date: option::some(start_date), - price_per_day, - kiosk_id: renter_kiosk, + let item_price = ItemPrice { + price, }; - place_in_bag(kiosk, item, rentable); + place_in_bag(kiosk, Listed { id }, item_price); } - /// Enables the owner to reclaim their asset once the rental period has concluded. - public fun reclaim( - renter_kiosk: &mut Kiosk, - borrower_kiosk: &mut Kiosk, - transfer_policy: &TransferPolicy, - clock: &Clock, - item_id: ID, - _ctx: &mut TxContext, - ) { - assert!(kiosk_extension::is_installed(renter_kiosk), EExtensionNotInstalled); - - let rentable = take_from_bag(borrower_kiosk, Rented { id: item_id }); - - let Rentable { - object, - duration, - start_date, - price_per_day: _, - kiosk_id, - } = rentable; - - assert!(object::id(renter_kiosk) == kiosk_id, EInvalidKiosk); - - let start_date_ms = *option::borrow(&start_date); - let current_timestamp = clock.timestamp_ms(); - let final_timestamp = start_date_ms + duration * SECONDS_IN_A_DAY; - assert!(current_timestamp > final_timestamp, ERentingPeriodNotOver); - - if (transfer_policy.has_rule()) { - kiosk_extension::lock( - Marketplace {}, - renter_kiosk, - object, - transfer_policy, - ); - } else { - kiosk_extension::place( - Marketplace {}, - renter_kiosk, - object, - transfer_policy, - ); - }; - } // === Private Functions === fun take_from_bag( kiosk: &mut Kiosk, - item: Key, - ) : Rentable { + item_key: Key, + ) : ItemPrice { let ext_storage_mut = kiosk_extension::storage_mut(Marketplace {}, kiosk); - assert!(bag::contains(ext_storage_mut, item), EObjectNotExist); - bag::remove>( + assert!(bag::contains(ext_storage_mut, item_key), EObjectNotExist); + bag::remove>( ext_storage_mut, - item, + item_key, ) } fun place_in_bag( kiosk: &mut Kiosk, - item: Key, - rentable: Rentable, + item_key: Key, + item_price: ItemPrice, ) { let ext_storage_mut = kiosk_extension::storage_mut(Marketplace {}, kiosk); - bag::add(ext_storage_mut, item, rentable); - } - - // === Test Functions === - - #[test_only] - // public fun test_take_from_bag(kiosk: &mut Kiosk, item_id: ID) { - public fun test_take_from_bag( - kiosk: &mut Kiosk, - item: Key, - ) { - let rentable = take_from_bag(kiosk, item); - - let Rentable { - object, - duration: _, - start_date: _, - price_per_day: _, - kiosk_id: _ - } = rentable; - - transfer::public_share_object(object); - } - - #[test_only] - public fun create_listed(id: ID) : Listed { - Listed { id } + bag::add(ext_storage_mut, item_key, item_price); } -} +} \ No newline at end of file diff --git a/docs/examples/move/nft_marketplace/sources/rental_extension.move b/docs/examples/move/nft_marketplace/sources/rental_extension.move new file mode 100644 index 00000000000..1bb4b2722f5 --- /dev/null +++ b/docs/examples/move/nft_marketplace/sources/rental_extension.move @@ -0,0 +1,417 @@ +module nft_marketplace::rental_extension { + + // iota imports + use iota::{ + kiosk::{Kiosk, KioskOwnerCap, purchase}, + kiosk_extension, + bag, + transfer_policy::{Self, TransferPolicy, TransferPolicyCap, has_rule}, + clock::Clock, + coin::{Self, Coin}, + balance::{Self, Balance}, + iota::IOTA, + package::Publisher + }; + + // rules imports + use kiosk::kiosk_lock_rule::Rule as LockRule; + + // === Errors === + const EExtensionNotInstalled: u64 = 0; + const ENotOwner: u64 = 1; + const ENotEnoughCoins: u64 = 2; + const EInvalidKiosk: u64 = 3; + const ERentingPeriodNotOver: u64 = 4; + const EObjectNotExist: u64 = 5; + const ETotalPriceOverflow: u64 = 6; + + // === Constants === + const PERMISSIONS: u128 = 11; + const SECONDS_IN_A_DAY: u64 = 86400; + const MAX_BASIS_POINTS: u16 = 10_000; + const MAX_VALUE_U64: u64 = 0xff_ff_ff_ff__ff_ff_ff_ff; + + // === Structs === + + /// Extension Key for Kiosk Marketplace extension. + public struct Rental has drop {} + + /// Struct representing a rented item. + /// Used as a key for the Rentable that's placed in the Extension's Bag. + public struct Rented has store, copy, drop { id: ID } + + /// Struct representing a listed item. + /// Used as a key for the Rentable that's placed in the Extension's Bag. + public struct Listed has store, copy, drop { id: ID } + + /// Promise struct for borrowing by value. + public struct Promise { + item: Rented, + duration: u64, + start_date: u64, + price_per_day: u64, + renter_kiosk: ID, + borrower_kiosk: ID, + } + + /// A wrapper object that holds an asset that is being rented. + /// Contains information relevant to the rental period, cost and renter. + public struct Rentable has store { + object: T, + /// Total amount of time offered for renting in days. + duration: u64, + /// Initially undefined, is updated once someone rents it. + start_date: Option, + price_per_day: u64, + /// The kiosk id that the object was taken from. + kiosk_id: ID, + } + + /// A shared object that should be minted by every creator. + /// Defines the royalties the creator will receive from each rent invocation. + public struct RentalPolicy has key, store { + id: UID, + balance: Balance, + /// Note: Move does not support float numbers. + /// + /// If you need to represent a float, you need to determine the desired + /// precision and use a larger integer representation. + /// + /// For example, fpercentages can be represented using basis points: + /// 10000 basis points represent 100% and 100 basis points represent 1%. + amount_bp: u64 + } + + /// A shared object that should be minted by every creator. + /// Even for creators that do not wish to enforce royalties. Provides authorized access to an + /// empty TransferPolicy. + public struct ProtectedTP has key, store { + id: UID, + transfer_policy: TransferPolicy, + policy_cap: TransferPolicyCap + } + + // === Public Functions === + + /// Enables someone to install the Marketplace extension in their Kiosk. + public fun install( + kiosk: &mut Kiosk, + cap: &KioskOwnerCap, + ctx: &mut TxContext, + ) { + kiosk_extension::add(Rental {}, kiosk, cap, PERMISSIONS, ctx); + } + + /// Remove the extension from the Kiosk. Can only be performed by the owner, + /// The extension storage must be empty for the transaction to succeed. + public fun remove(kiosk: &mut Kiosk, cap: &KioskOwnerCap, _ctx: &mut TxContext) { + kiosk_extension::remove(kiosk, cap); + } + + /// Mints and shares a ProtectedTP & a RentalPolicy object for type T. + /// Can only be performed by the publisher of type T. + public fun setup_renting(publisher: &Publisher, amount_bp: u64, ctx: &mut TxContext) { + // Creates an empty TP and shares a ProtectedTP object. + // This can be used to bypass the lock rule under specific conditions. + // Storing inside the cap the ProtectedTP with no way to access it + // as we do not want to modify this policy + let (transfer_policy, policy_cap) = transfer_policy::new(publisher, ctx); + + let protected_tp = ProtectedTP { + id: object::new(ctx), + transfer_policy, + policy_cap, + }; + + let rental_policy = RentalPolicy { + id: object::new(ctx), + balance: balance::zero(), + amount_bp, + }; + + transfer::share_object(protected_tp); + transfer::share_object(rental_policy); + } + + /// Enables someone to list an asset within the Marketplace extension's Bag, + /// creating a Bag entry with the asset's ID as the key and a Rentable wrapper object as the value. + /// Requires the existance of a ProtectedTP which can only be created by the creator of type T. + /// Assumes item is already placed (& optionally locked) in a Kiosk. + public fun list( + kiosk: &mut Kiosk, + cap: &KioskOwnerCap, + protected_tp: &ProtectedTP, + item_id: ID, + duration: u64, + price_per_day: u64, + ctx: &mut TxContext, + ) { + assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); + + kiosk.set_owner(cap, ctx); + kiosk.list(cap, item_id, 0); + + let coin = coin::zero(ctx); + let (object, request) = kiosk.purchase(item_id, coin); + + let (_item, _paid, _from) = protected_tp.transfer_policy.confirm_request(request); + + let rentable = Rentable { + object, + duration, + start_date: option::none(), + price_per_day, + kiosk_id: object::id(kiosk), + }; + + place_in_bag(kiosk, Listed { id: item_id }, rentable); + } + + /// Allows the renter to delist an item, that is not currently being rented. + /// Places (or locks, if a lock rule is present) the object back to owner's Kiosk. + /// Creators should mint an empty TransferPolicy even if they don't want to apply any royalties. + /// If they wish at some point to enforce royalties, they can update the existing TransferPolicy. + public fun delist( + kiosk: &mut Kiosk, + cap: &KioskOwnerCap, + transfer_policy: &TransferPolicy, + item_id: ID, + _ctx: &mut TxContext, + ) { + assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); + assert!(kiosk.has_access(cap), ENotOwner); + + let rentable = take_from_bag(kiosk, Listed { id: item_id }); + + let Rentable { + object, + duration: _, + start_date: _, + price_per_day: _, + kiosk_id: _, + } = rentable; + + if (has_rule(transfer_policy)) { + kiosk.lock(cap, transfer_policy, object); + } else { + kiosk.place(cap, object); + }; + } + + /// This enables individuals to rent a listed Rentable. + /// + /// It permits anyone to borrow an item on behalf of another user, provided they have the + /// Marketplace extension installed. + /// + /// The Rental Policy defines the portion of the coin that will be retained as fees and added to + /// the Rental Policy's balance. + public fun rent( + renter_kiosk: &mut Kiosk, + borrower_kiosk: &mut Kiosk, + rental_policy: &mut RentalPolicy, + item_id: ID, + mut coin: Coin, + clock: &Clock, + ctx: &mut TxContext, + ) { + assert!(kiosk_extension::is_installed(borrower_kiosk), EExtensionNotInstalled); + + let mut rentable = take_from_bag(renter_kiosk, Listed { id: item_id }); + + let max_price_per_day = MAX_VALUE_U64 / rentable.duration; + assert!(rentable.price_per_day <= max_price_per_day, ETotalPriceOverflow); + let total_price = rentable.price_per_day * rentable.duration; + + let coin_value = coin.value(); + assert!(coin_value == total_price, ENotEnoughCoins); + + // Calculate fees_amount using the given basis points amount (percentage), ensuring the + // result fits into a 64-bit unsigned integer. + let mut fees_amount = coin_value as u128; + fees_amount = fees_amount * (rental_policy.amount_bp as u128); + fees_amount = fees_amount / (MAX_BASIS_POINTS as u128); + + let fees = coin.split(fees_amount as u64, ctx); + + coin::put(&mut rental_policy.balance, fees); + transfer::public_transfer(coin, renter_kiosk.owner()); + rentable.start_date.fill(clock.timestamp_ms()); + + place_in_bag(borrower_kiosk, Rented { id: item_id }, rentable); + } + + /// Enables the borrower to acquire the Rentable by reference from their bag. + public fun borrow( + kiosk: &mut Kiosk, + cap: &KioskOwnerCap, + item_id: ID, + _ctx: &mut TxContext, + ): &T { + assert!(kiosk.has_access(cap), ENotOwner); + let ext_storage_mut = kiosk_extension::storage_mut(Rental {}, kiosk); + let rentable: &Rentable = &ext_storage_mut[Rented { id: item_id }]; + &rentable.object + } + + /// Enables the borrower to temporarily acquire the Rentable with an agreement or promise to + /// return it. + /// + /// All the information about the Rentable is stored within the promise, facilitating the + /// reconstruction of the Rentable when the object is returned. + public fun borrow_val( + kiosk: &mut Kiosk, + cap: &KioskOwnerCap, + item_id: ID, + _ctx: &mut TxContext, + ): (T, Promise) { + assert!(kiosk.has_access(cap), ENotOwner); + let borrower_kiosk = object::id(kiosk); + + let rentable = take_from_bag(kiosk, Rented { id: item_id }); + + let promise = Promise { + item: Rented { id: item_id }, + duration: rentable.duration, + start_date: *option::borrow(&rentable.start_date), + price_per_day: rentable.price_per_day, + renter_kiosk: rentable.kiosk_id, + borrower_kiosk + }; + + let Rentable { + object, + duration: _, + start_date: _, + price_per_day: _, + kiosk_id: _, + } = rentable; + + (object, promise) + } + + /// Enables the borrower to return the borrowed item. + public fun return_val( + kiosk: &mut Kiosk, + object: T, + promise: Promise, + _ctx: &mut TxContext, + ) { + assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); + + let Promise { + item, + duration, + start_date, + price_per_day, + renter_kiosk, + borrower_kiosk, + } = promise; + + let kiosk_id = object::id(kiosk); + assert!(kiosk_id == borrower_kiosk, EInvalidKiosk); + + let rentable = Rentable { + object, + duration, + start_date: option::some(start_date), + price_per_day, + kiosk_id: renter_kiosk, + }; + + place_in_bag(kiosk, item, rentable); + } + + /// Enables the owner to reclaim their asset once the rental period has concluded. + public fun reclaim( + renter_kiosk: &mut Kiosk, + borrower_kiosk: &mut Kiosk, + transfer_policy: &TransferPolicy, + clock: &Clock, + item_id: ID, + _ctx: &mut TxContext, + ) { + assert!(kiosk_extension::is_installed(renter_kiosk), EExtensionNotInstalled); + + let rentable = take_from_bag(borrower_kiosk, Rented { id: item_id }); + + let Rentable { + object, + duration, + start_date, + price_per_day: _, + kiosk_id, + } = rentable; + + assert!(object::id(renter_kiosk) == kiosk_id, EInvalidKiosk); + + let start_date_ms = *option::borrow(&start_date); + let current_timestamp = clock.timestamp_ms(); + let final_timestamp = start_date_ms + duration * SECONDS_IN_A_DAY; + assert!(current_timestamp > final_timestamp, ERentingPeriodNotOver); + + if (transfer_policy.has_rule()) { + kiosk_extension::lock( + Rental {}, + renter_kiosk, + object, + transfer_policy, + ); + } else { + kiosk_extension::place( + Rental {}, + renter_kiosk, + object, + transfer_policy, + ); + }; + } + + // === Private Functions === + + fun take_from_bag( + kiosk: &mut Kiosk, + item: Key, + ) : Rentable { + let ext_storage_mut = kiosk_extension::storage_mut(Rental {}, kiosk); + assert!(bag::contains(ext_storage_mut, item), EObjectNotExist); + bag::remove>( + ext_storage_mut, + item, + ) + } + + fun place_in_bag( + kiosk: &mut Kiosk, + item: Key, + rentable: Rentable, + ) { + let ext_storage_mut = kiosk_extension::storage_mut(Rental {}, kiosk); + bag::add(ext_storage_mut, item, rentable); + } + + // === Test Functions === + + #[test_only] + // public fun test_take_from_bag(kiosk: &mut Kiosk, item_id: ID) { + public fun test_take_from_bag( + kiosk: &mut Kiosk, + item: Key, + ) { + let rentable = take_from_bag(kiosk, item); + + let Rentable { + object, + duration: _, + start_date: _, + price_per_day: _, + kiosk_id: _ + } = rentable; + + transfer::public_share_object(object); + } + + #[test_only] + public fun create_listed(id: ID) : Listed { + Listed { id } + } +}