From d9cc1d2ee5f390295d69d71dcbded4fa9f7e7775 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Wed, 20 Nov 2024 12:48:57 +0200 Subject: [PATCH 01/22] feat(move/examples): draft of kiosk nft marketplace using kiosk rules --- docs/examples/move/nft_marketplace/Move.toml | 36 ++ .../sources/nft_marketplace.move | 430 ++++++++++++++++++ .../tests/nft_marketplace_tests.move | 19 + 3 files changed, 485 insertions(+) create mode 100644 docs/examples/move/nft_marketplace/Move.toml create mode 100644 docs/examples/move/nft_marketplace/sources/nft_marketplace.move create mode 100644 docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move diff --git a/docs/examples/move/nft_marketplace/Move.toml b/docs/examples/move/nft_marketplace/Move.toml new file mode 100644 index 00000000000..045bb6a0158 --- /dev/null +++ b/docs/examples/move/nft_marketplace/Move.toml @@ -0,0 +1,36 @@ +[package] +name = "nft_marketplace" +edition = "2024.beta" + +[dependencies] +Iota = { local = "../../../../crates/iota-framework/packages/iota-framework" } +Kiosk = { local = "../../../../kiosk" } + +# For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. +# Revision can be a branch, a tag, and a commit hash. +# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } + +# For local dependencies use `local = path`. Path is relative to the package root +# Local = { local = "../path/to" } + +# To resolve a version conflict and force a specific version for dependency +# override use `override = true` +# Override = { local = "../conflicting/version", override = true } + +[addresses] +nft_marketplace = "0x0" + +# Named addresses will be accessible in Move as `@name`. They're also exported: +# for example, `std = "0x1"` is exported by the Standard Library. +# alice = "0xA11CE" + +[dev-dependencies] +# The dev-dependencies section allows overriding dependencies for `--test` and +# `--dev` modes. You can introduce test-only dependencies here. +# Local = { local = "../path/to/dev-build" } + +[dev-addresses] +# The dev-addresses section allows overwriting named addresses for the `--test` +# and `--dev` modes. +# alice = "0xB0B" + diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move new file mode 100644 index 00000000000..90f2319ea5b --- /dev/null +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -0,0 +1,430 @@ +module nft_marketplace::nft_marketplace { + + // iota imports + use iota::{ + kiosk::{Kiosk, KioskOwnerCap, purchase}, + kiosk_extension, + bag, + transfer_policy::{Self, TransferPolicy, TransferRequest, 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; + // 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::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; + + // === 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. + 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(Marketplace {}, 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.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); + }; + } + + /// Buy listed item + public fun buy(kiosk: &mut Kiosk, item_id: object::ID, payment: Coin): (T, TransferRequest){ + purchase(kiosk, item_id, payment) + } + + /// Confirm the TransferRequest + public fun confirm_request(policy: &TransferPolicy, req: TransferRequest) { + transfer_policy::confirm_request(policy, req); + } + + /// 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(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, + ) { + 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( + 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 { + let ext_storage_mut = kiosk_extension::storage_mut(Marketplace {}, 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(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 } + } +} diff --git a/docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move b/docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move new file mode 100644 index 00000000000..614ef2a0778 --- /dev/null +++ b/docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move @@ -0,0 +1,19 @@ +/* +#[test_only] +module nft_marketplace::nft_marketplace_tests { + // uncomment this line to import the module + // use nft_marketplace::nft_marketplace; + + const ENotImplemented: u64 = 0; + + #[test] + fun test_nft_marketplace() { + // pass + } + + #[test, expected_failure(abort_code = ::nft_marketplace::nft_marketplace_tests::ENotImplemented)] + fun test_nft_marketplace_fail() { + abort ENotImplemented + } +} +*/ From e5b37b3b3c9580a3bb02800625538e90bdbcd043 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Fri, 22 Nov 2024 01:31:32 +0200 Subject: [PATCH 02/22] feat(move/examples/nft_marketplace): update buy function with royalties rule --- .../sources/nft_marketplace.move | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move index 90f2319ea5b..f52c13437c4 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -5,7 +5,7 @@ module nft_marketplace::nft_marketplace { kiosk::{Kiosk, KioskOwnerCap, purchase}, kiosk_extension, bag, - transfer_policy::{Self, TransferPolicy, TransferRequest, TransferPolicyCap, has_rule}, + transfer_policy::{Self, TransferPolicy, TransferRequest, TransferPolicyCap, has_rule, get_rule}, clock::Clock, coin::{Self, Coin}, balance::{Self, Balance}, @@ -17,7 +17,8 @@ module nft_marketplace::nft_marketplace { 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::Rule as RoyaltyRule; + use kiosk::royalty_rule; // use kiosk::witness_rule::Rule as WitnessRule; // === Errors === @@ -28,6 +29,7 @@ module nft_marketplace::nft_marketplace { const ERentingPeriodNotOver: u64 = 4; const EObjectNotExist: u64 = 5; const ETotalPriceOverflow: u64 = 6; + const EInvalidRoyaltiesAmount: u64 = 7; // === Constants === const PERMISSIONS: u128 = 11; @@ -95,6 +97,13 @@ module nft_marketplace::nft_marketplace { policy_cap: TransferPolicyCap } + /// 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. @@ -137,6 +146,9 @@ module nft_marketplace::nft_marketplace { 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. @@ -201,14 +213,21 @@ module nft_marketplace::nft_marketplace { }; } - /// Buy listed item - public fun buy(kiosk: &mut Kiosk, item_id: object::ID, payment: Coin): (T, TransferRequest){ - purchase(kiosk, item_id, payment) - } - - /// Confirm the TransferRequest - public fun confirm_request(policy: &TransferPolicy, req: TransferRequest) { - transfer_policy::confirm_request(policy, req); + /// 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); + 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(); + }; + transfer_policy::confirm_request(policy, transfer_request); + transfer::public_transfer(item, ctx.sender()); } /// This enables individuals to rent a listed Rentable. From 4b11522cab4bab4c352b8abe0924c003981ef84f Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Fri, 22 Nov 2024 02:55:39 +0200 Subject: [PATCH 03/22] 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 } + } +} From ac68331feba329874e1152b715193714cba868de Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Fri, 22 Nov 2024 11:42:56 +0200 Subject: [PATCH 04/22] readme updated --- docs/examples/move/nft_marketplace/README.md | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/examples/move/nft_marketplace/README.md diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md new file mode 100644 index 00000000000..b0efd25e3ad --- /dev/null +++ b/docs/examples/move/nft_marketplace/README.md @@ -0,0 +1,113 @@ +# Marketplace Guide + +The `nft_marketplace.move` module provides a straightforward implementation of a marketplace extension. To utilize it, follow the steps outlined below. +The `item_for_market.move` contains mocked item data for use within the marketplace. +The `rental_extension.move` is an extension adds functionality to enable item rentals. + + +## Steps to Use the Marketplace + +### 1. Connect to the Network +Connect to the IOTA network (e.g., using a faucet to obtain tokens). + +### 2. Install Kiosk +Run the following command to install the Kiosk module: + +```bash +iota client call \ + --package 0x2 \ + --module kiosk \ + --function default +``` + +### 3. Publish `item_for_market.move` + +```bash +iota client publish +``` + +### 4. Create an Item + +Create an item, for instance: + +```bash +iota client call \ + --package $M_ITEMS_ID \ + --module market_items \ + --function new_jeans +``` + +After creation, export the following variables: + +- PACKAGE_ID +- ITEM_ID +- PUBLISHER_ID + +### 5. Create a Transfer Policy + +Set up a transfer policy for the created item using the command: +```bash +iota client call \ + --package 0x2 \ + --module transfer_policy \ + --function default \ + --gas-budget 10000000 \ + --args $PUBLISHER_ID \ + --type-args "$ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" +``` + +### 6. Publish marketplac extension +Publish the nft_marketplace.move module: +```bash +iota client publish` +``` + +### 7. Install the Extension on the Kiosk + +Install the marketplace extension on the created kiosk using the command: +```bash +iota client call \ + --package $MARKETPLACE_ID \ + --module nft_marketplace \ + --function install \ + --args $KIOSK_ID $KIOSK_CAP_ID +``` + +### 8. Set a Price for the Item + +Set the price for the item: +```bash +iota client call \ + --package $MARKETPLACE_ID \ + --module nft_marketplace \ + --function set_price \ + --args $KIOSK_ID $KIOSK_CAP_ID $ITEM_ID 50000 \ + --type-args "&ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" + +``` + +### 9.(Optional) Set Royalties + +Set royalties for the item: + +```bash +iota client call \ + --package $MARKETPLACE \ + --module nft_marketplace \ + --function setup_royalties \ + --args $ITEM_TRANS_POLICY_ID $ITEM_TRANS_POLICY_CAP_ID 5000 2000 \ + --type-args "ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" +``` + +### 10. Buy an Item: + +To purchase the item: +```bash +iota client call \ + --package $MARKETPLACE \ + --module nft_marketplace \ + --function buy_item \ + --args $KIOSK_ID $ITEM_TRANS_POLICY_ID &ITEM_ID $COIN_ID \ + --type-args "ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" + +``` \ No newline at end of file From b6e5d8845f0dbe7da279c8314d00de91b3684ef2 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Fri, 22 Nov 2024 16:29:29 +0200 Subject: [PATCH 05/22] fix(docs/kiosk): update some Kiosk links, add more examples --- docs/content/developer/iota-101/nft/rent-nft.mdx | 4 ++-- docs/content/developer/standards/kiosk-apps.mdx | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/content/developer/iota-101/nft/rent-nft.mdx b/docs/content/developer/iota-101/nft/rent-nft.mdx index 86e0f4f5748..d4e93624b30 100644 --- a/docs/content/developer/iota-101/nft/rent-nft.mdx +++ b/docs/content/developer/iota-101/nft/rent-nft.mdx @@ -77,10 +77,10 @@ The rental smart contract utilizes th [Kiosk Apps](../../standards/kiosk-apps.md Both lenders and borrowers must install a Kiosk extension to participate. Additionally, the creator of the NFT type must create a rental policy and a `ProtectedTP` object to allow the extension to manage rentals while enforcing royalties. -## Move Module Details TODO UPDATE LINK +## Move Module Details The NFT rental functionality is implemented in a single Move module: `nft_rental.move`. -You can find the source code in the [IOTA repository](https://github.com/iotaledger/iota/tree/main/examples/move/nft-rental/sources/nft_rental.move) under the `examples` directory. The code includes comments to help you understand the logic and structure. +You can find the source code in the [IOTA repository](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/rental_extension.move) under the `examples` directory. The code includes comments to help you understand the logic and structure. ### The `nft_rental` Module diff --git a/docs/content/developer/standards/kiosk-apps.mdx b/docs/content/developer/standards/kiosk-apps.mdx index 116cdccf28f..7a8f8aca9a3 100644 --- a/docs/content/developer/standards/kiosk-apps.mdx +++ b/docs/content/developer/standards/kiosk-apps.mdx @@ -259,5 +259,7 @@ txb.moveCall({ ## Related links - [NFT Rental](../iota-101/nft/rent-nft.mdx): An example implementation of the Kiosk Apps standard that enables renting NFTs. +- [NFT Rental Extension](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/rental_extension.move): An example implementation of the Kiosk Extension standard that enables renting NFTs. +- [Marketplace Extension](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/nft_marketplace.move): Move package that contains the source code(Kiosk extension) for the marketplace app. \ No newline at end of file From 8c7d374dfbaef917f8d91e2eb8e449216cb4965a Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Mon, 25 Nov 2024 10:30:48 +0200 Subject: [PATCH 06/22] dprint --- docs/examples/move/nft_marketplace/Move.toml | 1 - docs/examples/move/nft_marketplace/README.md | 13 +++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/examples/move/nft_marketplace/Move.toml b/docs/examples/move/nft_marketplace/Move.toml index 045bb6a0158..11388926dfb 100644 --- a/docs/examples/move/nft_marketplace/Move.toml +++ b/docs/examples/move/nft_marketplace/Move.toml @@ -33,4 +33,3 @@ nft_marketplace = "0x0" # The dev-addresses section allows overwriting named addresses for the `--test` # and `--dev` modes. # alice = "0xB0B" - diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index b0efd25e3ad..de2c1383061 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -4,13 +4,14 @@ The `nft_marketplace.move` module provides a straightforward implementation of a The `item_for_market.move` contains mocked item data for use within the marketplace. The `rental_extension.move` is an extension adds functionality to enable item rentals. - ## Steps to Use the Marketplace ### 1. Connect to the Network + Connect to the IOTA network (e.g., using a faucet to obtain tokens). ### 2. Install Kiosk + Run the following command to install the Kiosk module: ```bash @@ -46,6 +47,7 @@ After creation, export the following variables: ### 5. Create a Transfer Policy Set up a transfer policy for the created item using the command: + ```bash iota client call \ --package 0x2 \ @@ -57,7 +59,9 @@ iota client call \ ``` ### 6. Publish marketplac extension + Publish the nft_marketplace.move module: + ```bash iota client publish` ``` @@ -65,6 +69,7 @@ iota client publish` ### 7. Install the Extension on the Kiosk Install the marketplace extension on the created kiosk using the command: + ```bash iota client call \ --package $MARKETPLACE_ID \ @@ -76,6 +81,7 @@ iota client call \ ### 8. Set a Price for the Item Set the price for the item: + ```bash iota client call \ --package $MARKETPLACE_ID \ @@ -83,7 +89,6 @@ iota client call \ --function set_price \ --args $KIOSK_ID $KIOSK_CAP_ID $ITEM_ID 50000 \ --type-args "&ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" - ``` ### 9.(Optional) Set Royalties @@ -102,6 +107,7 @@ iota client call \ ### 10. Buy an Item: To purchase the item: + ```bash iota client call \ --package $MARKETPLACE \ @@ -109,5 +115,4 @@ iota client call \ --function buy_item \ --args $KIOSK_ID $ITEM_TRANS_POLICY_ID &ITEM_ID $COIN_ID \ --type-args "ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" - -``` \ No newline at end of file +``` From 24f3516b029ecf3d5898403de43d7620f5e00d7d Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Mon, 25 Nov 2024 12:43:00 +0200 Subject: [PATCH 07/22] update readme --- docs/examples/move/nft_marketplace/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index b0efd25e3ad..a7c30168663 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -56,10 +56,16 @@ iota client call \ --type-args "$ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" ``` -### 6. Publish marketplac extension +### 6. Publish rules and marketplace extension + +Publish Kiosk rules modules: +```bash +iota client publish iota/kiosk/Move.toml +``` + Publish the nft_marketplace.move module: ```bash -iota client publish` +iota client publish iota/docs/examples/move/nft_marketplace/sources/nft_marketplace.move` ``` ### 7. Install the Extension on the Kiosk From 17b658292a70a39044c2c4c9155aa0b3ec7eee68 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Tue, 26 Nov 2024 14:15:51 +0200 Subject: [PATCH 08/22] review comments fix, improving readme descroption, minor code refactoring --- docs/examples/move/nft_marketplace/Move.toml | 1 - docs/examples/move/nft_marketplace/README.md | 28 ++++++++++++----- .../sources/nft_marketplace.move | 16 +++++++--- .../sources/rental_extension.move | 31 ++++++++++++++----- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/docs/examples/move/nft_marketplace/Move.toml b/docs/examples/move/nft_marketplace/Move.toml index 045bb6a0158..11388926dfb 100644 --- a/docs/examples/move/nft_marketplace/Move.toml +++ b/docs/examples/move/nft_marketplace/Move.toml @@ -33,4 +33,3 @@ nft_marketplace = "0x0" # The dev-addresses section allows overwriting named addresses for the `--test` # and `--dev` modes. # alice = "0xB0B" - diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index a7c30168663..8fc1e78b23b 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -4,13 +4,15 @@ The `nft_marketplace.move` module provides a straightforward implementation of a The `item_for_market.move` contains mocked item data for use within the marketplace. The `rental_extension.move` is an extension adds functionality to enable item rentals. - ## Steps to Use the Marketplace ### 1. Connect to the Network + Connect to the IOTA network (e.g., using a faucet to obtain tokens). ### 2. Install Kiosk + +By installation, we mean creating a Kiosk object and an OwnerCap, then transferring them to the caller. Run the following command to install the Kiosk module: ```bash @@ -39,13 +41,17 @@ iota client call \ After creation, export the following variables: -- PACKAGE_ID -- ITEM_ID -- PUBLISHER_ID +- `PACKAGE_ID`: The ID of the `market_items` package. +- `ITEM_ID`: The ID of the published item (in this case, Jeans). +- `PUBLISHER_ID`: The ID of the publisher object created during package publishing." ### 5. Create a Transfer Policy +`TransferPolicy` is a generic shared object acting as a central authority enforcing everyone to check their purchase is valid against the defined policy before the purchased item is transferred to the buyers. Object is specified by concrete type. +`default` function creates `TransferPolicy` object and an OwnerCap, then transferring them to the caller. + Set up a transfer policy for the created item using the command: + ```bash iota client call \ --package 0x2 \ @@ -59,18 +65,23 @@ iota client call \ ### 6. Publish rules and marketplace extension Publish Kiosk rules modules: + ```bash iota client publish iota/kiosk/Move.toml ``` Publish the nft_marketplace.move module: + ```bash iota client publish iota/docs/examples/move/nft_marketplace/sources/nft_marketplace.move` ``` ### 7. Install the Extension on the Kiosk +The install function enables installation of the Marketplace extension in a kiosk. +Under the hood it invokes `kiosk_extension::add` that adds extension to the Kiosk via dynamic field. Install the marketplace extension on the created kiosk using the command: + ```bash iota client call \ --package $MARKETPLACE_ID \ @@ -82,6 +93,7 @@ iota client call \ ### 8. Set a Price for the Item Set the price for the item: + ```bash iota client call \ --package $MARKETPLACE_ID \ @@ -89,11 +101,11 @@ iota client call \ --function set_price \ --args $KIOSK_ID $KIOSK_CAP_ID $ITEM_ID 50000 \ --type-args "&ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" - ``` ### 9.(Optional) Set Royalties +Royalties are a percentage of the item's price or revenue paid to the owner for the use or sale of their asset Set royalties for the item: ```bash @@ -107,7 +119,10 @@ iota client call \ ### 10. Buy an Item: +Here, when we buy an item, we pay the owner the item's price. If the royalty rule is enabled, an additional royalty fee, calculated as a percentage of the initial item price, is also paid. Once both payments are completed, the item is transferred to the buyer. + To purchase the item: + ```bash iota client call \ --package $MARKETPLACE \ @@ -115,5 +130,4 @@ iota client call \ --function buy_item \ --args $KIOSK_ID $ITEM_TRANS_POLICY_ID &ITEM_ID $COIN_ID \ --type-args "ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" - -``` \ 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 b49b4979d53..b48c9096176 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -22,7 +22,7 @@ module nft_marketplace::nft_marketplace { const EObjectNotExist: u64 = 1; // === Constants === - const PERMISSIONS: u128 = 11; + const ALLOW_PLACE_AND_LOCK: u128 = 11; /// Extension Key for Kiosk Marketplace extension. public struct Marketplace has drop {} @@ -35,13 +35,15 @@ module nft_marketplace::nft_marketplace { price: u64, } + // === 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(Marketplace {}, kiosk, cap, PERMISSIONS, ctx); + kiosk_extension::add(Marketplace {}, kiosk, cap, ALLOW_PLACE_AND_LOCK, ctx); } /// Remove the extension from the Kiosk. Can only be performed by the owner, @@ -50,15 +52,19 @@ module nft_marketplace::nft_marketplace { kiosk_extension::remove(kiosk, cap); } + /// Setup item royalty percentage + /// - amount_bp - the percentage of the purchase price to be paid as a + /// fee, denominated in basis points (100_00 = 100%, 1 = 0.01%). + /// - min_amount - the minimum amount to be paid as a fee if the relative + /// amount is lower than this setting. 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); } - /// Buy listed item and pay royalties if needed + /// Buy listed item with the indicated price and pay royalties if needed 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 ItemPrice { price } = take_from_bag(kiosk, Listed { id: item_id }); 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); diff --git a/docs/examples/move/nft_marketplace/sources/rental_extension.move b/docs/examples/move/nft_marketplace/sources/rental_extension.move index 1bb4b2722f5..7599f9be54b 100644 --- a/docs/examples/move/nft_marketplace/sources/rental_extension.move +++ b/docs/examples/move/nft_marketplace/sources/rental_extension.move @@ -1,3 +1,11 @@ +/// NFT renting is a mechanism that allows individuals without ownership or possession of a specific +/// NFT to temporarily utilize or experience it. +/// The rental_extension module provides an API that facilitates lending or borrowing through the following operations: +/// - List for renting +/// - Delist from renting +/// - Rent +/// - Borrow by reference and borrow by value +/// - Reclaim for the lender module nft_marketplace::rental_extension { // iota imports @@ -26,7 +34,7 @@ module nft_marketplace::rental_extension { const ETotalPriceOverflow: u64 = 6; // === Constants === - const PERMISSIONS: u128 = 11; + const ALLOW_PLACE_AND_LOCK: 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; @@ -93,13 +101,13 @@ module nft_marketplace::rental_extension { // === Public Functions === - /// Enables someone to install the Marketplace extension in their Kiosk. + /// Enables someone to install the Rental extension in their Kiosk. public fun install( kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext, ) { - kiosk_extension::add(Rental {}, kiosk, cap, PERMISSIONS, ctx); + kiosk_extension::add(Rental {}, kiosk, cap, ALLOW_PLACE_AND_LOCK, ctx); } /// Remove the extension from the Kiosk. Can only be performed by the owner, @@ -225,11 +233,7 @@ module nft_marketplace::rental_extension { 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_amount = calculate_fees_amount(coin_value as u128, rental_policy.amount_bp as u128); let fees = coin.split(fees_amount as u64, ctx); @@ -368,6 +372,17 @@ module nft_marketplace::rental_extension { // === Private Functions === + // Calculate fees_amount using the given basis points amount (percentage), ensuring the + // result fits into a 64-bit unsigned integer. + fun calculate_fees_amount(coin_value: u128, rental_amount_bp: u128): u128 { + + let mut fees_amount = coin_value as u128; + fees_amount = fees_amount * (rental_amount_bp as u128); + fees_amount = fees_amount / (MAX_BASIS_POINTS as u128); + + fees_amount + } + fun take_from_bag( kiosk: &mut Kiosk, item: Key, From 0eda47f0fc1fcc4edd9c5724acfb9d32d76dc152 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Tue, 26 Nov 2024 14:25:03 +0200 Subject: [PATCH 09/22] ETotalPriceOverflow check moved to list method from the rent --- .../move/nft_marketplace/sources/rental_extension.move | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/examples/move/nft_marketplace/sources/rental_extension.move b/docs/examples/move/nft_marketplace/sources/rental_extension.move index 7599f9be54b..4db493e356f 100644 --- a/docs/examples/move/nft_marketplace/sources/rental_extension.move +++ b/docs/examples/move/nft_marketplace/sources/rental_extension.move @@ -156,6 +156,7 @@ module nft_marketplace::rental_extension { ) { assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); + assert!(price_per_day * duration <= MAX_VALUE_U64, ETotalPriceOverflow); kiosk.set_owner(cap, ctx); kiosk.list(cap, item_id, 0); @@ -226,8 +227,6 @@ module nft_marketplace::rental_extension { 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(); From c140b109511abe97a2388239e85d7a4a1675f8ab Mon Sep 17 00:00:00 2001 From: Pavlo Botnar Date: Tue, 26 Nov 2024 14:28:58 +0200 Subject: [PATCH 10/22] Update docs/examples/move/nft_marketplace/sources/nft_marketplace.move Co-authored-by: Mirko Zichichi --- docs/examples/move/nft_marketplace/sources/nft_marketplace.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move index b48c9096176..e6a07a1d07c 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -80,7 +80,7 @@ module nft_marketplace::nft_marketplace { } - public fun set_price( + public fun set_price( kiosk: &mut Kiosk, cap: &KioskOwnerCap, item: T, From c056138b90e36c62132cdf3399be07ca89bc1d5f Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Tue, 26 Nov 2024 14:34:12 +0200 Subject: [PATCH 11/22] Minor code refactroing --- docs/examples/move/nft_marketplace/sources/item_for_market.move | 1 + docs/examples/move/nft_marketplace/sources/nft_marketplace.move | 1 - docs/examples/move/nft_marketplace/sources/rental_extension.move | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/examples/move/nft_marketplace/sources/item_for_market.move b/docs/examples/move/nft_marketplace/sources/item_for_market.move index cfea0bfef5a..2a11e131717 100644 --- a/docs/examples/move/nft_marketplace/sources/item_for_market.move +++ b/docs/examples/move/nft_marketplace/sources/item_for_market.move @@ -1,3 +1,4 @@ +/// Module provides `mock` items for using them in nft_marketplace and rental extensions. module nft_marketplace::market_items { use iota::package; /// One Time Witness. diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move index e6a07a1d07c..571302f2eec 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -105,7 +105,6 @@ module nft_marketplace::nft_marketplace { item_key: Key, ) : ItemPrice { let ext_storage_mut = kiosk_extension::storage_mut(Marketplace {}, kiosk); - assert!(bag::contains(ext_storage_mut, item_key), EObjectNotExist); bag::remove>( ext_storage_mut, item_key, diff --git a/docs/examples/move/nft_marketplace/sources/rental_extension.move b/docs/examples/move/nft_marketplace/sources/rental_extension.move index 4db493e356f..592beb2049e 100644 --- a/docs/examples/move/nft_marketplace/sources/rental_extension.move +++ b/docs/examples/move/nft_marketplace/sources/rental_extension.move @@ -387,7 +387,6 @@ module nft_marketplace::rental_extension { 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, From 79a64126c273942fc5a025cd443e85b2dd0a1122 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Tue, 26 Nov 2024 15:20:02 +0200 Subject: [PATCH 12/22] Minor fix --- docs/examples/move/nft_marketplace/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index 8fc1e78b23b..2a9b4748dfc 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -105,7 +105,8 @@ iota client call \ ### 9.(Optional) Set Royalties -Royalties are a percentage of the item's price or revenue paid to the owner for the use or sale of their asset +Royalties are a percentage of the item's price or revenue paid to the owner for the use or sale of their asset. + Set royalties for the item: ```bash From 97dc111570c30b5dba7ad9eca7b346a206cdf82d Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Wed, 27 Nov 2024 16:16:11 +0200 Subject: [PATCH 13/22] Fix review comments, improvements of bying flow --- docs/examples/move/nft_marketplace/README.md | 127 ++++++++++++------ ...em_for_market.move => clothing_store.move} | 6 +- .../sources/nft_marketplace.move | 43 +++--- .../sources/rental_extension.move | 1 - 4 files changed, 117 insertions(+), 60 deletions(-) rename docs/examples/move/nft_marketplace/sources/{item_for_market.move => clothing_store.move} (89%) diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index 2a9b4748dfc..8078004157c 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -22,28 +22,47 @@ iota client call \ --function default ``` -### 3. Publish `item_for_market.move` +### 3. Publish `nft_marketplace` package + +#### 3.1(Optional) Publish Kiosk rules modules if these are not present in the network you are using + +Publish Kiosk rules modules(package): ```bash -iota client publish +iota client publish iota/kiosk ``` -### 4. Create an Item +After publishing, export the following variable: + +- `RULES_PACKAGE_ID`: The ID of rules package. + +#### 3.2 Publish marketplace extension + +Publish the nft_marketplace.move module: + +```bash +iota client publish iota/docs/examples/move/nft_marketplace +``` + +After publishing, export the following variables: + +- `MARKETPLACE_PACKAGE_ID`: The ID of whole marketplace package. +- `MARKETPLACE_PUBLISHER_ID`: The ID of the publisher object created during marketplace package publishing." + +### 4. Create an Clothing Store Item Create an item, for instance: ```bash iota client call \ - --package $M_ITEMS_ID \ - --module market_items \ + --package $MARKETPLACE_PACKAGE_ID \ + --module clothing_store \ --function new_jeans ``` -After creation, export the following variables: +After creation, export the following variable: -- `PACKAGE_ID`: The ID of the `market_items` package. -- `ITEM_ID`: The ID of the published item (in this case, Jeans). -- `PUBLISHER_ID`: The ID of the publisher object created during package publishing." +- `CLOTHING_STORE_ITEM_ID`: The ID of the published item (in this case, Jeans). ### 5. Create a Transfer Policy @@ -58,25 +77,16 @@ iota client call \ --module transfer_policy \ --function default \ --gas-budget 10000000 \ - --args $PUBLISHER_ID \ - --type-args "$ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" + --args $MARKETPLACE_PUBLISHER_ID \ + --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" ``` -### 6. Publish rules and marketplace extension - -Publish Kiosk rules modules: +After publishing, export the following variables: -```bash -iota client publish iota/kiosk/Move.toml -``` +- `ITEM_TRANS_POLICY`: The ID of the item transfer policy object. +- `ITEM_TRANS_POLICY_CAP`: The ID of the item transfer policy object owner capability" -Publish the nft_marketplace.move module: - -```bash -iota client publish iota/docs/examples/move/nft_marketplace/sources/nft_marketplace.move` -``` - -### 7. Install the Extension on the Kiosk +### 6. Install the Extension on the Kiosk The install function enables installation of the Marketplace extension in a kiosk. Under the hood it invokes `kiosk_extension::add` that adds extension to the Kiosk via dynamic field. @@ -84,26 +94,26 @@ Install the marketplace extension on the created kiosk using the command: ```bash iota client call \ - --package $MARKETPLACE_ID \ + --package $MARKETPLACE_PACKAGE_ID \ --module nft_marketplace \ --function install \ --args $KIOSK_ID $KIOSK_CAP_ID ``` -### 8. Set a Price for the Item +### 7. Set a Price for the Item Set the price for the item: ```bash iota client call \ - --package $MARKETPLACE_ID \ + --package $MARKETPLACE_PACKAGE_ID \ --module nft_marketplace \ --function set_price \ - --args $KIOSK_ID $KIOSK_CAP_ID $ITEM_ID 50000 \ - --type-args "&ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" + --args $KIOSK_ID $KIOSK_CAP_ID $CLOTHING_STORE_ITEM_ID 50000 \ + --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" ``` -### 9.(Optional) Set Royalties +### 8.(Optional) Set Royalties Royalties are a percentage of the item's price or revenue paid to the owner for the use or sale of their asset. @@ -111,24 +121,59 @@ Set royalties for the item: ```bash iota client call \ - --package $MARKETPLACE \ + --package $MARKETPLACE_PACKAGE_ID \ --module nft_marketplace \ --function setup_royalties \ - --args $ITEM_TRANS_POLICY_ID $ITEM_TRANS_POLICY_CAP_ID 5000 2000 \ - --type-args "ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" + --args $ITEM_TRANS_POLICY $ITEM_TRANS_POLICY_CAP 5000 2000 \ + --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" ``` -### 10. Buy an Item: +### 9. Buy an Item: -Here, when we buy an item, we pay the owner the item's price. If the royalty rule is enabled, an additional royalty fee, calculated as a percentage of the initial item price, is also paid. Once both payments are completed, the item is transferred to the buyer. +#### 9.1 Get the Item Price: + +```bash +iota client ptb \ +--move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ +``` + +#### 9.2(Optional) Calculate rolyalties of the Item: + +```bash +--move-call $RULES_PACKAGE_ID::royalty_rule::fee_amount "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$ITEM_TRANS_POLICY item_price --assign royalties_amount \ +``` + +#### 9.3 Create a payment coin with a specific amount (price + optional royalties): + +```bash +--split-coins gas "[item_price, royalties_amount]" --assign payment_coins \ +--merge-coins payment_coins.0 "[payment_coins.1]" \ +``` + +#### 9.4 Buy an Item using `payment_coins.0`: + +Here, when we buy an item, we pay the owner the item's price. If the royalty rule is enabled, an additional royalty fee, calculated as a percentage of the initial item price, is also paid. Once both payments are completed, the item is ready for transferring to the buyer. To purchase the item: ```bash -iota client call \ - --package $MARKETPLACE \ - --module nft_marketplace \ - --function buy_item \ - --args $KIOSK_ID $ITEM_TRANS_POLICY_ID &ITEM_ID $COIN_ID \ - --type-args "ITEM_FOR_MARKET_PACKAGE_ID::market_items::Jeans" +--move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item +``` + +#### 9.5 Transfer an Item to the buyer: + +```bash +--move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @ \ +``` + +The final purchase PTB request, including royalties, should look like this: + +```bash +iota client ptb \ +--move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ +--move-call $RULES_PACKAGE_ID::royalty_rule::fee_amount "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$ITEM_TRANS_POLICY item_price --assign royalties_amount \ +--split-coins gas "[item_price, royalties_amount]" --assign payment_coins \ +--merge-coins payment_coins.0 "[payment_coins.1]" \ +--move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item \ +--move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @ ``` diff --git a/docs/examples/move/nft_marketplace/sources/item_for_market.move b/docs/examples/move/nft_marketplace/sources/clothing_store.move similarity index 89% rename from docs/examples/move/nft_marketplace/sources/item_for_market.move rename to docs/examples/move/nft_marketplace/sources/clothing_store.move index 2a11e131717..55294e84d75 100644 --- a/docs/examples/move/nft_marketplace/sources/item_for_market.move +++ b/docs/examples/move/nft_marketplace/sources/clothing_store.move @@ -1,11 +1,11 @@ /// Module provides `mock` items for using them in nft_marketplace and rental extensions. -module nft_marketplace::market_items { +module nft_marketplace::clothing_store { use iota::package; /// One Time Witness. - public struct MARKET_ITEMS has drop {} + public struct CLOTHING_STORE has drop {} - fun init(otw: MARKET_ITEMS, ctx: &mut TxContext) { + fun init(otw: CLOTHING_STORE, ctx: &mut TxContext) { package::claim_and_keep(otw, ctx) } diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move index 571302f2eec..f34e6c36b66 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -10,16 +10,13 @@ module nft_marketplace::nft_marketplace { }; // rules imports - // 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 EObjectNotExist: u64 = 1; + const EWrongPaymentRoyalties: u64 = 1; // === Constants === const ALLOW_PLACE_AND_LOCK: u128 = 11; @@ -30,7 +27,7 @@ module nft_marketplace::nft_marketplace { /// 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 } - public struct ItemPrice has store { + public struct ItemPrice has store { /// Total amount of time offered for renting in days. price: u64, } @@ -62,21 +59,23 @@ module nft_marketplace::nft_marketplace { } /// Buy listed item with the indicated price and pay royalties if needed - public fun buy_item(kiosk: &mut Kiosk, policy: &mut TransferPolicy, item_id: object::ID, mut payment: Coin, ctx: &mut TxContext) { + public fun buy_item(kiosk: &mut Kiosk, policy: &mut TransferPolicy, item_id: object::ID, mut payment: Coin, ctx: &mut TxContext): T { assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); let ItemPrice { price } = take_from_bag(kiosk, Listed { id: item_id }); - 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); + + let payment_amount_value = payment.value(); + let coin_price = payment.split(price, ctx); + + let (item, mut transfer_request) = purchase(kiosk, item_id, coin_price); if (policy.has_rule()) { - 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); + let royalties_value = royalty_rule::fee_amount(policy, price); + assert!(payment_amount_value == price + royalties_value, EWrongPaymentRoyalties); + royalty_rule::pay(policy, &mut transfer_request, payment); + } else { + payment.destroy_zero(); }; 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()); + item } @@ -98,6 +97,20 @@ module nft_marketplace::nft_marketplace { } + public fun get_item_price( + kiosk: &Kiosk, + item_id: ID, + ) : u64 { + let storage_ref = kiosk_extension::storage(Marketplace {}, kiosk); + let ItemPrice { price } = bag::borrow>( + storage_ref, + Listed { id: item_id }, + ); + + *price + } + + // === Private Functions === fun take_from_bag( diff --git a/docs/examples/move/nft_marketplace/sources/rental_extension.move b/docs/examples/move/nft_marketplace/sources/rental_extension.move index 592beb2049e..f8502c11dc6 100644 --- a/docs/examples/move/nft_marketplace/sources/rental_extension.move +++ b/docs/examples/move/nft_marketplace/sources/rental_extension.move @@ -30,7 +30,6 @@ module nft_marketplace::rental_extension { const ENotEnoughCoins: u64 = 2; const EInvalidKiosk: u64 = 3; const ERentingPeriodNotOver: u64 = 4; - const EObjectNotExist: u64 = 5; const ETotalPriceOverflow: u64 = 6; // === Constants === From 2c859cfa6780483a7d475ac24f164fe5de5f9a7a Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Wed, 27 Nov 2024 16:20:02 +0200 Subject: [PATCH 14/22] Minor space fixes --- docs/examples/move/nft_marketplace/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index 8078004157c..3a6f8c04eb5 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -16,7 +16,7 @@ By installation, we mean creating a Kiosk object and an OwnerCap, then transferr Run the following command to install the Kiosk module: ```bash -iota client call \ +iota client call \ --package 0x2 \ --module kiosk \ --function default @@ -72,7 +72,7 @@ After creation, export the following variable: Set up a transfer policy for the created item using the command: ```bash -iota client call \ +iota client call \ --package 0x2 \ --module transfer_policy \ --function default \ From eb830f7e57a9d2fe0a560020fa3feebf7d7574df Mon Sep 17 00:00:00 2001 From: Pavlo Botnar Date: Wed, 27 Nov 2024 16:59:05 +0200 Subject: [PATCH 15/22] Update docs/examples/move/nft_marketplace/sources/clothing_store.move Co-authored-by: Mirko Zichichi --- docs/examples/move/nft_marketplace/sources/clothing_store.move | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/examples/move/nft_marketplace/sources/clothing_store.move b/docs/examples/move/nft_marketplace/sources/clothing_store.move index 55294e84d75..6c0f5636ed2 100644 --- a/docs/examples/move/nft_marketplace/sources/clothing_store.move +++ b/docs/examples/move/nft_marketplace/sources/clothing_store.move @@ -1,4 +1,5 @@ /// Module provides `mock` items for using them in nft_marketplace and rental extensions. +#[allow(lint(self_transfer))] module nft_marketplace::clothing_store { use iota::package; /// One Time Witness. From 6535b5ccfa9843afd274efd30a41435fa18f193c Mon Sep 17 00:00:00 2001 From: Pavlo Botnar Date: Wed, 27 Nov 2024 16:59:18 +0200 Subject: [PATCH 16/22] Update docs/examples/move/nft_marketplace/sources/nft_marketplace.move Co-authored-by: Mirko Zichichi --- docs/examples/move/nft_marketplace/sources/nft_marketplace.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move index f34e6c36b66..6c994dadce3 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/nft_marketplace.move @@ -54,7 +54,7 @@ module nft_marketplace::nft_marketplace { /// fee, denominated in basis points (100_00 = 100%, 1 = 0.01%). /// - min_amount - the minimum amount to be paid as a fee if the relative /// amount is lower than this setting. - public fun setup_royalties(policy: &mut TransferPolicy, cap: &TransferPolicyCap, amount_bp: u16, min_amount: u64, ctx: &mut TxContext) { + public fun setup_royalties(policy: &mut TransferPolicy, cap: &TransferPolicyCap, amount_bp: u16, min_amount: u64) { royalty_rule::add(policy, cap, amount_bp, min_amount); } From 4c8441c647bc58a0676586254bef631157add710 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Thu, 28 Nov 2024 12:02:19 +0200 Subject: [PATCH 17/22] Add tests for nft_marketplace package --- docs/examples/move/nft_marketplace/README.md | 18 +- .../sources/clothing_store.move | 2 +- ...tplace.move => marketplace_extension.move} | 4 +- .../sources/rental_extension.move | 3 +- .../tests/marketplace_extension_tests.move | 223 +++++++++++ .../tests/nft_marketplace_tests.move | 19 - .../tests/rental_extension_tests.move | 362 ++++++++++++++++++ .../move/nft_marketplace/tests/utils.move | 24 ++ 8 files changed, 624 insertions(+), 31 deletions(-) rename docs/examples/move/nft_marketplace/sources/{nft_marketplace.move => marketplace_extension.move} (96%) create mode 100644 docs/examples/move/nft_marketplace/tests/marketplace_extension_tests.move delete mode 100644 docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move create mode 100644 docs/examples/move/nft_marketplace/tests/rental_extension_tests.move create mode 100644 docs/examples/move/nft_marketplace/tests/utils.move diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index 3a6f8c04eb5..c44b4c71132 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -1,6 +1,6 @@ # Marketplace Guide -The `nft_marketplace.move` module provides a straightforward implementation of a marketplace extension. To utilize it, follow the steps outlined below. +The `marketplace_extension.move` module provides a straightforward implementation of a marketplace extension. To utilize it, follow the steps outlined below. The `item_for_market.move` contains mocked item data for use within the marketplace. The `rental_extension.move` is an extension adds functionality to enable item rentals. @@ -38,7 +38,7 @@ After publishing, export the following variable: #### 3.2 Publish marketplace extension -Publish the nft_marketplace.move module: +Publish the marketplace_extension.move module: ```bash iota client publish iota/docs/examples/move/nft_marketplace @@ -95,7 +95,7 @@ Install the marketplace extension on the created kiosk using the command: ```bash iota client call \ --package $MARKETPLACE_PACKAGE_ID \ - --module nft_marketplace \ + --module marketplace_extension \ --function install \ --args $KIOSK_ID $KIOSK_CAP_ID ``` @@ -107,7 +107,7 @@ Set the price for the item: ```bash iota client call \ --package $MARKETPLACE_PACKAGE_ID \ - --module nft_marketplace \ + --module marketplace_extension \ --function set_price \ --args $KIOSK_ID $KIOSK_CAP_ID $CLOTHING_STORE_ITEM_ID 50000 \ --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" @@ -122,7 +122,7 @@ Set royalties for the item: ```bash iota client call \ --package $MARKETPLACE_PACKAGE_ID \ - --module nft_marketplace \ + --module marketplace_extension \ --function setup_royalties \ --args $ITEM_TRANS_POLICY $ITEM_TRANS_POLICY_CAP 5000 2000 \ --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" @@ -134,7 +134,7 @@ iota client call \ ```bash iota client ptb \ ---move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ ``` #### 9.2(Optional) Calculate rolyalties of the Item: @@ -157,7 +157,7 @@ Here, when we buy an item, we pay the owner the item's price. If the royalty rul To purchase the item: ```bash ---move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item ``` #### 9.5 Transfer an Item to the buyer: @@ -170,10 +170,10 @@ The final purchase PTB request, including royalties, should look like this: ```bash iota client ptb \ ---move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ --move-call $RULES_PACKAGE_ID::royalty_rule::fee_amount "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$ITEM_TRANS_POLICY item_price --assign royalties_amount \ --split-coins gas "[item_price, royalties_amount]" --assign payment_coins \ --merge-coins payment_coins.0 "[payment_coins.1]" \ ---move-call $MARKETPLACE_PACKAGE_ID::nft_marketplace::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item \ +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item \ --move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @ ``` diff --git a/docs/examples/move/nft_marketplace/sources/clothing_store.move b/docs/examples/move/nft_marketplace/sources/clothing_store.move index 6c0f5636ed2..050c8f93a75 100644 --- a/docs/examples/move/nft_marketplace/sources/clothing_store.move +++ b/docs/examples/move/nft_marketplace/sources/clothing_store.move @@ -1,4 +1,4 @@ -/// Module provides `mock` items for using them in nft_marketplace and rental extensions. +/// Module provides `mock` items for using them in marketplace and rental extensions. #[allow(lint(self_transfer))] module nft_marketplace::clothing_store { use iota::package; diff --git a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move b/docs/examples/move/nft_marketplace/sources/marketplace_extension.move similarity index 96% rename from docs/examples/move/nft_marketplace/sources/nft_marketplace.move rename to docs/examples/move/nft_marketplace/sources/marketplace_extension.move index 6c994dadce3..54113826d0d 100644 --- a/docs/examples/move/nft_marketplace/sources/nft_marketplace.move +++ b/docs/examples/move/nft_marketplace/sources/marketplace_extension.move @@ -1,4 +1,4 @@ -module nft_marketplace::nft_marketplace { +module nft_marketplace::marketplace_extension { // iota imports use iota::{ kiosk::{Kiosk, KioskOwnerCap, purchase}, @@ -17,6 +17,7 @@ module nft_marketplace::nft_marketplace { // === Errors === const EExtensionNotInstalled: u64 = 0; const EWrongPaymentRoyalties: u64 = 1; + const ENotEnoughPaymentAmount: u64 = 2; // === Constants === const ALLOW_PLACE_AND_LOCK: u128 = 11; @@ -64,6 +65,7 @@ module nft_marketplace::nft_marketplace { let ItemPrice { price } = take_from_bag(kiosk, Listed { id: item_id }); let payment_amount_value = payment.value(); + assert!(payment_amount_value >= price, ENotEnoughPaymentAmount); let coin_price = payment.split(price, ctx); let (item, mut transfer_request) = purchase(kiosk, item_id, coin_price); diff --git a/docs/examples/move/nft_marketplace/sources/rental_extension.move b/docs/examples/move/nft_marketplace/sources/rental_extension.move index f8502c11dc6..e971e79b210 100644 --- a/docs/examples/move/nft_marketplace/sources/rental_extension.move +++ b/docs/examples/move/nft_marketplace/sources/rental_extension.move @@ -155,7 +155,8 @@ module nft_marketplace::rental_extension { ) { assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); - assert!(price_per_day * duration <= MAX_VALUE_U64, ETotalPriceOverflow); + let max_price_per_day = MAX_VALUE_U64 / duration; + assert!(price_per_day <= max_price_per_day, ETotalPriceOverflow); kiosk.set_owner(cap, ctx); kiosk.list(cap, item_id, 0); diff --git a/docs/examples/move/nft_marketplace/tests/marketplace_extension_tests.move b/docs/examples/move/nft_marketplace/tests/marketplace_extension_tests.move new file mode 100644 index 00000000000..d0e97eb016a --- /dev/null +++ b/docs/examples/move/nft_marketplace/tests/marketplace_extension_tests.move @@ -0,0 +1,223 @@ + +#[test_only] +module nft_marketplace::marketplace_extension_tests { + + use nft_marketplace::test_utils::{create_kiosk, create_transfer_policy}; + use kiosk::royalty_rule as royalty_rule; + use nft_marketplace::marketplace_extension::Self; + use iota::{ + iota::IOTA, + coin::Coin, + kiosk::Kiosk, + kiosk_test_utils, + package::Self, + test_scenario::{Self as ts, Scenario}, + transfer_policy::{TransferPolicy, TransferPolicyCap} + }; + + const CREATOR: address = @0xCCCC; + const SELLER: address = @0xAAAA; + const BUYER: address = @0xBBBB; + + public struct T has key, store { id: UID } + public struct WITNESS has drop {} + + + // ==================== Happy path scenarios ==================== + + #[test] + fun test_buy_item_without_royalties() { + let mut ts = ts::begin(SELLER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let seller_kiosk_id = create_kiosk(SELLER, ts.ctx()); + let item_price = 50000; + + create_transfer_policy( CREATOR, &publisher, ts.ctx()); + install_ext(&mut ts, SELLER, seller_kiosk_id); + setup_price(&mut ts, SELLER, seller_kiosk_id, item, item_price); + let payment = kiosk_test_utils::get_iota(item_price, ts.ctx()); + buy(&mut ts, BUYER, seller_kiosk_id, item_id, payment); + + publisher.burn_publisher(); + ts.end(); + } + + #[test] + fun test_buy_item_with_royalties() { + let mut ts = ts::begin(SELLER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let seller_kiosk_id = create_kiosk(SELLER, ts.ctx()); + let item_price = 50000; + let royalty_amount_bp = 5000; + let royalty_min_amount = 2000; + create_transfer_policy( CREATOR, &publisher, ts.ctx()); + add_royalty_rule(&mut ts, CREATOR, royalty_amount_bp, royalty_min_amount); + install_ext(&mut ts, SELLER, seller_kiosk_id); + setup_price(&mut ts, SELLER, seller_kiosk_id, item, item_price); + let mut payment = kiosk_test_utils::get_iota(item_price, ts.ctx()); + let royalty_amount_to_pay = get_royalty_fee_amount(&ts, item_price); + let royalties_coin = kiosk_test_utils::get_iota(royalty_amount_to_pay, ts.ctx()); + payment.join(royalties_coin); + buy(&mut ts, BUYER, seller_kiosk_id, item_id, payment); + + publisher.burn_publisher(); + ts.end(); + } + + #[test] + fun test_get_item_price() { + let mut ts = ts::begin(SELLER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let seller_kiosk_id = create_kiosk(SELLER, ts.ctx()); + let item_price = 50000; + + install_ext(&mut ts, SELLER, seller_kiosk_id); + setup_price(&mut ts, SELLER, seller_kiosk_id, item, item_price); + ts.next_tx(SELLER); + let kiosk: Kiosk = ts.take_shared_by_id(seller_kiosk_id); + let storage_item_price = marketplace_extension::get_item_price(&kiosk, item_id); + + assert!(storage_item_price == item_price); + + ts::return_shared(kiosk); + ts.end(); + } + + // ==================== Negative scenarios ==================== + + #[test] + #[expected_failure(abort_code = marketplace_extension::EWrongPaymentRoyalties)] + fun test_buy_item_with_royalties_wrong_royalties_amount() { + let mut ts = ts::begin(SELLER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let seller_kiosk_id = create_kiosk(SELLER, ts.ctx()); + let item_price = 50000; + let royalty_amount_bp = 5000; + let royalty_min_amount = 2000; + create_transfer_policy( CREATOR, &publisher, ts.ctx()); + add_royalty_rule(&mut ts, CREATOR, royalty_amount_bp, royalty_min_amount); + install_ext(&mut ts, SELLER, seller_kiosk_id); + setup_price(&mut ts, SELLER, seller_kiosk_id, item, item_price); + let mut payment = kiosk_test_utils::get_iota(item_price, ts.ctx()); + let royalty_amount_to_pay = get_royalty_fee_amount(&ts, 1000); + let royalties_coin = kiosk_test_utils::get_iota(royalty_amount_to_pay, ts.ctx()); + payment.join(royalties_coin); + buy(&mut ts, BUYER, seller_kiosk_id, item_id, payment); + + publisher.burn_publisher(); + ts.end(); + } + + #[test] + #[expected_failure(abort_code = marketplace_extension::ENotEnoughPaymentAmount)] + fun test_buy_item_without_royalties_wrong_price() { + let mut ts = ts::begin(SELLER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let seller_kiosk_id = create_kiosk(SELLER, ts.ctx()); + let item_price = 50000; + + create_transfer_policy( CREATOR, &publisher, ts.ctx()); + install_ext(&mut ts, SELLER, seller_kiosk_id); + setup_price(&mut ts, SELLER, seller_kiosk_id, item, item_price); + let payment = kiosk_test_utils::get_iota(40000, ts.ctx()); + buy(&mut ts, BUYER, seller_kiosk_id, item_id, payment); + + publisher.burn_publisher(); + ts.end(); + } + + #[test] + #[expected_failure(abort_code = marketplace_extension::EExtensionNotInstalled)] + fun test_set_price_without_extension() { + let mut ts = ts::begin(SELLER); + + let item = T { id: object::new(ts.ctx()) }; + + let seller_kiosk_id = create_kiosk(SELLER, ts.ctx()); + let item_price = 50000; + + setup_price(&mut ts, SELLER, seller_kiosk_id, item, item_price); + + ts.end(); + } + // ==================== Helper methods ==================== + + + fun setup_price(ts: &mut Scenario, sender: address, seller_kiosk_id: ID, item: T, price: u64) { + ts.next_tx(sender); + let mut kiosk: Kiosk = ts.take_shared_by_id(seller_kiosk_id); + let kiosk_cap = ts.take_from_sender(); + + marketplace_extension::set_price(&mut kiosk, &kiosk_cap, item, price); + + ts::return_shared(kiosk); + ts.return_to_sender(kiosk_cap); + } + + fun install_ext(ts: &mut Scenario, sender: address, kiosk_id: ID) { + ts.next_tx(sender); + let mut kiosk: Kiosk = ts.take_shared_by_id(kiosk_id); + let kiosk_cap = ts.take_from_sender(); + + marketplace_extension::install(&mut kiosk, &kiosk_cap, ts.ctx()); + + ts::return_shared(kiosk); + ts.return_to_sender(kiosk_cap); + } + + fun buy(ts: &mut Scenario, buyer: address, seller_kiosk_id: ID, item_id: ID, payment: Coin) { + ts.next_tx(buyer); + let mut kiosk: Kiosk = ts.take_shared_by_id(seller_kiosk_id); + let mut policy: TransferPolicy = ts.take_shared(); + + let item = marketplace_extension::buy_item(&mut kiosk, &mut policy, item_id, payment, ts.ctx()); + transfer::public_transfer(item, buyer); + ts::return_shared(kiosk); + ts::return_shared(policy); + } + + fun add_royalty_rule(ts: &mut Scenario, sender: address, amount_bp: u16, min_amount: u64) { + ts.next_tx(sender); + let mut transfer_policy: TransferPolicy = ts.take_shared(); + let policy_cap: TransferPolicyCap = ts.take_from_sender(); + + marketplace_extension::setup_royalties(&mut transfer_policy, &policy_cap, amount_bp, min_amount); + + ts::return_shared(transfer_policy); + ts.return_to_sender(policy_cap); + } + + fun get_royalty_fee_amount(ts: &Scenario, price: u64): u64 { + let transfer_policy: TransferPolicy = ts.take_shared(); + let royalty_fee = royalty_rule::fee_amount(&transfer_policy, price); + ts::return_shared(transfer_policy); + royalty_fee + } +} \ No newline at end of file diff --git a/docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move b/docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move deleted file mode 100644 index 614ef2a0778..00000000000 --- a/docs/examples/move/nft_marketplace/tests/nft_marketplace_tests.move +++ /dev/null @@ -1,19 +0,0 @@ -/* -#[test_only] -module nft_marketplace::nft_marketplace_tests { - // uncomment this line to import the module - // use nft_marketplace::nft_marketplace; - - const ENotImplemented: u64 = 0; - - #[test] - fun test_nft_marketplace() { - // pass - } - - #[test, expected_failure(abort_code = ::nft_marketplace::nft_marketplace_tests::ENotImplemented)] - fun test_nft_marketplace_fail() { - abort ENotImplemented - } -} -*/ diff --git a/docs/examples/move/nft_marketplace/tests/rental_extension_tests.move b/docs/examples/move/nft_marketplace/tests/rental_extension_tests.move new file mode 100644 index 00000000000..8fabfd78d7d --- /dev/null +++ b/docs/examples/move/nft_marketplace/tests/rental_extension_tests.move @@ -0,0 +1,362 @@ + +#[test_only] +module nft_marketplace::rental_extension_tests { + + use kiosk::kiosk_lock_rule as lock_rule; + use nft_marketplace::test_utils::{create_kiosk, create_transfer_policy}; + use nft_marketplace::rental_extension::{Self, ProtectedTP, RentalPolicy}; + use iota::{ + clock::{Self, Clock}, + kiosk::{Kiosk, KioskOwnerCap}, + kiosk_test_utils, + package::{Self, Publisher}, + test_scenario::{Self as ts, Scenario}, + transfer_policy::{TransferPolicy, TransferPolicyCap} + }; + + const CREATOR: address = @0xCCCC; + const RENTER: address = @0xAAAA; + const BORROWER: address = @0xBBBB; + const THIEF: address = @0xDDDD; + + public struct T has key, store { id: UID } + public struct WITNESS has drop {} + + +// ==================== Happy path scenarios ==================== + + #[test] + fun test_rent_with_extension() { + let mut ts = ts::begin(BORROWER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let clock = clock::create_for_testing(ts.ctx()); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let renter_kiosk_id = create_kiosk(RENTER, ts.ctx()); + let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx()); + + setup(&mut ts, RENTER, &publisher, 50); + install_ext(&mut ts, RENTER, renter_kiosk_id); + place_in_kiosk(&mut ts, RENTER, renter_kiosk_id, item); + list_for_rent(&mut ts, RENTER, renter_kiosk_id, item_id, 10, 10); + install_ext(&mut ts, BORROWER, borrower_kiosk_id); + rent(&mut ts, BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 100, &clock); + + clock.destroy_for_testing(); + publisher.burn_publisher(); + ts.end(); + } + + #[test] + fun test_reclaim() { + let mut ts = ts::begin(BORROWER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let mut clock = clock::create_for_testing(ts.ctx()); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let renter_kiosk_id = create_kiosk(RENTER, ts.ctx()); + let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx()); + + create_transfer_policy( CREATOR, &publisher, ts.ctx()); + setup(&mut ts, RENTER, &publisher, 50); + place_in_kiosk(&mut ts, RENTER, renter_kiosk_id, item); + install_ext(&mut ts, RENTER, renter_kiosk_id); + list_for_rent(&mut ts, RENTER, renter_kiosk_id, item_id, 10, 10); + install_ext(&mut ts, BORROWER, borrower_kiosk_id); + rent(&mut ts, BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 100, &clock); + reclaim(&mut ts, RENTER, renter_kiosk_id, borrower_kiosk_id, item_id, 432000000, &mut clock); + + clock.destroy_for_testing(); + publisher.burn_publisher(); + ts.end(); + } + + // ==================== Negative scenarios ==================== + + #[test] + #[expected_failure(abort_code = rental_extension::EExtensionNotInstalled)] + fun test_rent_without_extension() { + let mut ts = ts::begin(BORROWER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let clock = clock::create_for_testing(ts.ctx()); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let renter_kiosk_id = create_kiosk(RENTER, ts.ctx()); + let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx()); + + setup(&mut ts, RENTER, &publisher, 50); + place_in_kiosk(&mut ts, RENTER, renter_kiosk_id, item); + install_ext(&mut ts, RENTER, renter_kiosk_id); + list_for_rent(&mut ts, RENTER, renter_kiosk_id, item_id, 10, 10); + rent(&mut ts, BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 100, &clock); + abort 0xbad + } + + #[test] + #[expected_failure(abort_code = rental_extension::ENotEnoughCoins)] + fun test_rent_with_not_enough_coins() { + let mut ts = ts::begin(BORROWER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let clock = clock::create_for_testing(ts.ctx()); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let renter_kiosk_id = create_kiosk(RENTER, ts.ctx()); + let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx()); + + setup(&mut ts, RENTER, &publisher, 50); + place_in_kiosk(&mut ts, RENTER, renter_kiosk_id, item); + install_ext(&mut ts, RENTER, renter_kiosk_id); + list_for_rent(&mut ts, RENTER, renter_kiosk_id, item_id, 10, 10); + install_ext(&mut ts, BORROWER, borrower_kiosk_id); + rent(&mut ts, BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 10, &clock); + abort 0xbad + } + + #[test] + #[expected_failure(abort_code = rental_extension::ETotalPriceOverflow)] + fun test_rent_with_overflow() { + let mut ts = ts::begin(BORROWER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let clock = clock::create_for_testing(ts.ctx()); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let renter_kiosk_id = create_kiosk(RENTER, ts.ctx()); + let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx()); + + setup(&mut ts, RENTER, &publisher, 50); + place_in_kiosk(&mut ts, RENTER, renter_kiosk_id, item); + install_ext(&mut ts, RENTER, renter_kiosk_id); + list_for_rent(&mut ts, RENTER, renter_kiosk_id, item_id, 100, 1844674407370955160); + install_ext(&mut ts, BORROWER, borrower_kiosk_id); + rent(&mut ts, BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 100, &clock); + abort 0xbad + } + + #[test] + fun test_reclaim_locked() { + let mut ts = ts::begin(RENTER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let mut clock = clock::create_for_testing(ts.ctx()); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let renter_kiosk_id = create_kiosk(RENTER, ts.ctx()); + let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx()); + + create_transfer_policy(CREATOR, &publisher, ts.ctx()); + add_lock_rule(&mut ts, CREATOR); + setup(&mut ts, RENTER, &publisher, 50); + lock_in_kiosk(&mut ts, RENTER, renter_kiosk_id, item); + install_ext(&mut ts, RENTER, renter_kiosk_id); + list_for_rent(&mut ts, RENTER, renter_kiosk_id, item_id, 10, 10); + install_ext(&mut ts, BORROWER, borrower_kiosk_id); + rent(&mut ts, BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 100, &clock); + reclaim(&mut ts, RENTER, renter_kiosk_id, borrower_kiosk_id, item_id, 432000000, &mut clock); + + clock.destroy_for_testing(); + publisher.burn_publisher(); + ts.end(); + } + + #[test] + #[expected_failure(abort_code = rental_extension::EInvalidKiosk)] + fun test_reclaim_wrong_kiosk() { + let mut ts = ts::begin(BORROWER); + + let item = T { id: object::new(ts.ctx()) }; + let item_id = object::id(&item); + + let mut clock = clock::create_for_testing(ts.ctx()); + + let witness = WITNESS {}; + let publisher = package::test_claim(witness, ts.ctx()); + + let renter_kiosk_id = create_kiosk(RENTER, ts.ctx()); + let borrower_kiosk_id = create_kiosk(BORROWER, ts.ctx()); + let thief_kiosk_id = create_kiosk(THIEF, ts.ctx()); + + create_transfer_policy(CREATOR, &publisher, ts.ctx()); + setup(&mut ts, RENTER, &publisher, 50); + place_in_kiosk(&mut ts, RENTER, renter_kiosk_id, item); + install_ext(&mut ts, RENTER, renter_kiosk_id); + list_for_rent(&mut ts, RENTER, renter_kiosk_id, item_id, 10, 10); + install_ext(&mut ts, BORROWER, borrower_kiosk_id); + rent(&mut ts, BORROWER, renter_kiosk_id, borrower_kiosk_id, item_id, 100, &clock); + install_ext(&mut ts, THIEF, thief_kiosk_id); + reclaim(&mut ts, RENTER, thief_kiosk_id, borrower_kiosk_id, item_id, 432000000, &mut clock); + abort 0xbad + } + + // ==================== Helper methods ==================== + + fun place_in_kiosk(ts: &mut Scenario, sender: address, kiosk_id: ID, item: T) { + ts.next_tx(sender); + let mut kiosk: Kiosk = ts.take_shared_by_id(kiosk_id); + let kiosk_cap: KioskOwnerCap = ts.take_from_sender(); + + kiosk.place(&kiosk_cap, item); + + ts::return_shared(kiosk); + ts.return_to_sender(kiosk_cap); + } + + + fun list_for_rent( + ts: &mut Scenario, + sender: address, + kiosk_id: ID, + item_id: ID, + duration: u64, + price: u64, + ) { + ts.next_tx(sender); + let mut kiosk: Kiosk = ts.take_shared_by_id(kiosk_id); + let kiosk_cap: KioskOwnerCap = ts.take_from_sender(); + let protected_tp: ProtectedTP = ts.take_shared(); + + rental_extension::list( + &mut kiosk, + &kiosk_cap, + &protected_tp, + item_id, + duration, + price, + ts.ctx(), + ); + + ts::return_shared(kiosk); + ts.return_to_sender(kiosk_cap); + ts::return_shared(protected_tp); + } + + fun rent( + ts: &mut Scenario, + sender: address, + renter_kiosk_id: ID, + borrower_kiosk_id: ID, + item_id: ID, + coin_amount: u64, + clock: &Clock, + ) { + ts.next_tx(sender); + + let mut borrower_kiosk: Kiosk = ts.take_shared_by_id(borrower_kiosk_id); + let mut renter_kiosk: Kiosk = ts.take_shared_by_id(renter_kiosk_id); + let mut rental_policy: RentalPolicy = ts.take_shared(); + + let coin = kiosk_test_utils::get_iota(coin_amount, ts.ctx()); + + rental_extension::rent( + &mut renter_kiosk, + &mut borrower_kiosk, + &mut rental_policy, + item_id, + coin, + clock, + ts.ctx(), + ); + + ts::return_shared(borrower_kiosk); + ts::return_shared(renter_kiosk); + ts::return_shared(rental_policy); + } + + fun setup(ts: &mut Scenario, sender: address, publisher: &Publisher, amount_bp: u64) { + ts.next_tx(sender); + rental_extension::setup_renting(publisher, amount_bp, ts.ctx()); + } + + fun reclaim( + ts: &mut Scenario, + sender: address, + renter_kiosk_id: ID, + borrower_kiosk_id: ID, + item_id: ID, + tick: u64, + clock: &mut Clock, + ) { + ts.next_tx(sender); + let mut borrower_kiosk: Kiosk = ts.take_shared_by_id(borrower_kiosk_id); + let mut renter_kiosk: Kiosk = ts.take_shared_by_id(renter_kiosk_id); + let policy: TransferPolicy = ts.take_shared(); + + clock.increment_for_testing(tick); + rental_extension::reclaim( + &mut renter_kiosk, + &mut borrower_kiosk, + &policy, + clock, + item_id, + ts.ctx(), + ); + + ts::return_shared(policy); + ts::return_shared(borrower_kiosk); + ts::return_shared(renter_kiosk); + } + + fun add_lock_rule(ts: &mut Scenario, sender: address) { + ts.next_tx(sender); + let mut transfer_policy: TransferPolicy = ts.take_shared(); + let policy_cap: TransferPolicyCap = ts.take_from_sender(); + + lock_rule::add(&mut transfer_policy, &policy_cap); + + ts::return_shared(transfer_policy); + ts.return_to_sender(policy_cap); + } + + fun lock_in_kiosk(ts: &mut Scenario, sender: address, kiosk_id: ID, item: T) { + ts.next_tx(sender); + + let mut kiosk: Kiosk = ts.take_shared_by_id(kiosk_id); + let kiosk_cap: KioskOwnerCap = ts.take_from_sender(); + let transfer_policy: TransferPolicy = ts.take_shared(); + + kiosk.lock(&kiosk_cap, &transfer_policy, item); + + ts::return_shared(kiosk); + ts.return_to_sender(kiosk_cap); + ts::return_shared(transfer_policy); + } + + fun install_ext(ts: &mut Scenario, sender: address, kiosk_id: ID) { + ts.next_tx(sender); + let mut kiosk: Kiosk = ts.take_shared_by_id(kiosk_id); + let kiosk_cap = ts.take_from_sender(); + + rental_extension::install(&mut kiosk, &kiosk_cap, ts.ctx()); + + ts::return_shared(kiosk); + ts.return_to_sender(kiosk_cap); +} +} diff --git a/docs/examples/move/nft_marketplace/tests/utils.move b/docs/examples/move/nft_marketplace/tests/utils.move new file mode 100644 index 00000000000..01498370bcc --- /dev/null +++ b/docs/examples/move/nft_marketplace/tests/utils.move @@ -0,0 +1,24 @@ +#[test_only] +module nft_marketplace::test_utils { + use iota::{ + kiosk_test_utils, + package::Publisher, + transfer_policy::Self + }; + + + public fun create_transfer_policy(sender: address, publisher: &Publisher, ctx: &mut TxContext) { + let (transfer_policy, policy_cap) = transfer_policy::new(publisher, ctx); + transfer::public_share_object(transfer_policy); + transfer::public_transfer(policy_cap, sender); + } + + public fun create_kiosk(sender: address, ctx: &mut TxContext): ID { + let (kiosk, kiosk_cap) = kiosk_test_utils::get_kiosk(ctx); + let kiosk_id = object::id(&kiosk); + transfer::public_share_object(kiosk); + transfer::public_transfer(kiosk_cap, sender); + + kiosk_id +} +} \ No newline at end of file From 340cb12351b8440f18aab670cd893189c2d5422a Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Thu, 28 Nov 2024 12:05:06 +0200 Subject: [PATCH 18/22] Add minor comments to the buy_item method --- .../sources/marketplace_extension.move | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/examples/move/nft_marketplace/sources/marketplace_extension.move b/docs/examples/move/nft_marketplace/sources/marketplace_extension.move index 54113826d0d..0531c5488a3 100644 --- a/docs/examples/move/nft_marketplace/sources/marketplace_extension.move +++ b/docs/examples/move/nft_marketplace/sources/marketplace_extension.move @@ -62,20 +62,34 @@ module nft_marketplace::marketplace_extension { /// Buy listed item with the indicated price and pay royalties if needed public fun buy_item(kiosk: &mut Kiosk, policy: &mut TransferPolicy, item_id: object::ID, mut payment: Coin, ctx: &mut TxContext): T { assert!(kiosk_extension::is_installed(kiosk), EExtensionNotInstalled); + + // Get item price let ItemPrice { price } = take_from_bag(kiosk, Listed { id: item_id }); - + + // Compute the value of the coin in input let payment_amount_value = payment.value(); + + // If the payment_amount_value is less than the item price, the request is invalid. assert!(payment_amount_value >= price, ENotEnoughPaymentAmount); + + // Prepare the payment coin for the purchase (if no royalties are present then the + // remaining balance will be 0 after this operation) let coin_price = payment.split(price, ctx); - + + // Purchase and create the transfer request let (item, mut transfer_request) = purchase(kiosk, item_id, coin_price); + + // If the royalty is present, then update the request with a royalty payment if (policy.has_rule()) { let royalties_value = royalty_rule::fee_amount(policy, price); assert!(payment_amount_value == price + royalties_value, EWrongPaymentRoyalties); royalty_rule::pay(policy, &mut transfer_request, payment); } else { + // Else clean the input coin (if the input payment amount is not exact, this will fail) payment.destroy_zero(); }; + + // Confirm the request transfer_policy::confirm_request(policy, transfer_request); item } From d4bb3516d9e344fac349974458468f813e0d7ddc Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Thu, 28 Nov 2024 12:26:28 +0200 Subject: [PATCH 19/22] Minor link fix --- docs/content/developer/standards/kiosk-apps.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/developer/standards/kiosk-apps.mdx b/docs/content/developer/standards/kiosk-apps.mdx index 7a8f8aca9a3..624a4c5128d 100644 --- a/docs/content/developer/standards/kiosk-apps.mdx +++ b/docs/content/developer/standards/kiosk-apps.mdx @@ -260,6 +260,6 @@ txb.moveCall({ - [NFT Rental](../iota-101/nft/rent-nft.mdx): An example implementation of the Kiosk Apps standard that enables renting NFTs. - [NFT Rental Extension](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/rental_extension.move): An example implementation of the Kiosk Extension standard that enables renting NFTs. -- [Marketplace Extension](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/nft_marketplace.move): Move package that contains the source code(Kiosk extension) for the marketplace app. +- [Marketplace Extension](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move): Move module that contains the source code(Kiosk extension) for the marketplace app. \ No newline at end of file From 86ffbcbb01b9303614e555180ceb11c4e1bdb5cf Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Thu, 5 Dec 2024 12:25:18 +0200 Subject: [PATCH 20/22] fix(docs): create marketplace extension doc page --- .../developer/iota-101/nft/marketplace.mdx | 218 ++++++++++++++++++ .../developer/standards/kiosk-apps.mdx | 1 + docs/content/sidebars/developer.js | 2 +- 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 docs/content/developer/iota-101/nft/marketplace.mdx diff --git a/docs/content/developer/iota-101/nft/marketplace.mdx b/docs/content/developer/iota-101/nft/marketplace.mdx new file mode 100644 index 00000000000..0fc3f47300d --- /dev/null +++ b/docs/content/developer/iota-101/nft/marketplace.mdx @@ -0,0 +1,218 @@ +--- +description: A brief introduction to implementing NFT marketplace extension using the Kiosk Apps standard in IOTA's Move language. +--- + +# Marketplace Extension Usage + +## Modules + +The [`marketplace_extension.move`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move) +module provides a straightforward implementation of a marketplace extension. To use it, follow the +[steps outlined below](#how-to-use-the-marketplace). + +The [`clothing_store.move`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/clothing_store.move) module contains mocked item data for +use within the marketplace. + +The [`rental_extension.move`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/rental_extension.move) module adds the functionality to enable item rentals. + +## How To Use the Marketplace + +### 1. Install the IOTA CLI and Connect to the Network + +The first thing you'll need to do is [install the IOTA CLI](https://docs.iota.org/developer/getting-started/install-iota), [connect to an IOTA network](https://docs.iota.org/developer/getting-started/connect) and [get some test tokens](https://docs.iota.org/developer/getting-started/get-coins) to pay for [gas](https://docs.iota.org/about-iota/tokenomics/gas-in-iota). + +### 2. Install Kiosk + +You can install the Kiosk by creating a `Kiosk` object, which will also create its `OwnerCap`, and then transferring +them to the caller. + +Run the following command to install the Kiosk module: + +```bash +iota client call \ + --package 0x2 \ + --module kiosk \ + --function default +``` + +After publishing, export the following variables: + +- `KIOSK_ID`: The ID of the installed Kiosk object. +- `KIOSK_CAP_ID`: The ID of the installed Kiosk's owner cap + +### 3. Publish `nft_marketplace` package + +#### 3.1(Optional) Publish Kiosk rules modules if these are not present in the network you are using + +You can publish Kiosk rules modules(package) using the following command: + +```bash +iota client publish $IOTA_REPO_DIR/kiosk +``` + +After publishing, export the following variable: + +- `RULES_PACKAGE_ID`: The ID of the rules package. + +#### 3.2 Publish the `nft_marketplace` Package + +```bash +iota client publish $IOTA_REPO_DIR/docs/examples/move/nft_marketplace +``` + +After publishing, export the following variables: + +- `MARKETPLACE_PACKAGE_ID`: The ID of the whole marketplace package. +- `MARKETPLACE_PUBLISHER_ID`: The ID of the publisher object created during marketplace package publishing." + +### 4. Create a Clothing Store Item + +Next, you should use the functions in the `clothing_store` module to create an item, for instance: + +```bash +iota client call \ + --package $MARKETPLACE_PACKAGE_ID \ + --module clothing_store \ + --function new_jeans +``` + +After creation, export the following variable: + +- `CLOTHING_STORE_ITEM_ID`: The ID of the published item (in this case, Jeans). + +### 5. Create a Transfer Policy + +`TransferPolicy` is a generic-shared object that acts as a central authority enforcing that everyone checks their +purchase is valid against the defined policy before the purchased item is transferred to the buyer. The object is +specified by concrete type: +The `default` function creates a `TransferPolicy` object and a `TransferPolicyCap`, then transfers them to the caller. + +The `TransferPolicyCap` object serves as proof of ownership of the `TransferPolicy` object. +A capability granting the owner permission to `add/remove` rules, `withdraw`, and `destroy_and_withdraw` the `TransferPolicy`. + +You can set up a transfer policy for the created item using the following command: + +```bash +iota client call \ + --package 0x2 \ + --module transfer_policy \ + --function default \ + --gas-budget 10000000 \ + --args $MARKETPLACE_PUBLISHER_ID \ + --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" +``` + +After publishing, export the following variables: + +- `ITEM_TRANS_POLICY`: The ID of the item transfer policy object. +- `ITEM_TRANS_POLICY_CAP`: The ID of the item transfer policy object owner capability" + +### 6. Install the Extension on the Kiosk + +The [`install`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L39-L45) function enables the installation of the Marketplace extension in a kiosk. +Under the hood, it invokes `kiosk_extension::add`, which adds an extension to the Kiosk via a [dynamic field](https://docs.iota.org/developer/iota-101/objects/dynamic-fields/). +You can install the marketplace extension on the created kiosk using the following command: + +```bash +iota client call \ + --package $MARKETPLACE_PACKAGE_ID \ + --module marketplace_extension \ + --function install \ + --args $KIOSK_ID $KIOSK_CAP_ID +``` + +### 7. Set a Price for the Item + +You can use the [`set_price`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L98-L114) function to set the price for the item: + +```bash +iota client call \ + --package $MARKETPLACE_PACKAGE_ID \ + --module marketplace_extension \ + --function set_price \ + --args $KIOSK_ID $KIOSK_CAP_ID $CLOTHING_STORE_ITEM_ID 50000 \ + --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" +``` + +### 8.(Optional) Set Royalties + +Royalties are a percentage of the item's price or revenue paid to the owner for using or selling their asset. + +You can use the [`set_royalties`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L58-L60) function to set royalties for the item: + +```bash +iota client call \ + --package $MARKETPLACE_PACKAGE_ID \ + --module marketplace_extension \ + --function setup_royalties \ + --args $ITEM_TRANS_POLICY $ITEM_TRANS_POLICY_CAP 5000 2000 \ + --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" +``` + +### 9. Buy an Item + +#### 9.1 Get the Item Price + +You can use the following [Programmable Transaction Block](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks-overview) to call the +[`get_item_price`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L116-L127) +and assign it to an `item_price` variable. In this case, the Jeans item: + +```bash +iota client ptb \ +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ +``` + +#### 9.2(Optional) Calculate the Royalties For the Item + +You can use the following [move-call](https://docs.iota.org/references/cli/ptb#move-call) to get the royalties for any given product by calling the `kiosk::royalty_rule::fee_amount` function +and assign it to a `royalties_amount` variable. In this case, the Jeans item: + +```bash +--move-call $RULES_PACKAGE_ID::royalty_rule::fee_amount "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$ITEM_TRANS_POLICY item_price --assign royalties_amount \ +``` + +#### 9.3 Create a Payment Coin With a Specific Amount (Price + Optional Royalties) + +You can use the following command to [split your gas tokens](https://docs.iota.org/references/cli/ptb#split-destroy-and-merge-coins) to pay for the item's price and royalties: + +```bash +--split-coins gas "[item_price, royalties_amount]" --assign payment_coins \ +--merge-coins payment_coins.0 "[payment_coins.1]" \ +``` + +#### 9.4 Buy an Item Using `payment_coins.0` + +You can use the following [move-call](https://docs.iota.org/references/cli/ptb#move-call) to pay the owner the item's price. +If the royalty rule is enabled, an additional royalty fee, calculated as a percentage of the initial item price, is also +paid. +Once both payments are completed, the item is ready for transfer to the buyer. + +To purchase the item: + +```bash +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item +``` + +#### 9.5 Transfer an Item to the Buyer + +Finally, you can set up the +[public_transfer](https://docs.iota.org/references/framework/iota-framework/transfer#function-public_transfer) to +transfer the purchased item to the buyer: + +```bash +--move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @ \ +``` + +You can combine all the previous steps into one purchase +[PTB](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks-overview) request, +including royalties, which should look like this: + +```bash +iota client ptb \ +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ +--move-call $RULES_PACKAGE_ID::royalty_rule::fee_amount "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$ITEM_TRANS_POLICY item_price --assign royalties_amount \ +--split-coins gas "[item_price, royalties_amount]" --assign payment_coins \ +--merge-coins payment_coins.0 "[payment_coins.1]" \ +--move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item \ +--move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @ +``` diff --git a/docs/content/developer/standards/kiosk-apps.mdx b/docs/content/developer/standards/kiosk-apps.mdx index 624a4c5128d..cb88f6eab24 100644 --- a/docs/content/developer/standards/kiosk-apps.mdx +++ b/docs/content/developer/standards/kiosk-apps.mdx @@ -260,6 +260,7 @@ txb.moveCall({ - [NFT Rental](../iota-101/nft/rent-nft.mdx): An example implementation of the Kiosk Apps standard that enables renting NFTs. - [NFT Rental Extension](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/rental_extension.move): An example implementation of the Kiosk Extension standard that enables renting NFTs. +- [Marketplace](../iota-101/nft/marketplace.mdx): An example implementation of the Kiosk Apps standard that enables marketplace functionality. - [Marketplace Extension](https://github.com/iotaledger/iota/tree/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move): Move module that contains the source code(Kiosk extension) for the marketplace app. \ No newline at end of file diff --git a/docs/content/sidebars/developer.js b/docs/content/sidebars/developer.js index 32192dd666b..e8ac1eb1bb5 100644 --- a/docs/content/sidebars/developer.js +++ b/docs/content/sidebars/developer.js @@ -212,7 +212,7 @@ const developer = [ { type: 'category', label: 'NFT', - items: ['developer/iota-101/nft/create-nft', 'developer/iota-101/nft/rent-nft'], + items: ['developer/iota-101/nft/create-nft', 'developer/iota-101/nft/rent-nft', 'developer/iota-101/nft/marketplace'], }, { type: 'category', From 1de79563978ad3f9cd5e5799037e4ba933a6ad75 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Thu, 5 Dec 2024 13:45:21 +0200 Subject: [PATCH 21/22] fix(docs): add short description for Marketplace Extension Usage --- docs/content/developer/iota-101/nft/marketplace.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/content/developer/iota-101/nft/marketplace.mdx b/docs/content/developer/iota-101/nft/marketplace.mdx index 0fc3f47300d..867ab0860fd 100644 --- a/docs/content/developer/iota-101/nft/marketplace.mdx +++ b/docs/content/developer/iota-101/nft/marketplace.mdx @@ -4,6 +4,10 @@ description: A brief introduction to implementing NFT marketplace extension usin # Marketplace Extension Usage +The Marketplace Extension for [IOTA Kiosk](../../standards/kiosk.mdx) is a customizable framework that extends the functionality of the IOTA Kiosk by enabling efficient asset trading with integrated royalty management and pricing mechanism. + +Kiosk owners can list items for sale by setting prices, and enforce royalties, ensuring creators receive a percentage of each sale. Buyers can securely purchase items, with the extension validating payments and handling royalties automatically. All transactions are governed by robust transfer policies, ensuring security and compliance. + ## Modules The [`marketplace_extension.move`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move) From b6b029951aff1d8233c79944465e986a5854c733 Mon Sep 17 00:00:00 2001 From: Dkwcs Date: Thu, 5 Dec 2024 17:52:53 +0200 Subject: [PATCH 22/22] fix(docs): import readme instead of duplicate it --- .../developer/iota-101/nft/marketplace.mdx | 215 +----------------- docs/examples/move/nft_marketplace/README.md | 2 +- 2 files changed, 6 insertions(+), 211 deletions(-) diff --git a/docs/content/developer/iota-101/nft/marketplace.mdx b/docs/content/developer/iota-101/nft/marketplace.mdx index 867ab0860fd..e5c20e2fa67 100644 --- a/docs/content/developer/iota-101/nft/marketplace.mdx +++ b/docs/content/developer/iota-101/nft/marketplace.mdx @@ -2,221 +2,16 @@ description: A brief introduction to implementing NFT marketplace extension using the Kiosk Apps standard in IOTA's Move language. --- -# Marketplace Extension Usage -The Marketplace Extension for [IOTA Kiosk](../../standards/kiosk.mdx) is a customizable framework that extends the functionality of the IOTA Kiosk by enabling efficient asset trading with integrated royalty management and pricing mechanism. - -Kiosk owners can list items for sale by setting prices, and enforce royalties, ensuring creators receive a percentage of each sale. Buyers can securely purchase items, with the extension validating payments and handling royalties automatically. All transactions are governed by robust transfer policies, ensuring security and compliance. - -## Modules - -The [`marketplace_extension.move`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move) -module provides a straightforward implementation of a marketplace extension. To use it, follow the -[steps outlined below](#how-to-use-the-marketplace). - -The [`clothing_store.move`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/clothing_store.move) module contains mocked item data for -use within the marketplace. - -The [`rental_extension.move`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/rental_extension.move) module adds the functionality to enable item rentals. - -## How To Use the Marketplace - -### 1. Install the IOTA CLI and Connect to the Network - -The first thing you'll need to do is [install the IOTA CLI](https://docs.iota.org/developer/getting-started/install-iota), [connect to an IOTA network](https://docs.iota.org/developer/getting-started/connect) and [get some test tokens](https://docs.iota.org/developer/getting-started/get-coins) to pay for [gas](https://docs.iota.org/about-iota/tokenomics/gas-in-iota). - -### 2. Install Kiosk - -You can install the Kiosk by creating a `Kiosk` object, which will also create its `OwnerCap`, and then transferring -them to the caller. - -Run the following command to install the Kiosk module: - -```bash -iota client call \ - --package 0x2 \ - --module kiosk \ - --function default -``` - -After publishing, export the following variables: - -- `KIOSK_ID`: The ID of the installed Kiosk object. -- `KIOSK_CAP_ID`: The ID of the installed Kiosk's owner cap - -### 3. Publish `nft_marketplace` package - -#### 3.1(Optional) Publish Kiosk rules modules if these are not present in the network you are using - -You can publish Kiosk rules modules(package) using the following command: - -```bash -iota client publish $IOTA_REPO_DIR/kiosk -``` - -After publishing, export the following variable: - -- `RULES_PACKAGE_ID`: The ID of the rules package. - -#### 3.2 Publish the `nft_marketplace` Package - -```bash -iota client publish $IOTA_REPO_DIR/docs/examples/move/nft_marketplace -``` - -After publishing, export the following variables: - -- `MARKETPLACE_PACKAGE_ID`: The ID of the whole marketplace package. -- `MARKETPLACE_PUBLISHER_ID`: The ID of the publisher object created during marketplace package publishing." - -### 4. Create a Clothing Store Item - -Next, you should use the functions in the `clothing_store` module to create an item, for instance: - -```bash -iota client call \ - --package $MARKETPLACE_PACKAGE_ID \ - --module clothing_store \ - --function new_jeans -``` - -After creation, export the following variable: - -- `CLOTHING_STORE_ITEM_ID`: The ID of the published item (in this case, Jeans). -### 5. Create a Transfer Policy -`TransferPolicy` is a generic-shared object that acts as a central authority enforcing that everyone checks their -purchase is valid against the defined policy before the purchased item is transferred to the buyer. The object is -specified by concrete type: -The `default` function creates a `TransferPolicy` object and a `TransferPolicyCap`, then transfers them to the caller. +import Marketplace from '../../../../examples/move/nft_marketplace/README.md'; -The `TransferPolicyCap` object serves as proof of ownership of the `TransferPolicy` object. -A capability granting the owner permission to `add/remove` rules, `withdraw`, and `destroy_and_withdraw` the `TransferPolicy`. +# Marketplace Extension -You can set up a transfer policy for the created item using the following command: - -```bash -iota client call \ - --package 0x2 \ - --module transfer_policy \ - --function default \ - --gas-budget 10000000 \ - --args $MARKETPLACE_PUBLISHER_ID \ - --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" -``` - -After publishing, export the following variables: - -- `ITEM_TRANS_POLICY`: The ID of the item transfer policy object. -- `ITEM_TRANS_POLICY_CAP`: The ID of the item transfer policy object owner capability" - -### 6. Install the Extension on the Kiosk - -The [`install`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L39-L45) function enables the installation of the Marketplace extension in a kiosk. -Under the hood, it invokes `kiosk_extension::add`, which adds an extension to the Kiosk via a [dynamic field](https://docs.iota.org/developer/iota-101/objects/dynamic-fields/). -You can install the marketplace extension on the created kiosk using the following command: - -```bash -iota client call \ - --package $MARKETPLACE_PACKAGE_ID \ - --module marketplace_extension \ - --function install \ - --args $KIOSK_ID $KIOSK_CAP_ID -``` - -### 7. Set a Price for the Item - -You can use the [`set_price`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L98-L114) function to set the price for the item: - -```bash -iota client call \ - --package $MARKETPLACE_PACKAGE_ID \ - --module marketplace_extension \ - --function set_price \ - --args $KIOSK_ID $KIOSK_CAP_ID $CLOTHING_STORE_ITEM_ID 50000 \ - --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" -``` - -### 8.(Optional) Set Royalties - -Royalties are a percentage of the item's price or revenue paid to the owner for using or selling their asset. - -You can use the [`set_royalties`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L58-L60) function to set royalties for the item: - -```bash -iota client call \ - --package $MARKETPLACE_PACKAGE_ID \ - --module marketplace_extension \ - --function setup_royalties \ - --args $ITEM_TRANS_POLICY $ITEM_TRANS_POLICY_CAP 5000 2000 \ - --type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans" -``` - -### 9. Buy an Item - -#### 9.1 Get the Item Price - -You can use the following [Programmable Transaction Block](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks-overview) to call the -[`get_item_price`](https://github.com/iotaledger/iota/blob/develop/docs/examples/move/nft_marketplace/sources/marketplace_extension.move#L116-L127) -and assign it to an `item_price` variable. In this case, the Jeans item: - -```bash -iota client ptb \ ---move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ -``` - -#### 9.2(Optional) Calculate the Royalties For the Item - -You can use the following [move-call](https://docs.iota.org/references/cli/ptb#move-call) to get the royalties for any given product by calling the `kiosk::royalty_rule::fee_amount` function -and assign it to a `royalties_amount` variable. In this case, the Jeans item: - -```bash ---move-call $RULES_PACKAGE_ID::royalty_rule::fee_amount "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$ITEM_TRANS_POLICY item_price --assign royalties_amount \ -``` - -#### 9.3 Create a Payment Coin With a Specific Amount (Price + Optional Royalties) - -You can use the following command to [split your gas tokens](https://docs.iota.org/references/cli/ptb#split-destroy-and-merge-coins) to pay for the item's price and royalties: - -```bash ---split-coins gas "[item_price, royalties_amount]" --assign payment_coins \ ---merge-coins payment_coins.0 "[payment_coins.1]" \ -``` - -#### 9.4 Buy an Item Using `payment_coins.0` - -You can use the following [move-call](https://docs.iota.org/references/cli/ptb#move-call) to pay the owner the item's price. -If the royalty rule is enabled, an additional royalty fee, calculated as a percentage of the initial item price, is also -paid. -Once both payments are completed, the item is ready for transfer to the buyer. - -To purchase the item: - -```bash ---move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item -``` - -#### 9.5 Transfer an Item to the Buyer - -Finally, you can set up the -[public_transfer](https://docs.iota.org/references/framework/iota-framework/transfer#function-public_transfer) to -transfer the purchased item to the buyer: +The Marketplace Extension for [IOTA Kiosk](../../standards/kiosk.mdx) is a customizable framework that extends the functionality of the IOTA Kiosk by enabling efficient asset trading with integrated royalty management and pricing mechanism. -```bash ---move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @ \ -``` +Kiosk owners can list items for sale by setting prices, and enforce royalties, ensuring creators receive a percentage of each sale. Buyers can securely purchase items, with the extension validating payments and handling royalties automatically. All transactions are governed by robust transfer policies, ensuring security and compliance. -You can combine all the previous steps into one purchase -[PTB](https://docs.iota.org/developer/iota-101/transactions/ptb/programmable-transaction-blocks-overview) request, -including royalties, which should look like this: + -```bash -iota client ptb \ ---move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::get_item_price "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$CLOTHING_STORE_ITEM_ID --assign item_price \ ---move-call $RULES_PACKAGE_ID::royalty_rule::fee_amount "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$ITEM_TRANS_POLICY item_price --assign royalties_amount \ ---split-coins gas "[item_price, royalties_amount]" --assign payment_coins \ ---merge-coins payment_coins.0 "[payment_coins.1]" \ ---move-call $MARKETPLACE_PACKAGE_ID::marketplace_extension::buy_item "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" @$KIOSK_ID @$ITEM_TRANS_POLICY @$CLOTHING_STORE_ITEM_ID payment_coins.0 --assign purchased_item \ ---move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @ -``` diff --git a/docs/examples/move/nft_marketplace/README.md b/docs/examples/move/nft_marketplace/README.md index 42cb414ab65..80b330362ff 100644 --- a/docs/examples/move/nft_marketplace/README.md +++ b/docs/examples/move/nft_marketplace/README.md @@ -1,4 +1,4 @@ -# Marketplace Guide +# Marketplace Extension Usage ## Modules