Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'sc-platform/Create-an-example-NFT-marketplace-using-the…
Browse files Browse the repository at this point in the history
…-Kiosk-framework' into sc-platform/update-Kiosk-related-docs
Dkwcs committed Nov 28, 2024
2 parents 8c7d374 + 340cb12 commit 683415a
Showing 8 changed files with 798 additions and 98 deletions.
141 changes: 101 additions & 40 deletions docs/examples/move/nft_marketplace/README.md
Original file line number Diff line number Diff line change
@@ -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.

@@ -12,107 +12,168 @@ 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
iota client call \
iota client call \
--package 0x2 \
--module kiosk \
--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 marketplace_extension.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
- ITEM_ID
- PUBLISHER_ID
- `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 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 \
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"
--args $MARKETPLACE_PUBLISHER_ID \
--type-args "$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans"
```

### 6. Publish marketplac extension
After publishing, export the following variables:

Publish the nft_marketplace.move module:

```bash
iota client publish`
```
- `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"

### 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.
Install the marketplace extension on the created kiosk using the command:

```bash
iota client call \
--package $MARKETPLACE_ID \
--module nft_marketplace \
--package $MARKETPLACE_PACKAGE_ID \
--module marketplace_extension \
--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 \
--module nft_marketplace \
--package $MARKETPLACE_PACKAGE_ID \
--module marketplace_extension \
--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.

Set royalties for the item:

```bash
iota client call \
--package $MARKETPLACE \
--module nft_marketplace \
--package $MARKETPLACE_PACKAGE_ID \
--module marketplace_extension \
--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:

#### 9.1 Get the Item Price:

```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 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::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:

```bash
--move-call 0x2::transfer::public_transfer "<$MARKETPLACE_PACKAGE_ID::clothing_store::Jeans>" purchased_item @<buyer address> \
```

The final purchase PTB request, including royalties, 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 @<buyer address>
```
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
module nft_marketplace::market_items {
/// Module provides `mock` items for using them in marketplace and rental extensions.
#[allow(lint(self_transfer))]
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)
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module nft_marketplace::nft_marketplace {
module nft_marketplace::marketplace_extension {
// iota imports
use iota::{
kiosk::{Kiosk, KioskOwnerCap, purchase},
@@ -10,38 +10,38 @@ 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;
const ENotEnoughPaymentAmount: u64 = 2;

// === Constants ===
const PERMISSIONS: u128 = 11;
const ALLOW_PLACE_AND_LOCK: u128 = 11;

/// Extension Key for Kiosk Marketplace extension.
public struct Marketplace has drop {}

/// 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<T: key + store> has store {
public struct ItemPrice<phantom T: key + store> has store {
/// Total amount of time offered for renting in days.
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,31 +50,52 @@ module nft_marketplace::nft_marketplace {
kiosk_extension::remove<Marketplace>(kiosk, cap);
}

public fun setup_royalties<T: key + store>(policy: &mut TransferPolicy<T>, cap: &TransferPolicyCap<T>, amount_bp: u16, min_amount: u64, ctx: &mut TxContext) {
/// 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<T: key + store>(policy: &mut TransferPolicy<T>, cap: &TransferPolicyCap<T>, amount_bp: u16, min_amount: u64) {
royalty_rule::add<T>(policy, cap, amount_bp, min_amount);
}

/// Buy listed item and pay royalties if needed
public fun buy_item<T: key + store>(kiosk: &mut Kiosk, policy: &mut TransferPolicy<T>, item_id: object::ID, mut payment: Coin<IOTA>, ctx: &mut TxContext) {
/// Buy listed item with the indicated price and pay royalties if needed
public fun buy_item<T: key + store>(kiosk: &mut Kiosk, policy: &mut TransferPolicy<T>, item_id: object::ID, mut payment: Coin<IOTA>, ctx: &mut TxContext): T {
assert!(kiosk_extension::is_installed<Marketplace>(kiosk), EExtensionNotInstalled);
let item_price = take_from_bag<T, Listed>(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);

// Get item price
let ItemPrice { price } = take_from_bag<T, Listed>(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<T, RoyaltyRule>()) {
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 {
// 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);
transfer::public_transfer<T>(item, ctx.sender());
// Send a leftover back to buyer
transfer::public_transfer(payment, ctx.sender());
item
}


public fun set_price<T: key + store>(
public fun set_price<T: key + store>(
kiosk: &mut Kiosk,
cap: &KioskOwnerCap,
item: T,
@@ -92,14 +113,27 @@ module nft_marketplace::nft_marketplace {
}


public fun get_item_price<T: key + store>(
kiosk: &Kiosk,
item_id: ID,
) : u64 {
let storage_ref = kiosk_extension::storage(Marketplace {}, kiosk);
let ItemPrice { price } = bag::borrow<Listed, ItemPrice<T>>(
storage_ref,
Listed { id: item_id },
);

*price
}


// === Private Functions ===

fun take_from_bag<T: key + store, Key: store + copy + drop>(
kiosk: &mut Kiosk,
item_key: Key,
) : ItemPrice<T> {
let ext_storage_mut = kiosk_extension::storage_mut(Marketplace {}, kiosk);
assert!(bag::contains(ext_storage_mut, item_key), EObjectNotExist);
bag::remove<Key, ItemPrice<T>>(
ext_storage_mut,
item_key,
37 changes: 25 additions & 12 deletions docs/examples/move/nft_marketplace/sources/rental_extension.move
Original file line number Diff line number Diff line change
@@ -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
@@ -22,11 +30,10 @@ 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 ===
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 +100,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,
@@ -148,6 +155,8 @@ module nft_marketplace::rental_extension {
) {
assert!(kiosk_extension::is_installed<Rental>(kiosk), EExtensionNotInstalled);

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<T>(cap, item_id, 0);

@@ -218,18 +227,12 @@ module nft_marketplace::rental_extension {

let mut rentable = take_from_bag<T, Listed>(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_amount = calculate_fees_amount(coin_value as u128, rental_policy.amount_bp as u128);

let fees = coin.split(fees_amount as u64, ctx);

@@ -368,12 +371,22 @@ 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<T: key + store, Key: store + copy + drop>(
kiosk: &mut Kiosk,
item: Key,
) : Rentable<T> {
let ext_storage_mut = kiosk_extension::storage_mut(Rental {}, kiosk);
assert!(bag::contains(ext_storage_mut, item), EObjectNotExist);
bag::remove<Key, Rentable<T>>(
ext_storage_mut,
item,
Original file line number Diff line number Diff line change
@@ -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<T>( CREATOR, &publisher, ts.ctx());
install_ext(&mut ts, SELLER, seller_kiosk_id);
setup_price<T>(&mut ts, SELLER, seller_kiosk_id, item, item_price);
let payment = kiosk_test_utils::get_iota(item_price, ts.ctx());
buy<T>(&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<T>( 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<T>(&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<T>(&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<T>(&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<T>(&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<T>( 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<T>(&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<T>(&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<T>( CREATOR, &publisher, ts.ctx());
install_ext(&mut ts, SELLER, seller_kiosk_id);
setup_price<T>(&mut ts, SELLER, seller_kiosk_id, item, item_price);
let payment = kiosk_test_utils::get_iota(40000, ts.ctx());
buy<T>(&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<T>(&mut ts, SELLER, seller_kiosk_id, item, item_price);

ts.end();
}
// ==================== Helper methods ====================


fun setup_price<T: key + store>(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<T>(&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<T: key + store>(ts: &mut Scenario, buyer: address, seller_kiosk_id: ID, item_id: ID, payment: Coin<IOTA>) {
ts.next_tx(buyer);
let mut kiosk: Kiosk = ts.take_shared_by_id(seller_kiosk_id);
let mut policy: TransferPolicy<T> = ts.take_shared();

let item = marketplace_extension::buy_item<T>(&mut kiosk, &mut policy, item_id, payment, ts.ctx());
transfer::public_transfer<T>(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<T> = ts.take_shared();
let policy_cap: TransferPolicyCap<T> = 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<T> = ts.take_shared();
let royalty_fee = royalty_rule::fee_amount(&transfer_policy, price);
ts::return_shared(transfer_policy);
royalty_fee
}
}

This file was deleted.

362 changes: 362 additions & 0 deletions docs/examples/move/nft_marketplace/tests/rental_extension_tests.move
Original file line number Diff line number Diff line change
@@ -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<T>( 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<T>(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<T>(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<T> = 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<T> = ts.take_shared();

let coin = kiosk_test_utils::get_iota(coin_amount, ts.ctx());

rental_extension::rent<T>(
&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<T>(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<T> = 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<T> = ts.take_shared();
let policy_cap: TransferPolicyCap<T> = 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<T> = 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);
}
}
24 changes: 24 additions & 0 deletions docs/examples/move/nft_marketplace/tests/utils.move
Original file line number Diff line number Diff line change
@@ -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<T>(sender: address, publisher: &Publisher, ctx: &mut TxContext) {
let (transfer_policy, policy_cap) = transfer_policy::new<T>(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
}
}

0 comments on commit 683415a

Please sign in to comment.