From 5f04ee62b96ffdb46531e3c5e0b692acb18d0716 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Mon, 26 Feb 2024 12:24:25 +0300 Subject: [PATCH 01/12] adds marketplace --- kiosk-marketplace/.gitignore | 1 + kiosk-marketplace/Move.lock | 36 +++ kiosk-marketplace/Move.toml | 38 +++ kiosk-marketplace/sources/adapter.move | 130 +++++++++ .../sources/collection_bidding_ext.move | 246 ++++++++++++++++++ kiosk-marketplace/sources/single_bid_ext.move | 176 +++++++++++++ kiosk-marketplace/sources/trading_ext.move | 150 +++++++++++ kiosk-marketplace/tests/adapter_tests.move | 83 ++++++ .../tests/collection_bidding_tests.move | 96 +++++++ .../tests/trading_ext_tests.move | 81 ++++++ 10 files changed, 1037 insertions(+) create mode 100644 kiosk-marketplace/.gitignore create mode 100644 kiosk-marketplace/Move.lock create mode 100644 kiosk-marketplace/Move.toml create mode 100644 kiosk-marketplace/sources/adapter.move create mode 100644 kiosk-marketplace/sources/collection_bidding_ext.move create mode 100644 kiosk-marketplace/sources/single_bid_ext.move create mode 100644 kiosk-marketplace/sources/trading_ext.move create mode 100644 kiosk-marketplace/tests/adapter_tests.move create mode 100644 kiosk-marketplace/tests/collection_bidding_tests.move create mode 100644 kiosk-marketplace/tests/trading_ext_tests.move diff --git a/kiosk-marketplace/.gitignore b/kiosk-marketplace/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/kiosk-marketplace/.gitignore @@ -0,0 +1 @@ +build diff --git a/kiosk-marketplace/Move.lock b/kiosk-marketplace/Move.lock new file mode 100644 index 0000000..be80855 --- /dev/null +++ b/kiosk-marketplace/Move.lock @@ -0,0 +1,36 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 0 +manifest_digest = "7760EF9C6E870653F409B1B878A4C1BB2934E076A6ABFB75790B5EDFA4D04F62" +deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" + +dependencies = [ + { name = "Kiosk" }, + { name = "Sui" }, +] + +[[move.package]] +name = "Kiosk" +source = { local = "../kiosk" } + +dependencies = [ + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.19.0" +edition = "legacy" +flavor = "sui" diff --git a/kiosk-marketplace/Move.toml b/kiosk-marketplace/Move.toml new file mode 100644 index 0000000..47b0af3 --- /dev/null +++ b/kiosk-marketplace/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "kiosk-mkt-adapter" + +# edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha +# license = "" # e.g., "MIT", "GPL", "Apache 2.0" +# authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } +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] +mkt = "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/kiosk-marketplace/sources/adapter.move b/kiosk-marketplace/sources/adapter.move new file mode 100644 index 0000000..39c8985 --- /dev/null +++ b/kiosk-marketplace/sources/adapter.move @@ -0,0 +1,130 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// The best practical approach to trading on marketplaces and favoring their +/// fees and conditions is issuing an additional `TransferRequest` (eg `Market`). +/// However, doing so is not always possible because it must be copied from +/// TransferRequest. Mainly because the price of the sale is not known to +/// the very moment of the sale. And if there's already a TransferRequest, +/// how do we enforce the creation of an extra request? That means that sale has +/// already happened. +/// +/// To address this problem and also solve the extension interoperability issue, +/// we created a `marketplace_adapter` - simple utility which wraps the +/// `PurchaseCap` and handles the last step of the purchase flow in the Kiosk. +/// +/// Unlike `PurchaseCap` purpose of which is to be as compatible as possible, +/// `MarketPurchaseCap` - the wrapper - only comes with a `store` to reduce the +/// amount of scenarios when it is transferred by accident or sent to an address +/// or object. +/// +/// Notes: +/// +/// - The Adapter intentionally does not have any errors built-in and the error +/// handling needs to be implemented in the extension utilizing the Marketplace +/// Adapter. +module mkt::adapter { + use sui::transfer_policy::{Self as policy, TransferRequest}; + use sui::kiosk::{Self, Kiosk, KioskOwnerCap, PurchaseCap}; + use sui::tx_context::TxContext; + use sui::object::ID; + use sui::coin::Coin; + use sui::sui::SUI; + + friend mkt::collection_bidding_ext; + friend mkt::single_bid_ext; + friend mkt::trading_ext; + + /// The `NoMarket` type is used to provide a default `Market` type parameter + /// for a scenario when the `MarketplaceAdapter` is not used and extensions + /// maintain uniformity of emitted events. NoMarket = no marketplace. + struct NoMarket {} + + /// The `MarketPurchaseCap` wraps the `PurchaseCap` and forces the unlocking + /// party to satisfy the `TransferPolicy` requirements. + struct MarketPurchaseCap has store { + purchase_cap: PurchaseCap + } + + /// Create a new `PurchaseCap` and wrap it into the `MarketPurchaseCap`. + public(friend) fun new( + kiosk: &mut Kiosk, + cap: &KioskOwnerCap, + item_id: ID, + min_price: u64, + ctx: &mut TxContext + ): MarketPurchaseCap { + MarketPurchaseCap { + purchase_cap: kiosk::list_with_purchase_cap( + kiosk, cap, item_id, min_price, ctx + ) + } + } + + /// Return the `MarketPurchaseCap` to the `Kiosk`. Similar to how the + /// `PurchaseCap` can be returned at any moment. But it can't be unwrapped + /// into the `PurchaseCap` because that would allow cheating on a `Market`. + public(friend) fun return_cap( + kiosk: &mut Kiosk, + cap: MarketPurchaseCap, + _ctx: &mut TxContext + ) { + let MarketPurchaseCap { purchase_cap } = cap; + kiosk::return_purchase_cap(kiosk, purchase_cap); + } + + /// Use the `MarketPurchaseCap` to purchase an item from the `Kiosk`. Unlike + /// the default flow, this function adds a `TransferRequest` which + /// forces the unlocking party to satisfy the `TransferPolicy` + public(friend) fun purchase( + kiosk: &mut Kiosk, + cap: MarketPurchaseCap, + coin: Coin, + _ctx: &mut TxContext + ): (T, TransferRequest, TransferRequest) { + let MarketPurchaseCap { purchase_cap } = cap; + let (item, request) = kiosk::purchase_with_cap(kiosk, purchase_cap, coin); + let market_request = policy::new_request( + policy::item(&request), + policy::paid(&request), + policy::from(&request), + ); + + (item, request, market_request) + } + + /// Purchase an item listed with "NoMarket" policy. This function ignores + /// the `Market` type parameter and returns only a `TransferRequest`. + public(friend) fun purchase_no_market( + kiosk: &mut Kiosk, + cap: MarketPurchaseCap, + coin: Coin, + _ctx: &mut TxContext + ): (T, TransferRequest) { + let MarketPurchaseCap { purchase_cap } = cap; + kiosk::purchase_with_cap(kiosk, purchase_cap, coin) + } + + // === Getters === + + /// Handy wrapper to read the `kiosk` field of the inner `PurchaseCap` + public(friend) fun kiosk(self: &MarketPurchaseCap): ID { + kiosk::purchase_cap_kiosk(&self.purchase_cap) + } + + /// Handy wrapper to read the `item` field of the inner `PurchaseCap` + public(friend) fun item(self: &MarketPurchaseCap): ID { + kiosk::purchase_cap_item(&self.purchase_cap) + } + + /// Handy wrapper to read the `min_price` field of the inner `PurchaseCap` + public(friend) fun min_price(self: &MarketPurchaseCap): u64 { + kiosk::purchase_cap_min_price(&self.purchase_cap) + } + + // === Test === + + #[test_only] friend mkt::adapter_tests; + #[test_only] friend mkt::trading_ext_tests; + #[test_only] friend mkt::collection_bidding_tests; +} diff --git a/kiosk-marketplace/sources/collection_bidding_ext.move b/kiosk-marketplace/sources/collection_bidding_ext.move new file mode 100644 index 0000000..254c211 --- /dev/null +++ b/kiosk-marketplace/sources/collection_bidding_ext.move @@ -0,0 +1,246 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Implements Collection Bidding. Currently it's a Marketplace-only functionality. +/// +/// It is important that the bidder chooses the Marketplace, not the buyer. +module mkt::collection_bidding_ext { + use std::option::Option; + use std::type_name; + use std::vector; + + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::kiosk_extension as ext; + use sui::tx_context::TxContext; + use sui::coin::{Self, Coin}; + use sui::transfer_policy::{ + Self as policy, + TransferPolicy, + TransferRequest, + }; + use sui::sui::SUI; + use sui::vec_set; + use sui::object::{Self, ID}; + use sui::event; + use sui::pay; + use sui::bag; + + use kiosk::personal_kiosk; + use kiosk::kiosk_lock_rule::Rule as LockRule; + use mkt::adapter::{Self as mkt, MarketPurchaseCap, NoMarket}; + + /// Trying to perform an action in another user's Kiosk. + const ENotAuthorized: u64 = 0; + /// Trying to accept the bid in a disabled extension. + const EExtensionDisabled: u64 = 1; + /// A `PurchaseCap` was created in a different Kiosk. + const EIncorrectKiosk: u64 = 2; + /// The bid amount is less than the minimum price. This check makes sure + /// the seller does not lose due to a race condition. By specifying the + /// `min_price` in the `MarketPurchaseCap` the seller sets the minimum price + /// for the item. And if there's a race, and someone frontruns the seller, + /// the seller does not accidentally take the bid for lower than they expected. + const EBidDoesntMatchExpectation: u64 = 3; + /// Trying to accept a bid using a wrong function. + const EIncorrectMarketArg: u64 = 4; + /// Trying to accept a bid that does not exist. + const EBidNotFound: u64 = 5; + /// Trying to place a bid with no coins. + const ENoCoinsPassed: u64 = 6; + /// Trying to access the extension without installing it. + const EExtensionNotInstalled: u64 = 7; + + /// A key for Extension storage - a single bid on an item of type `T` on a `Market`. + struct Bid has copy, store, drop {} + + // === Events === + + /// An event that is emitted when a new bid is placed. + struct NewBid has copy, drop { + kiosk_id: ID, + bids: vector, + is_personal: bool, + } + + /// An event that is emitted when a bid is accepted. + struct BidAccepted has copy, drop { + seller_kiosk_id: ID, + buyer_kiosk_id: ID, + item_id: ID, + amount: u64, + buyer_is_personal: bool, + seller_is_personal: bool, + } + + /// An event that is emitted when a bid is canceled. + struct BidCanceled has copy, drop { + kiosk_id: ID, + kiosk_owner: Option
, + } + + // === Extension === + + /// Extension permissions - `place` and `lock`. + const PERMISSIONS: u128 = 3; + + /// The Extension witness. + struct Extension has drop {} + + /// Install the extension into the Kiosk. + public fun add(self: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { + ext::add(Extension {}, self, cap, PERMISSIONS, ctx) + } + + // === Bidding logic === + + /// Place a bid on any item in a collection (`T`). We do not assert that all + /// the values in the `place_bids` are identical, the amounts are emitted + /// in the event, the order is reversed. + /// + /// Use `sui::pay::split_n` to prepare the Coins for the bid. + public fun place_bids( + self: &mut Kiosk, + cap: &KioskOwnerCap, + bids: vector>, + _ctx: &mut TxContext + ) { + assert!(vector::length(&bids) > 0, ENoCoinsPassed); + assert!(kiosk::has_access(self, cap), ENotAuthorized); + assert!(ext::is_installed(self), EExtensionNotInstalled); + + let amounts = vector[]; + let (i, count) = (0, vector::length(&bids)); + while (i < count) { + vector::push_back(&mut amounts, coin::value(vector::borrow(&bids, i))); + i = i + 1; + }; + + event::emit(NewBid { + kiosk_id: object::id(self), + bids: amounts, + is_personal: personal_kiosk::is_personal(self) + }); + + bag::add(ext::storage_mut(Extension {}, self), Bid {}, bids); + } + + /// Cancel all bids, return the funds to the owner. + public fun cancel_all( + self: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext + ): Coin { + assert!(ext::is_installed(self), EExtensionNotInstalled); + assert!(kiosk::has_access(self, cap), ENotAuthorized); + + event::emit(BidCanceled { + kiosk_id: object::id(self), + kiosk_owner: personal_kiosk::try_owner(self) + }); + + let coins = bag::remove(ext::storage_mut(Extension {}, self), Bid {}); + let total = coin::zero(ctx); + pay::join_vec(&mut total, coins); + total + } + + /// Accept the bid and make a purchase on in the `Kiosk`. + /// + /// 1. The seller creates a `MarketPurchaseCap` using the Marketplace adapter, + /// and passes the Cap to this function. The `min_price` value is the expectation + /// of the seller. It protects them from race conditions in case the next bid + /// is smaller than the current one and someone frontrunned the seller. + /// See `EBidDoesntMatchExpectation` for more details on this scenario. + /// + /// 2. The `bid` is taken from the `source` Kiosk's extension storage and is + /// used to purchase the item with the `MarketPurchaseCap`. Proceeds go to + /// the `destination` Kiosk, as this Kiosk offers the `T`. + /// + /// 3. The item is placed in the `destination` Kiosk using the `place` or `lock` + /// functions (see `PERMISSIONS`). The extension must be installed and enabled + /// for this to work. + public fun accept_market_bid( + buyer: &mut Kiosk, + seller: &mut Kiosk, + mkt_cap: MarketPurchaseCap, + policy: &TransferPolicy, + // keeping these arguments for extendability + _lock: bool, + ctx: &mut TxContext + ): (TransferRequest, TransferRequest) { + assert!(ext::is_installed(buyer), EExtensionNotInstalled); + assert!(ext::is_installed(seller), EExtensionNotInstalled); + + let storage = ext::storage_mut(Extension {}, buyer); + assert!(bag::contains(storage, Bid {}), EBidNotFound); + + // Take 1 Coin from the bag - this is our bid (bids can't be empty, we + // make sure of it). + let bid = vector::pop_back(bag::borrow_mut(storage, Bid {})); + + // If there are no bids left, remove the bag and the key from the storage. + if (bid_count(buyer) == 0) { + vector::destroy_empty>( + bag::remove( + ext::storage_mut(Extension {}, buyer), + Bid {} + ) + ); + }; + + let amount = coin::value(&bid); + + assert!(ext::is_enabled(seller), EExtensionDisabled); + assert!(mkt::kiosk(&mkt_cap) == object::id(seller), EIncorrectKiosk); + assert!(mkt::min_price(&mkt_cap) <= amount, EBidDoesntMatchExpectation); + assert!(type_name::get() != type_name::get(), EIncorrectMarketArg); + + // Perform the purchase operation in the seller's Kiosk using the `Bid`. + let (item, request, market_request) = mkt::purchase(seller, mkt_cap, bid, ctx); + + event::emit(BidAccepted { + amount, + item_id: object::id(&item), + buyer_kiosk_id: object::id(buyer), + seller_kiosk_id: object::id(seller), + buyer_is_personal: personal_kiosk::is_personal(buyer), + seller_is_personal: personal_kiosk::is_personal(seller) + }); + + // Place or lock the item in the `source` Kiosk. + place_or_lock(buyer, item, policy); + + (request, market_request) + } + + // === Getters === + + /// Number of currently active bids. + public fun offers_count(self: &Kiosk): u64 { + bag::length(ext::storage(Extension {}, self)) + } + + /// Number of bids on an item of type `T` on a `Market` in a `Kiosk`. + public fun bid_count(self: &Kiosk): u64 { + let coins = bag::borrow(ext::storage(Extension {}, self), Bid {}); + vector::length>(coins) + } + + /// Returns the amount of the bid on an item of type `T` on a `Market`. + /// The `NoMarket` generic can be used to check an item listed off the market. + public fun bid_amount(self: &Kiosk): u64 { + let coins = bag::borrow(ext::storage(Extension {}, self), Bid {}); + coin::value(vector::borrow>(coins, 0)) + } + + // === Internal === + + /// A helper function which either places or locks an item in the Kiosk depending + /// on the Rules set in the `TransferPolicy`. + fun place_or_lock(kiosk: &mut Kiosk, item: T, policy: &TransferPolicy) { + let should_lock = vec_set::contains(policy::rules(policy), &type_name::get()); + if (should_lock) { + ext::lock(Extension {}, kiosk, item, policy) + } else { + ext::place(Extension {}, kiosk, item, policy) + }; + } +} diff --git a/kiosk-marketplace/sources/single_bid_ext.move b/kiosk-marketplace/sources/single_bid_ext.move new file mode 100644 index 0000000..0e29f45 --- /dev/null +++ b/kiosk-marketplace/sources/single_bid_ext.move @@ -0,0 +1,176 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// +module mkt::single_bid_ext { + use std::type_name; + use sui::kiosk_extension as ext; + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::transfer_policy::{Self as policy, TransferPolicy, TransferRequest}; + use sui::tx_context::TxContext; + use sui::object::{Self, ID}; + use sui::coin::{Self, Coin}; + use sui::sui::SUI; + use sui::vec_set; + use sui::event; + use sui::bag; + + use kiosk::personal_kiosk; + use kiosk::kiosk_lock_rule::Rule as LockRule; + use mkt::adapter as mkt; + + /// Not the kiosk owner. + const ENotAuthorized: u64 = 0; + /// Item is not found in the kiosk. + const EItemNotFound: u64 = 1; + /// Item is already listed in the kiosk. + const EAlreadyListed: u64 = 2; + /// Extension is not installed in the kiosk. + const EExtensionNotInstalled: u64 = 3; + /// No bid found for the item. + const ENoBid: u64 = 4; + + /// The dynamic field key for the Bid. + struct Bid has copy, store, drop { item_id: ID } + + // === Events === + + /// Event emitted when a bid is placed. + struct NewBid has copy, drop { + kiosk_id: ID, + item_id: ID, + bid: u64, + is_personal: bool, + } + + /// Event emitted when a bid is accepted. + struct BidAccepted has copy, drop { + kiosk_id: ID, + item_id: ID, + bid: u64, + is_personal: bool, + } + + /// Event emitted when a bid is cancelled. + struct BidCancelled has copy, drop { + kiosk_id: ID, + item_id: ID, + bid: u64, + is_personal: bool, + } + + // === Extension === + + /// Extension permissions - `place` and `lock`. + const PERMISSIONS: u128 = 3; + + /// The Witness for the extension. + struct Extension has drop {} + + /// Install the extension into the Kiosk. + public fun add(self: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { + ext::add(Extension {}, self, cap, PERMISSIONS, ctx) + } + + // === Bidding Logic === + + /// Place a single bid for an item with a specified `ID`. + public fun place( + kiosk: &mut Kiosk, + cap: &KioskOwnerCap, + bid: Coin, + item_id: ID, + _ctx: &mut TxContext + ) { + assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); + assert!(ext::is_installed(kiosk), EExtensionNotInstalled); + + event::emit(NewBid { + kiosk_id: object::id(kiosk), + bid: coin::value(&bid), + is_personal: personal_kiosk::is_personal(kiosk), + item_id, + }); + + bag::add( + ext::storage_mut(Extension {}, kiosk), + Bid { item_id }, + bid + ) + } + + /// Accept a single bid for an item with a specified `ID`. For that the + /// seller lists and sells the item to the buyer in a single action. The + /// requests are issued and need to be resolved by the seller. + public fun accept( + buyer: &mut Kiosk, + seller: &mut Kiosk, + seller_cap: &KioskOwnerCap, + policy: &TransferPolicy, + item_id: ID, + ctx: &mut TxContext + ): (TransferRequest, TransferRequest) { + assert!(ext::is_installed(buyer), EExtensionNotInstalled); + assert!(bag::contains(ext::storage(Extension {}, buyer), Bid { item_id }), ENoBid); + assert!(kiosk::has_item(seller, item_id), EItemNotFound); + assert!(!kiosk::is_listed(seller, item_id), EAlreadyListed); + + let coin: Coin = bag::remove( + ext::storage_mut(Extension {}, buyer), + Bid { item_id }, + ); + + let amount = coin::value(&coin); + let mkt_cap = mkt::new(seller, seller_cap, item_id, amount, ctx); + let (item, req, mkt_req) = mkt::purchase(seller, mkt_cap, coin, ctx); + + event::emit(BidAccepted { + kiosk_id: object::id(buyer), + bid: amount, + is_personal: personal_kiosk::is_personal(buyer), + item_id, + }); + + place_or_lock(buyer, item, policy); + (req, mkt_req) + } + + /// Cancel a single bid for an item with a specified `ID`. + public fun cancel( + kiosk: &mut Kiosk, + kiosk_cap: &KioskOwnerCap, + item_id: ID, + _ctx: &mut TxContext + ): Coin { + assert!(kiosk::has_access(kiosk, kiosk_cap), ENotAuthorized); + assert!(ext::is_installed(kiosk), EExtensionNotInstalled); + assert!(bag::contains(ext::storage(Extension {}, kiosk), Bid { item_id }), ENoBid); + + let coin: Coin = bag::remove( + ext::storage_mut(Extension {}, kiosk), + Bid { item_id }, + ); + + event::emit(BidCancelled { + kiosk_id: object::id(kiosk), + bid: coin::value(&coin), + is_personal: personal_kiosk::is_personal(kiosk), + item_id, + }); + + coin + } + + // === Internal === + + /// A helper function which either places or locks an item in the Kiosk depending + /// on the Rules set in the `TransferPolicy`. + fun place_or_lock(kiosk: &mut Kiosk, item: T, policy: &TransferPolicy) { + let should_lock = vec_set::contains(policy::rules(policy), &type_name::get()); + if (should_lock) { + ext::lock(Extension {}, kiosk, item, policy) + } else { + ext::place(Extension {}, kiosk, item, policy) + }; + } +} diff --git a/kiosk-marketplace/sources/trading_ext.move b/kiosk-marketplace/sources/trading_ext.move new file mode 100644 index 0000000..4a2a456 --- /dev/null +++ b/kiosk-marketplace/sources/trading_ext.move @@ -0,0 +1,150 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This extension implements the default list-purchase flow but for a specific +/// market (using the Marketplace Adapter). +/// +/// Consists of 3 functions: +/// - list +/// - delist +/// - purchase +module mkt::trading_ext { + use std::option::Option; + + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::transfer_policy::TransferRequest; + use sui::kiosk_extension as ext; + use sui::tx_context::TxContext; + use sui::object::{Self, ID}; + use sui::coin::{Self, Coin}; + use sui::sui::SUI; + use sui::event; + use sui::bag; + + use kiosk::personal_kiosk; + use mkt::adapter::{Self as mkt, MarketPurchaseCap}; + + /// For when the caller is not the owner of the Kiosk. + const ENotOwner: u64 = 0; + /// Trying to purchase or delist an item that is not listed. + const ENotListed: u64 = 1; + /// The payment is not enough to purchase the item. + const EIncorrectAmount: u64 = 2; + + // === Events === + + /// An item has been listed on a Marketplace. + struct ItemListed has copy, drop { + kiosk_id: ID, + item_id: ID, + price: u64, + is_personal: bool, + } + + /// An item has been delisted from a Marketplace. + struct ItemDelisted has copy, drop { + kiosk_id: ID, + item_id: ID, + is_personal: bool, + } + + /// An item has been purchased from a Marketplace. + struct ItemPurchased has copy, drop { + kiosk_id: ID, + item_id: ID, + kiosk_owner: Option
, + } + + // === Extension === + + /// The Extension Witness + struct Extension has drop {} + + /// This Extension does not require any permissions. + const PERMISSIONS: u128 = 0; + + /// Adds the Extension + public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { + ext::add(Extension {}, kiosk, cap, PERMISSIONS, ctx) + } + + // === Trading Functions === + + /// List an item on a specified Marketplace. + public fun list( + self: &mut Kiosk, + cap: &KioskOwnerCap, + item_id: ID, + price: u64, + ctx: &mut TxContext + ) { + assert!(kiosk::has_access(self, cap), ENotOwner); + + let mkt_cap = mkt::new(self, cap, item_id, price, ctx); + bag::add(ext::storage_mut(Extension {}, self), item_id, mkt_cap); + + event::emit(ItemListed { + is_personal: personal_kiosk::is_personal(self), + kiosk_id: object::id(self), + item_id, + price, + }); + } + + /// Delist an item from a specified Marketplace. + public fun delist( + self: &mut Kiosk, + cap: &KioskOwnerCap, + item_id: ID, + ctx: &mut TxContext + ) { + assert!(kiosk::has_access(self, cap), ENotOwner); + assert!(is_listed(self, item_id), ENotListed); + + let mkt_cap = bag::remove(ext::storage_mut(Extension {}, self), item_id); + mkt::return_cap(self, mkt_cap, ctx); + + event::emit(ItemDelisted { + is_personal: personal_kiosk::is_personal(self), + kiosk_id: object::id(self), + item_id + }); + } + + /// Purchase an item from a specified Marketplace. + public fun purchase( + self: &mut Kiosk, + item_id: ID, + payment: Coin, + ctx: &mut TxContext + ): (T, TransferRequest, TransferRequest) { + assert!(is_listed(self, item_id), ENotListed); + + let mkt_cap = bag::remove(ext::storage_mut(Extension {}, self), item_id); + assert!(coin::value(&payment) == mkt::min_price(&mkt_cap), EIncorrectAmount); + + event::emit(ItemPurchased { + kiosk_owner: personal_kiosk::try_owner(self), + kiosk_id: object::id(self), + item_id + }); + + mkt::purchase(self, mkt_cap, payment, ctx) + } + + // === Getters === + + /// Check if an item is currently listed on a specified Marketplace. + public fun is_listed(self: &Kiosk, item_id: ID): bool { + bag::contains_with_type>( + ext::storage(Extension {}, self), + item_id + ) + } + + /// Get the price of a currently listed item from a specified Marketplace. + public fun price(self: &Kiosk, item_id: ID): u64 { + let mkt_cap = bag::borrow(ext::storage(Extension {}, self), item_id); + mkt::min_price(mkt_cap) + } +} diff --git a/kiosk-marketplace/tests/adapter_tests.move b/kiosk-marketplace/tests/adapter_tests.move new file mode 100644 index 0000000..cf19ec8 --- /dev/null +++ b/kiosk-marketplace/tests/adapter_tests.move @@ -0,0 +1,83 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// Tests for the marketplace adapter. +module mkt::adapter_tests { + use sui::coin; + use sui::kiosk; + use sui::object; + use sui::sui::SUI; + use sui::transfer_policy as policy; + use sui::kiosk_test_utils::{Self as test, Asset}; + + use mkt::adapter as mkt; + + /// The Marketplace witness. + struct MyMarket has drop {} + + /// The witness to use in tests. + struct OTW has drop {} + + // Performs a test of the `new` and `return_cap` functions. Not supposed to + // abort, and there's only so many scenarios where it can fail due to strict + // type requirements. + #[test] fun test_new_return_flow() { + let ctx = &mut test::ctx(); + let (kiosk, kiosk_cap) = test::get_kiosk(ctx); + let (asset, asset_id) = test::get_asset(ctx); + + kiosk::place(&mut kiosk, &kiosk_cap, asset); + + let mkt_cap = mkt::new( + &mut kiosk, &kiosk_cap, asset_id, 100000, ctx + ); + + assert!(mkt::item(&mkt_cap) == asset_id, 0); + assert!(mkt::min_price(&mkt_cap) == 100000, 1); + assert!(mkt::kiosk(&mkt_cap) == object::id(&kiosk), 2); + + mkt::return_cap(&mut kiosk, mkt_cap, ctx); + + let asset = kiosk::take(&mut kiosk, &kiosk_cap, asset_id); + test::return_kiosk(kiosk, kiosk_cap, ctx); + test::return_assets(vector[ asset ]); + } + + // Perform a `purchase` using the `MarketPurchaseCap`. Type constraints make + // it impossible to cheat and pass another Cap. So the number of potential + // fail scenarios is limited and already covered by the base Kiosk + #[test] fun test_new_purchase_flow() { + let ctx = &mut test::ctx(); + let (kiosk, kiosk_cap) = test::get_kiosk(ctx); + let (asset, asset_id) = test::get_asset(ctx); + + kiosk::place(&mut kiosk, &kiosk_cap, asset); + + // Lock an item in the Marketplace + let mkt_cap = mkt::new( + &mut kiosk, &kiosk_cap, asset_id, 100000, ctx + ); + + // Mint a Coin and make a purchase + let coin = coin::mint_for_testing(100000, ctx); + let (item, req, mkt_req) = mkt::purchase( + &mut kiosk, mkt_cap, coin, ctx + ); + + // Get Policy for the Asset, use it and clean up. + let (policy, policy_cap) = test::get_policy(ctx); + policy::confirm_request(&policy, req); + test::return_policy(policy, policy_cap, ctx); + + // Get Policy for the Marketplace, use it and clean up. + let (policy, policy_cap) = policy::new_for_testing(ctx); + policy::confirm_request(&policy, mkt_req); + let proceeds = policy::destroy_and_withdraw(policy, policy_cap, ctx); + coin::destroy_zero(proceeds); + + // Now deal with the item and with the Kiosk. + test::return_assets(vector[ item ]); + test::return_kiosk(kiosk, kiosk_cap, ctx); + } +} diff --git a/kiosk-marketplace/tests/collection_bidding_tests.move b/kiosk-marketplace/tests/collection_bidding_tests.move new file mode 100644 index 0000000..02b6aef --- /dev/null +++ b/kiosk-marketplace/tests/collection_bidding_tests.move @@ -0,0 +1,96 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module mkt::collection_bidding_tests { + use sui::coin; + use sui::kiosk; + use sui::tx_context::TxContext; + use sui::kiosk_test_utils::{Self as test, Asset}; + use sui::transfer_policy::{ + Self as policy, + TransferPolicy, + TransferPolicyCap + }; + + use mkt::adapter as mkt; + use mkt::collection_bidding_ext::{Self as bidding}; + + /// The Marketplace witness. + struct MyMarket has drop {} + + #[test] + fun test_simple_bid() { + let ctx = &mut test::ctx(); + let (buyer_kiosk, buyer_cap) = test::get_kiosk(ctx); + + // install extension + bidding::add(&mut buyer_kiosk, &buyer_cap, ctx); + + // place bids on an Asset: 100 MIST + bidding::place_bids( + &mut buyer_kiosk, + &buyer_cap, + vector[ test::get_sui(100, ctx) ], + ctx + ); + + // prepare the seller Kiosk + let (seller_kiosk, seller_cap) = test::get_kiosk(ctx); + let (asset, asset_id) = test::get_asset(ctx); + + + // place the asset and create a MarketPurchaseCap + bidding::add(&mut seller_kiosk, &seller_cap, ctx); + kiosk::place(&mut seller_kiosk, &seller_cap, asset); + + let mkt_cap = mkt::new( + &mut seller_kiosk, &seller_cap, asset_id, 100, ctx + ); + + let (asset_policy, asset_policy_cap) = get_policy(ctx); + let (mkt_policy, mkt_policy_cap) = get_policy(ctx); + + // take the bid and perform the purchase + let (asset_request, mkt_request) = bidding::accept_market_bid( + &mut buyer_kiosk, + &mut seller_kiosk, + mkt_cap, + &asset_policy, + false, + ctx + ); + + policy::confirm_request(&asset_policy, asset_request); + policy::confirm_request(&mkt_policy, mkt_request); + + return_policy(asset_policy, asset_policy_cap, ctx); + return_policy(mkt_policy, mkt_policy_cap, ctx); + + assert!(kiosk::has_item(&buyer_kiosk, asset_id), 0); + assert!(!kiosk::has_item(&seller_kiosk, asset_id), 1); + + let asset = kiosk::take(&mut buyer_kiosk, &buyer_cap, asset_id); + + test::return_assets(vector[ asset ]); + test::return_kiosk(buyer_kiosk, buyer_cap, ctx); + let amount = test::return_kiosk(seller_kiosk, seller_cap, ctx); + + assert!(amount == 100, 2); + } + + fun get_policy( + ctx: &mut TxContext + ): (TransferPolicy, TransferPolicyCap) { + policy::new_for_testing(ctx) + } + + fun return_policy( + policy: TransferPolicy, + policy_cap: TransferPolicyCap, + ctx: &mut TxContext + ): u64 { + let proceeds = policy::destroy_and_withdraw(policy, policy_cap, ctx); + coin::burn_for_testing(proceeds) + } +} diff --git a/kiosk-marketplace/tests/trading_ext_tests.move b/kiosk-marketplace/tests/trading_ext_tests.move new file mode 100644 index 0000000..db9c53c --- /dev/null +++ b/kiosk-marketplace/tests/trading_ext_tests.move @@ -0,0 +1,81 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +/// Tests for the marketplace `marketplace_trading_ext`. +module mkt::trading_ext_tests { + use sui::coin; + use sui::object::ID; + use sui::kiosk_extension; + use sui::tx_context::TxContext; + use sui::transfer_policy as policy; + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::kiosk_test_utils::{Self as test, Asset}; + use mkt::trading_ext as ext; + + const PRICE: u64 = 100_000; + + /// Marketplace type. + struct MyMarket has drop {} + + #[test] fun test_list_and_delist() { + let ctx = &mut test::ctx(); + let (kiosk, kiosk_cap, asset_id) = prepare(ctx); + + ext::list(&mut kiosk, &kiosk_cap, asset_id, PRICE, ctx); + + assert!(ext::is_listed(&kiosk, asset_id), 0); + assert!(ext::price(&kiosk, asset_id) == PRICE, 1); + + ext::delist(&mut kiosk, &kiosk_cap, asset_id, ctx); + + let asset = kiosk::take(&mut kiosk, &kiosk_cap, asset_id); + test::return_assets(vector[ asset ]); + wrapup(kiosk, kiosk_cap, ctx); + } + + #[test] fun test_list_and_purchase() { + let ctx = &mut test::ctx(); + let (kiosk, kiosk_cap, asset_id) = prepare(ctx); + + ext::list(&mut kiosk, &kiosk_cap, asset_id, PRICE, ctx); + + let coin = test::get_sui(PRICE, ctx); + let (item, req, mkt_req) = ext::purchase( + &mut kiosk, asset_id, coin, ctx + ); + + // Resolve creator's Policy + let (policy, policy_cap) = test::get_policy(ctx); + policy::confirm_request(&policy, req); + test::return_policy(policy, policy_cap, ctx); + + // Resolve marketplace's Policy + let (policy, policy_cap) = policy::new_for_testing(ctx); + policy::confirm_request(&policy, mkt_req); + let proceeds = policy::destroy_and_withdraw(policy, policy_cap, ctx); + coin::destroy_zero(proceeds); + + // Deal with the Asset + Kiosk, KioskOwnerCap + test::return_assets(vector[ item ]); + wrapup(kiosk, kiosk_cap, ctx); + } + + /// Prepare a Kiosk with: + /// - extension installed + /// - an asset inside + fun prepare(ctx: &mut TxContext): (Kiosk, KioskOwnerCap, ID) { + let (kiosk, kiosk_cap) = test::get_kiosk(ctx); + let (asset, asset_id) = test::get_asset(ctx); + + kiosk::place(&mut kiosk, &kiosk_cap, asset); + ext::add(&mut kiosk, &kiosk_cap, ctx); + (kiosk, kiosk_cap, asset_id) + } + + /// Wrap everything up; remove the extension and the asset. + fun wrapup(kiosk: Kiosk, cap: KioskOwnerCap, ctx: &mut TxContext) { + kiosk_extension::remove(&mut kiosk, &cap); + test::return_kiosk(kiosk, cap, ctx); + } +} From 22f385b2d627fc9771a24a4883f88a2e18fcdb2f Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Mon, 26 Feb 2024 12:39:34 +0300 Subject: [PATCH 02/12] changes collection bidding methods --- .../sources/collection_bidding_ext.move | 33 +++++++++---------- kiosk-marketplace/sources/single_bid_ext.move | 1 + 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/kiosk-marketplace/sources/collection_bidding_ext.move b/kiosk-marketplace/sources/collection_bidding_ext.move index 254c211..53d09a0 100644 --- a/kiosk-marketplace/sources/collection_bidding_ext.move +++ b/kiosk-marketplace/sources/collection_bidding_ext.move @@ -27,28 +27,20 @@ module mkt::collection_bidding_ext { use kiosk::personal_kiosk; use kiosk::kiosk_lock_rule::Rule as LockRule; - use mkt::adapter::{Self as mkt, MarketPurchaseCap, NoMarket}; + use mkt::adapter::{Self as mkt, NoMarket}; /// Trying to perform an action in another user's Kiosk. const ENotAuthorized: u64 = 0; /// Trying to accept the bid in a disabled extension. const EExtensionDisabled: u64 = 1; - /// A `PurchaseCap` was created in a different Kiosk. - const EIncorrectKiosk: u64 = 2; - /// The bid amount is less than the minimum price. This check makes sure - /// the seller does not lose due to a race condition. By specifying the - /// `min_price` in the `MarketPurchaseCap` the seller sets the minimum price - /// for the item. And if there's a race, and someone frontruns the seller, - /// the seller does not accidentally take the bid for lower than they expected. - const EBidDoesntMatchExpectation: u64 = 3; /// Trying to accept a bid using a wrong function. - const EIncorrectMarketArg: u64 = 4; + const EIncorrectMarketArg: u64 = 2; /// Trying to accept a bid that does not exist. - const EBidNotFound: u64 = 5; + const EBidNotFound: u64 = 3; /// Trying to place a bid with no coins. - const ENoCoinsPassed: u64 = 6; + const ENoCoinsPassed: u64 = 4; /// Trying to access the extension without installing it. - const EExtensionNotInstalled: u64 = 7; + const EExtensionNotInstalled: u64 = 5; /// A key for Extension storage - a single bid on an item of type `T` on a `Market`. struct Bid has copy, store, drop {} @@ -160,14 +152,15 @@ module mkt::collection_bidding_ext { public fun accept_market_bid( buyer: &mut Kiosk, seller: &mut Kiosk, - mkt_cap: MarketPurchaseCap, + seller_cap: &KioskOwnerCap, policy: &TransferPolicy, + item_id: ID, // keeping these arguments for extendability _lock: bool, ctx: &mut TxContext ): (TransferRequest, TransferRequest) { assert!(ext::is_installed(buyer), EExtensionNotInstalled); - assert!(ext::is_installed(seller), EExtensionNotInstalled); + assert!(kiosk::has_access(seller, seller_cap), ENotAuthorized); let storage = ext::storage_mut(Extension {}, buyer); assert!(bag::contains(storage, Bid {}), EBidNotFound); @@ -188,11 +181,15 @@ module mkt::collection_bidding_ext { let amount = coin::value(&bid); - assert!(ext::is_enabled(seller), EExtensionDisabled); - assert!(mkt::kiosk(&mkt_cap) == object::id(seller), EIncorrectKiosk); - assert!(mkt::min_price(&mkt_cap) <= amount, EBidDoesntMatchExpectation); + assert!(ext::is_enabled(buyer), EExtensionDisabled); + // assert!(mkt::kiosk(&mkt_cap) == object::id(seller), EIncorrectKiosk); + // assert!(mkt::min_price(&mkt_cap) <= amount, EBidDoesntMatchExpectation); assert!(type_name::get() != type_name::get(), EIncorrectMarketArg); + let mkt_cap = mkt::new( + seller, seller_cap, item_id, amount, ctx + ); + // Perform the purchase operation in the seller's Kiosk using the `Bid`. let (item, request, market_request) = mkt::purchase(seller, mkt_cap, bid, ctx); diff --git a/kiosk-marketplace/sources/single_bid_ext.move b/kiosk-marketplace/sources/single_bid_ext.move index 0e29f45..cdcde03 100644 --- a/kiosk-marketplace/sources/single_bid_ext.move +++ b/kiosk-marketplace/sources/single_bid_ext.move @@ -108,6 +108,7 @@ module mkt::single_bid_ext { seller_cap: &KioskOwnerCap, policy: &TransferPolicy, item_id: ID, + _lock: bool, ctx: &mut TxContext ): (TransferRequest, TransferRequest) { assert!(ext::is_installed(buyer), EExtensionNotInstalled); From 0054d268a53e9d39c785314555bc8df4f1e1ffdc Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Mon, 26 Feb 2024 13:04:21 +0300 Subject: [PATCH 03/12] tests pass --- kiosk-marketplace/sources/adapter.move | 1 - kiosk-marketplace/tests/collection_bidding_tests.move | 11 +++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/kiosk-marketplace/sources/adapter.move b/kiosk-marketplace/sources/adapter.move index 39c8985..1b176f6 100644 --- a/kiosk-marketplace/sources/adapter.move +++ b/kiosk-marketplace/sources/adapter.move @@ -126,5 +126,4 @@ module mkt::adapter { #[test_only] friend mkt::adapter_tests; #[test_only] friend mkt::trading_ext_tests; - #[test_only] friend mkt::collection_bidding_tests; } diff --git a/kiosk-marketplace/tests/collection_bidding_tests.move b/kiosk-marketplace/tests/collection_bidding_tests.move index 02b6aef..fcf12c6 100644 --- a/kiosk-marketplace/tests/collection_bidding_tests.move +++ b/kiosk-marketplace/tests/collection_bidding_tests.move @@ -13,7 +13,6 @@ module mkt::collection_bidding_tests { TransferPolicyCap }; - use mkt::adapter as mkt; use mkt::collection_bidding_ext::{Self as bidding}; /// The Marketplace witness. @@ -39,15 +38,10 @@ module mkt::collection_bidding_tests { let (seller_kiosk, seller_cap) = test::get_kiosk(ctx); let (asset, asset_id) = test::get_asset(ctx); - // place the asset and create a MarketPurchaseCap - bidding::add(&mut seller_kiosk, &seller_cap, ctx); + // bidding::add(&mut seller_kiosk, &seller_cap, ctx); kiosk::place(&mut seller_kiosk, &seller_cap, asset); - let mkt_cap = mkt::new( - &mut seller_kiosk, &seller_cap, asset_id, 100, ctx - ); - let (asset_policy, asset_policy_cap) = get_policy(ctx); let (mkt_policy, mkt_policy_cap) = get_policy(ctx); @@ -55,8 +49,9 @@ module mkt::collection_bidding_tests { let (asset_request, mkt_request) = bidding::accept_market_bid( &mut buyer_kiosk, &mut seller_kiosk, - mkt_cap, + &seller_cap, &asset_policy, + asset_id, false, ctx ); From 410cd9971de4ea0d154323c75ae1b65041d4170a Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Mon, 26 Feb 2024 18:29:35 +0300 Subject: [PATCH 04/12] patching tests --- kiosk-marketplace/sources/adapter.move | 21 ++---- ...dding_ext.move => collection_bidding.move} | 74 ++++++++----------- kiosk-marketplace/sources/extension.move | 65 ++++++++++++++++ .../{trading_ext.move => fixed_trading.move} | 66 +++++++---------- .../{single_bid_ext.move => single_bid.move} | 50 ++++++------- .../tests/collection_bidding_tests.move | 60 ++++++++++----- .../tests/trading_ext_tests.move | 8 +- 7 files changed, 195 insertions(+), 149 deletions(-) rename kiosk-marketplace/sources/{collection_bidding_ext.move => collection_bidding.move} (76%) create mode 100644 kiosk-marketplace/sources/extension.move rename kiosk-marketplace/sources/{trading_ext.move => fixed_trading.move} (61%) rename kiosk-marketplace/sources/{single_bid_ext.move => single_bid.move} (78%) diff --git a/kiosk-marketplace/sources/adapter.move b/kiosk-marketplace/sources/adapter.move index 1b176f6..344ad12 100644 --- a/kiosk-marketplace/sources/adapter.move +++ b/kiosk-marketplace/sources/adapter.move @@ -3,15 +3,9 @@ /// The best practical approach to trading on marketplaces and favoring their /// fees and conditions is issuing an additional `TransferRequest` (eg `Market`). -/// However, doing so is not always possible because it must be copied from -/// TransferRequest. Mainly because the price of the sale is not known to -/// the very moment of the sale. And if there's already a TransferRequest, -/// how do we enforce the creation of an extra request? That means that sale has -/// already happened. -/// -/// To address this problem and also solve the extension interoperability issue, -/// we created a `marketplace_adapter` - simple utility which wraps the -/// `PurchaseCap` and handles the last step of the purchase flow in the Kiosk. +/// To achieve that, the `adapter` module provides a wrapper around the `PurchaseCap` +/// which adds an extra `Market` type parameter and forces the trade transaction +/// sender to satisfy the `TransferPolicy` requirements. /// /// Unlike `PurchaseCap` purpose of which is to be as compatible as possible, /// `MarketPurchaseCap` - the wrapper - only comes with a `store` to reduce the @@ -19,7 +13,6 @@ /// or object. /// /// Notes: -/// /// - The Adapter intentionally does not have any errors built-in and the error /// handling needs to be implemented in the extension utilizing the Marketplace /// Adapter. @@ -31,9 +24,9 @@ module mkt::adapter { use sui::coin::Coin; use sui::sui::SUI; - friend mkt::collection_bidding_ext; - friend mkt::single_bid_ext; - friend mkt::trading_ext; + friend mkt::collection_bidding; + friend mkt::fixed_trading; + friend mkt::single_bid; /// The `NoMarket` type is used to provide a default `Market` type parameter /// for a scenario when the `MarketplaceAdapter` is not used and extensions @@ -125,5 +118,5 @@ module mkt::adapter { // === Test === #[test_only] friend mkt::adapter_tests; - #[test_only] friend mkt::trading_ext_tests; + #[test_only] friend mkt::fixed_trading_tests; } diff --git a/kiosk-marketplace/sources/collection_bidding_ext.move b/kiosk-marketplace/sources/collection_bidding.move similarity index 76% rename from kiosk-marketplace/sources/collection_bidding_ext.move rename to kiosk-marketplace/sources/collection_bidding.move index 53d09a0..e0dffdf 100644 --- a/kiosk-marketplace/sources/collection_bidding_ext.move +++ b/kiosk-marketplace/sources/collection_bidding.move @@ -3,14 +3,17 @@ /// Implements Collection Bidding. Currently it's a Marketplace-only functionality. /// -/// It is important that the bidder chooses the Marketplace, not the buyer. -module mkt::collection_bidding_ext { +/// Flow: +/// 1. The bidder chooses a marketplace and calls `place_bids` with the amount +/// of coins they want to bid. +/// 2. The seller accepts the bid using `accept_market_bid`. The bid is taken +/// by the seller and the item is placed in the buyer's (bidder's) Kiosk. +/// 3. The seller resolves the requests for `Market` and creator. +module mkt::collection_bidding { use std::option::Option; use std::type_name; use std::vector; - use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; - use sui::kiosk_extension as ext; use sui::tx_context::TxContext; use sui::coin::{Self, Coin}; use sui::transfer_policy::{ @@ -28,6 +31,7 @@ module mkt::collection_bidding_ext { use kiosk::personal_kiosk; use kiosk::kiosk_lock_rule::Rule as LockRule; use mkt::adapter::{Self as mkt, NoMarket}; + use mkt::extension as ext; /// Trying to perform an action in another user's Kiosk. const ENotAuthorized: u64 = 0; @@ -70,19 +74,6 @@ module mkt::collection_bidding_ext { kiosk_owner: Option
, } - // === Extension === - - /// Extension permissions - `place` and `lock`. - const PERMISSIONS: u128 = 3; - - /// The Extension witness. - struct Extension has drop {} - - /// Install the extension into the Kiosk. - public fun add(self: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { - ext::add(Extension {}, self, cap, PERMISSIONS, ctx) - } - // === Bidding logic === /// Place a bid on any item in a collection (`T`). We do not assert that all @@ -91,14 +82,14 @@ module mkt::collection_bidding_ext { /// /// Use `sui::pay::split_n` to prepare the Coins for the bid. public fun place_bids( - self: &mut Kiosk, + kiosk: &mut Kiosk, cap: &KioskOwnerCap, bids: vector>, _ctx: &mut TxContext ) { assert!(vector::length(&bids) > 0, ENoCoinsPassed); - assert!(kiosk::has_access(self, cap), ENotAuthorized); - assert!(ext::is_installed(self), EExtensionNotInstalled); + assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); + assert!(ext::is_installed(kiosk), EExtensionNotInstalled); let amounts = vector[]; let (i, count) = (0, vector::length(&bids)); @@ -108,27 +99,27 @@ module mkt::collection_bidding_ext { }; event::emit(NewBid { - kiosk_id: object::id(self), + kiosk_id: object::id(kiosk), bids: amounts, - is_personal: personal_kiosk::is_personal(self) + is_personal: personal_kiosk::is_personal(kiosk) }); - bag::add(ext::storage_mut(Extension {}, self), Bid {}, bids); + bag::add(ext::storage_mut(kiosk), Bid {}, bids); } /// Cancel all bids, return the funds to the owner. public fun cancel_all( - self: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext + kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext ): Coin { - assert!(ext::is_installed(self), EExtensionNotInstalled); - assert!(kiosk::has_access(self, cap), ENotAuthorized); + assert!(ext::is_installed(kiosk), EExtensionNotInstalled); + assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); event::emit(BidCanceled { - kiosk_id: object::id(self), - kiosk_owner: personal_kiosk::try_owner(self) + kiosk_id: object::id(kiosk), + kiosk_owner: personal_kiosk::try_owner(kiosk) }); - let coins = bag::remove(ext::storage_mut(Extension {}, self), Bid {}); + let coins = bag::remove(ext::storage_mut(kiosk), Bid {}); let total = coin::zero(ctx); pay::join_vec(&mut total, coins); total @@ -159,10 +150,10 @@ module mkt::collection_bidding_ext { _lock: bool, ctx: &mut TxContext ): (TransferRequest, TransferRequest) { - assert!(ext::is_installed(buyer), EExtensionNotInstalled); + assert!(ext::is_enabled(buyer), EExtensionNotInstalled); assert!(kiosk::has_access(seller, seller_cap), ENotAuthorized); - let storage = ext::storage_mut(Extension {}, buyer); + let storage = ext::storage_mut(buyer); assert!(bag::contains(storage, Bid {}), EBidNotFound); // Take 1 Coin from the bag - this is our bid (bids can't be empty, we @@ -173,7 +164,7 @@ module mkt::collection_bidding_ext { if (bid_count(buyer) == 0) { vector::destroy_empty>( bag::remove( - ext::storage_mut(Extension {}, buyer), + ext::storage_mut(buyer), Bid {} ) ); @@ -181,7 +172,7 @@ module mkt::collection_bidding_ext { let amount = coin::value(&bid); - assert!(ext::is_enabled(buyer), EExtensionDisabled); + assert!(ext::is_enabled(buyer), EExtensionDisabled); // assert!(mkt::kiosk(&mkt_cap) == object::id(seller), EIncorrectKiosk); // assert!(mkt::min_price(&mkt_cap) <= amount, EBidDoesntMatchExpectation); assert!(type_name::get() != type_name::get(), EIncorrectMarketArg); @@ -210,21 +201,16 @@ module mkt::collection_bidding_ext { // === Getters === - /// Number of currently active bids. - public fun offers_count(self: &Kiosk): u64 { - bag::length(ext::storage(Extension {}, self)) - } - /// Number of bids on an item of type `T` on a `Market` in a `Kiosk`. - public fun bid_count(self: &Kiosk): u64 { - let coins = bag::borrow(ext::storage(Extension {}, self), Bid {}); + public fun bid_count(kiosk: &Kiosk): u64 { + let coins = bag::borrow(ext::storage(kiosk), Bid {}); vector::length>(coins) } /// Returns the amount of the bid on an item of type `T` on a `Market`. /// The `NoMarket` generic can be used to check an item listed off the market. - public fun bid_amount(self: &Kiosk): u64 { - let coins = bag::borrow(ext::storage(Extension {}, self), Bid {}); + public fun bid_amount(kiosk: &Kiosk): u64 { + let coins = bag::borrow(ext::storage(kiosk), Bid {}); coin::value(vector::borrow>(coins, 0)) } @@ -235,9 +221,9 @@ module mkt::collection_bidding_ext { fun place_or_lock(kiosk: &mut Kiosk, item: T, policy: &TransferPolicy) { let should_lock = vec_set::contains(policy::rules(policy), &type_name::get()); if (should_lock) { - ext::lock(Extension {}, kiosk, item, policy) + ext::lock(kiosk, item, policy) } else { - ext::place(Extension {}, kiosk, item, policy) + ext::place(kiosk, item, policy) }; } } diff --git a/kiosk-marketplace/sources/extension.move b/kiosk-marketplace/sources/extension.move new file mode 100644 index 0000000..563325f --- /dev/null +++ b/kiosk-marketplace/sources/extension.move @@ -0,0 +1,65 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// A single extension module for the package. Eliminates the need for separate +/// extension installation and management in every package. +module mkt::extension { + use sui::transfer_policy::TransferPolicy; + use sui::kiosk::{Kiosk, KioskOwnerCap}; + use sui::kiosk_extension as ext; + use sui::tx_context::TxContext; + use sui::bag::Bag; + + friend mkt::collection_bidding; + friend mkt::fixed_trading; + friend mkt::single_bid; + + /// The extension Witness. + struct Extension has drop {} + + /// Place and Lock permissions. + const PERMISSIONS: u128 = 3; + + /// Install the Marketplace Extension into the Kiosk. + public fun add( + kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext + ) { + ext::add(Extension {}, kiosk, cap, PERMISSIONS, ctx) + } + + /// Check if the extension is installed. + public fun is_installed(kiosk: &Kiosk): bool { + ext::is_installed(kiosk) + } + + /// Check if the extension is enabled. + public fun is_enabled(kiosk: &Kiosk): bool { + ext::is_enabled(kiosk) + } + + // === Friend only === + + /// Place the item into the Kiosk. + public(friend) fun place( + kiosk: &mut Kiosk, item: T, policy: &TransferPolicy + ) { + ext::place(Extension {}, kiosk, item, policy) + } + + /// Lock the item in the Kiosk. + public(friend) fun lock( + kiosk: &mut Kiosk, item: T, policy: &TransferPolicy + ) { + ext::lock(Extension {}, kiosk, item, policy) + } + + /// Get the reference to the extension storage. + public(friend) fun storage(kiosk: &Kiosk): &Bag { + ext::storage(Extension {}, kiosk) + } + + /// Get the mutable reference to the extension storage. + public(friend) fun storage_mut(kiosk: &mut Kiosk): &mut Bag { + ext::storage_mut(Extension {}, kiosk) + } +} diff --git a/kiosk-marketplace/sources/trading_ext.move b/kiosk-marketplace/sources/fixed_trading.move similarity index 61% rename from kiosk-marketplace/sources/trading_ext.move rename to kiosk-marketplace/sources/fixed_trading.move index 4a2a456..f623751 100644 --- a/kiosk-marketplace/sources/trading_ext.move +++ b/kiosk-marketplace/sources/fixed_trading.move @@ -8,12 +8,10 @@ /// - list /// - delist /// - purchase -module mkt::trading_ext { +module mkt::fixed_trading { use std::option::Option; - use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; use sui::transfer_policy::TransferRequest; - use sui::kiosk_extension as ext; use sui::tx_context::TxContext; use sui::object::{Self, ID}; use sui::coin::{Self, Coin}; @@ -23,6 +21,7 @@ module mkt::trading_ext { use kiosk::personal_kiosk; use mkt::adapter::{Self as mkt, MarketPurchaseCap}; + use mkt::extension as ext; /// For when the caller is not the owner of the Kiosk. const ENotOwner: u64 = 0; @@ -52,40 +51,27 @@ module mkt::trading_ext { struct ItemPurchased has copy, drop { kiosk_id: ID, item_id: ID, - kiosk_owner: Option
, - } - - // === Extension === - - /// The Extension Witness - struct Extension has drop {} - - /// This Extension does not require any permissions. - const PERMISSIONS: u128 = 0; - - /// Adds the Extension - public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { - ext::add(Extension {}, kiosk, cap, PERMISSIONS, ctx) + seller: Option
, } // === Trading Functions === /// List an item on a specified Marketplace. public fun list( - self: &mut Kiosk, + kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: ID, price: u64, ctx: &mut TxContext ) { - assert!(kiosk::has_access(self, cap), ENotOwner); + assert!(kiosk::has_access(kiosk, cap), ENotOwner); - let mkt_cap = mkt::new(self, cap, item_id, price, ctx); - bag::add(ext::storage_mut(Extension {}, self), item_id, mkt_cap); + let mkt_cap = mkt::new(kiosk, cap, item_id, price, ctx); + bag::add(ext::storage_mut(kiosk), item_id, mkt_cap); event::emit(ItemListed { - is_personal: personal_kiosk::is_personal(self), - kiosk_id: object::id(self), + is_personal: personal_kiosk::is_personal(kiosk), + kiosk_id: object::id(kiosk), item_id, price, }); @@ -93,58 +79,58 @@ module mkt::trading_ext { /// Delist an item from a specified Marketplace. public fun delist( - self: &mut Kiosk, + kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: ID, ctx: &mut TxContext ) { - assert!(kiosk::has_access(self, cap), ENotOwner); - assert!(is_listed(self, item_id), ENotListed); + assert!(kiosk::has_access(kiosk, cap), ENotOwner); + assert!(is_listed(kiosk, item_id), ENotListed); - let mkt_cap = bag::remove(ext::storage_mut(Extension {}, self), item_id); - mkt::return_cap(self, mkt_cap, ctx); + let mkt_cap = bag::remove(ext::storage_mut(kiosk), item_id); + mkt::return_cap(kiosk, mkt_cap, ctx); event::emit(ItemDelisted { - is_personal: personal_kiosk::is_personal(self), - kiosk_id: object::id(self), + is_personal: personal_kiosk::is_personal(kiosk), + kiosk_id: object::id(kiosk), item_id }); } /// Purchase an item from a specified Marketplace. public fun purchase( - self: &mut Kiosk, + kiosk: &mut Kiosk, item_id: ID, payment: Coin, ctx: &mut TxContext ): (T, TransferRequest, TransferRequest) { - assert!(is_listed(self, item_id), ENotListed); + assert!(is_listed(kiosk, item_id), ENotListed); - let mkt_cap = bag::remove(ext::storage_mut(Extension {}, self), item_id); + let mkt_cap = bag::remove(ext::storage_mut(kiosk), item_id); assert!(coin::value(&payment) == mkt::min_price(&mkt_cap), EIncorrectAmount); event::emit(ItemPurchased { - kiosk_owner: personal_kiosk::try_owner(self), - kiosk_id: object::id(self), + seller: personal_kiosk::try_owner(kiosk), + kiosk_id: object::id(kiosk), item_id }); - mkt::purchase(self, mkt_cap, payment, ctx) + mkt::purchase(kiosk, mkt_cap, payment, ctx) } // === Getters === /// Check if an item is currently listed on a specified Marketplace. - public fun is_listed(self: &Kiosk, item_id: ID): bool { + public fun is_listed(kiosk: &Kiosk, item_id: ID): bool { bag::contains_with_type>( - ext::storage(Extension {}, self), + ext::storage(kiosk), item_id ) } /// Get the price of a currently listed item from a specified Marketplace. - public fun price(self: &Kiosk, item_id: ID): u64 { - let mkt_cap = bag::borrow(ext::storage(Extension {}, self), item_id); + public fun price(kiosk: &Kiosk, item_id: ID): u64 { + let mkt_cap = bag::borrow(ext::storage(kiosk), item_id); mkt::min_price(mkt_cap) } } diff --git a/kiosk-marketplace/sources/single_bid_ext.move b/kiosk-marketplace/sources/single_bid.move similarity index 78% rename from kiosk-marketplace/sources/single_bid_ext.move rename to kiosk-marketplace/sources/single_bid.move index cdcde03..31ae868 100644 --- a/kiosk-marketplace/sources/single_bid_ext.move +++ b/kiosk-marketplace/sources/single_bid.move @@ -1,10 +1,16 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +/// A module for placing and managing single bids in a Kiosk. A single bid is +/// a bid for a specific item. The module provides functions to place, accept, and +/// cancel a bid. /// -module mkt::single_bid_ext { +/// Flow: +/// 1. A buyer places a bid for an item with a specified `ID`. +/// 2. A seller accepts the bid and sells the item to the buyer in a single action. +/// 3. The seller resolves the requests for `Market` and creator. +module mkt::single_bid { use std::type_name; - use sui::kiosk_extension as ext; use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; use sui::transfer_policy::{Self as policy, TransferPolicy, TransferRequest}; use sui::tx_context::TxContext; @@ -17,6 +23,7 @@ module mkt::single_bid_ext { use kiosk::personal_kiosk; use kiosk::kiosk_lock_rule::Rule as LockRule; + use mkt::extension as ext; use mkt::adapter as mkt; /// Not the kiosk owner. @@ -48,7 +55,8 @@ module mkt::single_bid_ext { kiosk_id: ID, item_id: ID, bid: u64, - is_personal: bool, + buyer_is_personal: bool, + seller_is_personal: bool, } /// Event emitted when a bid is cancelled. @@ -59,19 +67,6 @@ module mkt::single_bid_ext { is_personal: bool, } - // === Extension === - - /// Extension permissions - `place` and `lock`. - const PERMISSIONS: u128 = 3; - - /// The Witness for the extension. - struct Extension has drop {} - - /// Install the extension into the Kiosk. - public fun add(self: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { - ext::add(Extension {}, self, cap, PERMISSIONS, ctx) - } - // === Bidding Logic === /// Place a single bid for an item with a specified `ID`. @@ -83,7 +78,7 @@ module mkt::single_bid_ext { _ctx: &mut TxContext ) { assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); - assert!(ext::is_installed(kiosk), EExtensionNotInstalled); + assert!(ext::is_installed(kiosk), EExtensionNotInstalled); event::emit(NewBid { kiosk_id: object::id(kiosk), @@ -93,7 +88,7 @@ module mkt::single_bid_ext { }); bag::add( - ext::storage_mut(Extension {}, kiosk), + ext::storage_mut(kiosk), Bid { item_id }, bid ) @@ -111,13 +106,13 @@ module mkt::single_bid_ext { _lock: bool, ctx: &mut TxContext ): (TransferRequest, TransferRequest) { - assert!(ext::is_installed(buyer), EExtensionNotInstalled); - assert!(bag::contains(ext::storage(Extension {}, buyer), Bid { item_id }), ENoBid); + assert!(ext::is_enabled(buyer), EExtensionNotInstalled); + assert!(bag::contains(ext::storage(buyer), Bid { item_id }), ENoBid); assert!(kiosk::has_item(seller, item_id), EItemNotFound); assert!(!kiosk::is_listed(seller, item_id), EAlreadyListed); let coin: Coin = bag::remove( - ext::storage_mut(Extension {}, buyer), + ext::storage_mut(buyer), Bid { item_id }, ); @@ -128,7 +123,8 @@ module mkt::single_bid_ext { event::emit(BidAccepted { kiosk_id: object::id(buyer), bid: amount, - is_personal: personal_kiosk::is_personal(buyer), + buyer_is_personal: personal_kiosk::is_personal(buyer), + seller_is_personal: personal_kiosk::is_personal(seller), item_id, }); @@ -144,11 +140,11 @@ module mkt::single_bid_ext { _ctx: &mut TxContext ): Coin { assert!(kiosk::has_access(kiosk, kiosk_cap), ENotAuthorized); - assert!(ext::is_installed(kiosk), EExtensionNotInstalled); - assert!(bag::contains(ext::storage(Extension {}, kiosk), Bid { item_id }), ENoBid); + assert!(ext::is_installed(kiosk), EExtensionNotInstalled); + assert!(bag::contains(ext::storage(kiosk), Bid { item_id }), ENoBid); let coin: Coin = bag::remove( - ext::storage_mut(Extension {}, kiosk), + ext::storage_mut(kiosk), Bid { item_id }, ); @@ -169,9 +165,9 @@ module mkt::single_bid_ext { fun place_or_lock(kiosk: &mut Kiosk, item: T, policy: &TransferPolicy) { let should_lock = vec_set::contains(policy::rules(policy), &type_name::get()); if (should_lock) { - ext::lock(Extension {}, kiosk, item, policy) + ext::lock(kiosk, item, policy) } else { - ext::place(Extension {}, kiosk, item, policy) + ext::place(kiosk, item, policy) }; } } diff --git a/kiosk-marketplace/tests/collection_bidding_tests.move b/kiosk-marketplace/tests/collection_bidding_tests.move index fcf12c6..650d9d1 100644 --- a/kiosk-marketplace/tests/collection_bidding_tests.move +++ b/kiosk-marketplace/tests/collection_bidding_tests.move @@ -5,6 +5,7 @@ module mkt::collection_bidding_tests { use sui::coin; use sui::kiosk; + use sui::test_utils; use sui::tx_context::TxContext; use sui::kiosk_test_utils::{Self as test, Asset}; use sui::transfer_policy::{ @@ -13,7 +14,7 @@ module mkt::collection_bidding_tests { TransferPolicyCap }; - use mkt::collection_bidding_ext::{Self as bidding}; + use mkt::collection_bidding::{Self as bidding}; /// The Marketplace witness. struct MyMarket has drop {} @@ -23,14 +24,16 @@ module mkt::collection_bidding_tests { let ctx = &mut test::ctx(); let (buyer_kiosk, buyer_cap) = test::get_kiosk(ctx); - // install extension - bidding::add(&mut buyer_kiosk, &buyer_cap, ctx); + mkt::extension::add(&mut buyer_kiosk, &buyer_cap, ctx); // place bids on an Asset: 100 MIST bidding::place_bids( &mut buyer_kiosk, &buyer_cap, - vector[ test::get_sui(100, ctx) ], + vector[ + test::get_sui(100, ctx), + test::get_sui(300, ctx) + ], ctx ); @@ -59,33 +62,50 @@ module mkt::collection_bidding_tests { policy::confirm_request(&asset_policy, asset_request); policy::confirm_request(&mkt_policy, mkt_request); - return_policy(asset_policy, asset_policy_cap, ctx); - return_policy(mkt_policy, mkt_policy_cap, ctx); - assert!(kiosk::has_item(&buyer_kiosk, asset_id), 0); assert!(!kiosk::has_item(&seller_kiosk, asset_id), 1); + assert!(kiosk::profits_amount(&seller_kiosk) == 300, 2); - let asset = kiosk::take(&mut buyer_kiosk, &buyer_cap, asset_id); + // do it all over again + let (asset, asset_id) = test::get_asset(ctx); + kiosk::place(&mut seller_kiosk, &seller_cap, asset); - test::return_assets(vector[ asset ]); - test::return_kiosk(buyer_kiosk, buyer_cap, ctx); - let amount = test::return_kiosk(seller_kiosk, seller_cap, ctx); + // second bid + let (asset_request, mkt_request) = bidding::accept_market_bid( + &mut buyer_kiosk, + &mut seller_kiosk, + &seller_cap, + &asset_policy, + asset_id, + false, + ctx + ); + + policy::confirm_request(&asset_policy, asset_request); + policy::confirm_request(&mkt_policy, mkt_request); + + assert!(kiosk::has_item(&buyer_kiosk, asset_id), 3); + assert!(!kiosk::has_item(&seller_kiosk, asset_id), 4); + assert!(kiosk::profits_amount(&seller_kiosk) == 400, 5); + + test_utils::destroy(seller_kiosk); + test_utils::destroy(buyer_kiosk); + test_utils::destroy(seller_cap); + test_utils::destroy(buyer_cap); - assert!(amount == 100, 2); + return_policy(asset_policy, asset_policy_cap, ctx); + return_policy(mkt_policy, mkt_policy_cap, ctx); } - fun get_policy( - ctx: &mut TxContext - ): (TransferPolicy, TransferPolicyCap) { + fun get_policy(ctx: &mut TxContext): (TransferPolicy, TransferPolicyCap) { policy::new_for_testing(ctx) } fun return_policy( - policy: TransferPolicy, - policy_cap: TransferPolicyCap, - ctx: &mut TxContext + policy: TransferPolicy, policy_cap: TransferPolicyCap, ctx: &mut TxContext ): u64 { - let proceeds = policy::destroy_and_withdraw(policy, policy_cap, ctx); - coin::burn_for_testing(proceeds) + coin::burn_for_testing( + policy::destroy_and_withdraw(policy, policy_cap, ctx) + ) } } diff --git a/kiosk-marketplace/tests/trading_ext_tests.move b/kiosk-marketplace/tests/trading_ext_tests.move index db9c53c..32d1cb5 100644 --- a/kiosk-marketplace/tests/trading_ext_tests.move +++ b/kiosk-marketplace/tests/trading_ext_tests.move @@ -3,7 +3,7 @@ #[test_only] /// Tests for the marketplace `marketplace_trading_ext`. -module mkt::trading_ext_tests { +module mkt::fixed_trading_tests { use sui::coin; use sui::object::ID; use sui::kiosk_extension; @@ -11,7 +11,7 @@ module mkt::trading_ext_tests { use sui::transfer_policy as policy; use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; use sui::kiosk_test_utils::{Self as test, Asset}; - use mkt::trading_ext as ext; + use mkt::fixed_trading as ext; const PRICE: u64 = 100_000; @@ -69,13 +69,13 @@ module mkt::trading_ext_tests { let (asset, asset_id) = test::get_asset(ctx); kiosk::place(&mut kiosk, &kiosk_cap, asset); - ext::add(&mut kiosk, &kiosk_cap, ctx); + mkt::extension::add(&mut kiosk, &kiosk_cap, ctx); (kiosk, kiosk_cap, asset_id) } /// Wrap everything up; remove the extension and the asset. fun wrapup(kiosk: Kiosk, cap: KioskOwnerCap, ctx: &mut TxContext) { - kiosk_extension::remove(&mut kiosk, &cap); + kiosk_extension::remove(&mut kiosk, &cap); test::return_kiosk(kiosk, cap, ctx); } } From 15295167cbf6f63202ea3ae9a85d0d305e7cda0b Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Mon, 26 Feb 2024 19:10:41 +0300 Subject: [PATCH 05/12] minor comment --- kiosk-marketplace/sources/fixed_trading.move | 1 + 1 file changed, 1 insertion(+) diff --git a/kiosk-marketplace/sources/fixed_trading.move b/kiosk-marketplace/sources/fixed_trading.move index f623751..7deb998 100644 --- a/kiosk-marketplace/sources/fixed_trading.move +++ b/kiosk-marketplace/sources/fixed_trading.move @@ -51,6 +51,7 @@ module mkt::fixed_trading { struct ItemPurchased has copy, drop { kiosk_id: ID, item_id: ID, + /// The seller address if the Kiosk is personal. seller: Option
, } From bf7b7fc4e74d2b79fc6b209f8d20eb83dadfa997 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Wed, 28 Feb 2024 14:39:52 +0300 Subject: [PATCH 06/12] rename app to marketplace --- kiosk-marketplace/Move.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kiosk-marketplace/Move.toml b/kiosk-marketplace/Move.toml index 47b0af3..a98d967 100644 --- a/kiosk-marketplace/Move.toml +++ b/kiosk-marketplace/Move.toml @@ -1,5 +1,5 @@ [package] -name = "kiosk-mkt-adapter" +name = "kiosk-marketplace" # edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha # license = "" # e.g., "MIT", "GPL", "Apache 2.0" From 63d2211f4162cf511fa1ec5e2f590f0a84b6986c Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Wed, 28 Feb 2024 14:47:55 +0300 Subject: [PATCH 07/12] minor code optimization --- kiosk-marketplace/Move.lock | 2 +- .../sources/collection_bidding.move | 18 +------------ kiosk-marketplace/sources/extension.move | 26 ++++++++++++++++--- kiosk-marketplace/sources/single_bid.move | 20 ++------------ 4 files changed, 26 insertions(+), 40 deletions(-) diff --git a/kiosk-marketplace/Move.lock b/kiosk-marketplace/Move.lock index be80855..97847d3 100644 --- a/kiosk-marketplace/Move.lock +++ b/kiosk-marketplace/Move.lock @@ -2,7 +2,7 @@ [move] version = 0 -manifest_digest = "7760EF9C6E870653F409B1B878A4C1BB2934E076A6ABFB75790B5EDFA4D04F62" +manifest_digest = "BC5D4C89EC5A13FDFB57BFED7C1EAD242E385EEB00DFA4B11B9294D023CA8493" deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" dependencies = [ diff --git a/kiosk-marketplace/sources/collection_bidding.move b/kiosk-marketplace/sources/collection_bidding.move index e0dffdf..c23b2d5 100644 --- a/kiosk-marketplace/sources/collection_bidding.move +++ b/kiosk-marketplace/sources/collection_bidding.move @@ -17,19 +17,16 @@ module mkt::collection_bidding { use sui::tx_context::TxContext; use sui::coin::{Self, Coin}; use sui::transfer_policy::{ - Self as policy, TransferPolicy, TransferRequest, }; use sui::sui::SUI; - use sui::vec_set; use sui::object::{Self, ID}; use sui::event; use sui::pay; use sui::bag; use kiosk::personal_kiosk; - use kiosk::kiosk_lock_rule::Rule as LockRule; use mkt::adapter::{Self as mkt, NoMarket}; use mkt::extension as ext; @@ -194,7 +191,7 @@ module mkt::collection_bidding { }); // Place or lock the item in the `source` Kiosk. - place_or_lock(buyer, item, policy); + ext::place_or_lock(buyer, item, policy); (request, market_request) } @@ -213,17 +210,4 @@ module mkt::collection_bidding { let coins = bag::borrow(ext::storage(kiosk), Bid {}); coin::value(vector::borrow>(coins, 0)) } - - // === Internal === - - /// A helper function which either places or locks an item in the Kiosk depending - /// on the Rules set in the `TransferPolicy`. - fun place_or_lock(kiosk: &mut Kiosk, item: T, policy: &TransferPolicy) { - let should_lock = vec_set::contains(policy::rules(policy), &type_name::get()); - if (should_lock) { - ext::lock(kiosk, item, policy) - } else { - ext::place(kiosk, item, policy) - }; - } } diff --git a/kiosk-marketplace/sources/extension.move b/kiosk-marketplace/sources/extension.move index 563325f..1e8e235 100644 --- a/kiosk-marketplace/sources/extension.move +++ b/kiosk-marketplace/sources/extension.move @@ -4,11 +4,15 @@ /// A single extension module for the package. Eliminates the need for separate /// extension installation and management in every package. module mkt::extension { - use sui::transfer_policy::TransferPolicy; + use std::type_name; + use sui::transfer_policy::{Self as policy, TransferPolicy}; use sui::kiosk::{Kiosk, KioskOwnerCap}; use sui::kiosk_extension as ext; use sui::tx_context::TxContext; use sui::bag::Bag; + use sui::vec_set; + + use kiosk::kiosk_lock_rule::Rule as LockRule; friend mkt::collection_bidding; friend mkt::fixed_trading; @@ -21,9 +25,7 @@ module mkt::extension { const PERMISSIONS: u128 = 3; /// Install the Marketplace Extension into the Kiosk. - public fun add( - kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext - ) { + public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { ext::add(Extension {}, kiosk, cap, PERMISSIONS, ctx) } @@ -62,4 +64,20 @@ module mkt::extension { public(friend) fun storage_mut(kiosk: &mut Kiosk): &mut Bag { ext::storage_mut(Extension {}, kiosk) } + + /// Place or Lock the item into the Kiosk, based on the policy. + public(friend) fun place_or_lock( + kiosk: &mut Kiosk, item: T, policy: &TransferPolicy + ) { + let should_lock = vec_set::contains( + policy::rules(policy), + &type_name::get() + ); + + if (should_lock) { + lock(kiosk, item, policy) + } else { + place(kiosk, item, policy) + }; + } } diff --git a/kiosk-marketplace/sources/single_bid.move b/kiosk-marketplace/sources/single_bid.move index 31ae868..18a1652 100644 --- a/kiosk-marketplace/sources/single_bid.move +++ b/kiosk-marketplace/sources/single_bid.move @@ -10,19 +10,16 @@ /// 2. A seller accepts the bid and sells the item to the buyer in a single action. /// 3. The seller resolves the requests for `Market` and creator. module mkt::single_bid { - use std::type_name; use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; - use sui::transfer_policy::{Self as policy, TransferPolicy, TransferRequest}; + use sui::transfer_policy::{TransferPolicy, TransferRequest}; use sui::tx_context::TxContext; use sui::object::{Self, ID}; use sui::coin::{Self, Coin}; use sui::sui::SUI; - use sui::vec_set; use sui::event; use sui::bag; use kiosk::personal_kiosk; - use kiosk::kiosk_lock_rule::Rule as LockRule; use mkt::extension as ext; use mkt::adapter as mkt; @@ -128,7 +125,7 @@ module mkt::single_bid { item_id, }); - place_or_lock(buyer, item, policy); + ext::place_or_lock(buyer, item, policy); (req, mkt_req) } @@ -157,17 +154,4 @@ module mkt::single_bid { coin } - - // === Internal === - - /// A helper function which either places or locks an item in the Kiosk depending - /// on the Rules set in the `TransferPolicy`. - fun place_or_lock(kiosk: &mut Kiosk, item: T, policy: &TransferPolicy) { - let should_lock = vec_set::contains(policy::rules(policy), &type_name::get()); - if (should_lock) { - ext::lock(kiosk, item, policy) - } else { - ext::place(kiosk, item, policy) - }; - } } From c78c1a1880dbcca23cc9648155172d4c31aaf33b Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Wed, 6 Mar 2024 21:42:25 +0300 Subject: [PATCH 08/12] swaps arguments in events --- .../sources/collection_bidding.move | 8 +-- kiosk-marketplace/sources/fixed_trading.move | 6 +-- kiosk-marketplace/sources/single_bid.move | 8 +-- kiosk/Move.lock | 4 +- kiosk/Move.toml | 5 +- kiosk/sources/rules/floor_price_rule.move | 2 +- kiosk/sources/rules/royalty_rule.move | 6 +-- kiosk/sources/rules/witness_rule.move | 4 +- kiosk/sources/utils/compliance.move | 50 +++++++++++++++++++ 9 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 kiosk/sources/utils/compliance.move diff --git a/kiosk-marketplace/sources/collection_bidding.move b/kiosk-marketplace/sources/collection_bidding.move index c23b2d5..47ba2d6 100644 --- a/kiosk-marketplace/sources/collection_bidding.move +++ b/kiosk-marketplace/sources/collection_bidding.move @@ -44,19 +44,19 @@ module mkt::collection_bidding { const EExtensionNotInstalled: u64 = 5; /// A key for Extension storage - a single bid on an item of type `T` on a `Market`. - struct Bid has copy, store, drop {} + struct Bid has copy, store, drop {} // === Events === /// An event that is emitted when a new bid is placed. - struct NewBid has copy, drop { + struct NewBid has copy, drop { kiosk_id: ID, bids: vector, is_personal: bool, } /// An event that is emitted when a bid is accepted. - struct BidAccepted has copy, drop { + struct BidAccepted has copy, drop { seller_kiosk_id: ID, buyer_kiosk_id: ID, item_id: ID, @@ -66,7 +66,7 @@ module mkt::collection_bidding { } /// An event that is emitted when a bid is canceled. - struct BidCanceled has copy, drop { + struct BidCanceled has copy, drop { kiosk_id: ID, kiosk_owner: Option
, } diff --git a/kiosk-marketplace/sources/fixed_trading.move b/kiosk-marketplace/sources/fixed_trading.move index 7deb998..ba80fc4 100644 --- a/kiosk-marketplace/sources/fixed_trading.move +++ b/kiosk-marketplace/sources/fixed_trading.move @@ -33,7 +33,7 @@ module mkt::fixed_trading { // === Events === /// An item has been listed on a Marketplace. - struct ItemListed has copy, drop { + struct ItemListed has copy, drop { kiosk_id: ID, item_id: ID, price: u64, @@ -41,14 +41,14 @@ module mkt::fixed_trading { } /// An item has been delisted from a Marketplace. - struct ItemDelisted has copy, drop { + struct ItemDelisted has copy, drop { kiosk_id: ID, item_id: ID, is_personal: bool, } /// An item has been purchased from a Marketplace. - struct ItemPurchased has copy, drop { + struct ItemPurchased has copy, drop { kiosk_id: ID, item_id: ID, /// The seller address if the Kiosk is personal. diff --git a/kiosk-marketplace/sources/single_bid.move b/kiosk-marketplace/sources/single_bid.move index 18a1652..ed15a8e 100644 --- a/kiosk-marketplace/sources/single_bid.move +++ b/kiosk-marketplace/sources/single_bid.move @@ -35,12 +35,12 @@ module mkt::single_bid { const ENoBid: u64 = 4; /// The dynamic field key for the Bid. - struct Bid has copy, store, drop { item_id: ID } + struct Bid has copy, store, drop { item_id: ID } // === Events === /// Event emitted when a bid is placed. - struct NewBid has copy, drop { + struct NewBid has copy, drop { kiosk_id: ID, item_id: ID, bid: u64, @@ -48,7 +48,7 @@ module mkt::single_bid { } /// Event emitted when a bid is accepted. - struct BidAccepted has copy, drop { + struct BidAccepted has copy, drop { kiosk_id: ID, item_id: ID, bid: u64, @@ -57,7 +57,7 @@ module mkt::single_bid { } /// Event emitted when a bid is cancelled. - struct BidCancelled has copy, drop { + struct BidCancelled has copy, drop { kiosk_id: ID, item_id: ID, bid: u64, diff --git a/kiosk/Move.lock b/kiosk/Move.lock index 83be0be..e958ff4 100644 --- a/kiosk/Move.lock +++ b/kiosk/Move.lock @@ -2,8 +2,8 @@ [move] version = 0 -manifest_digest = "A62499C4014A0043E14032FCB71E9E52900167A18DDF760AC2C025FD3F369799" -deps_digest = "112928C94A84031C09CD9B9D1D44B149B73FC0EEA5FA8D8B2D7CA4D91936335A" +manifest_digest = "B2C32FD994631CF15A3F152AB4239BC2BE09438EDBE6DDD90B754AA1AB73DA66" +deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" dependencies = [ { name = "Sui" }, diff --git a/kiosk/Move.toml b/kiosk/Move.toml index 6412209..5568972 100644 --- a/kiosk/Move.toml +++ b/kiosk/Move.toml @@ -1,10 +1,11 @@ [package] name = "Kiosk" version = "0.0.1" -published-at = "0x34cc6762780f4f6f153c924c0680cfe2a1fb4601e7d33cc28a92297b62de1e0e" +# published-at = "0x34cc6762780f4f6f153c924c0680cfe2a1fb4601e7d33cc28a92297b62de1e0e" [dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } [addresses] -kiosk = "0x434b5bd8f6a7b05fede0ff46c6e511d71ea326ed38056e3bcd681d2d7c2a7879" +# kiosk = "0x434b5bd8f6a7b05fede0ff46c6e511d71ea326ed38056e3bcd681d2d7c2a7879" +kiosk = "0x0" diff --git a/kiosk/sources/rules/floor_price_rule.move b/kiosk/sources/rules/floor_price_rule.move index 7ce376a..2baf317 100644 --- a/kiosk/sources/rules/floor_price_rule.move +++ b/kiosk/sources/rules/floor_price_rule.move @@ -44,7 +44,7 @@ module kiosk::floor_price_rule { /// Buyer action: Prove that the amount is higher or equal to the floor_price. public fun prove( - policy: &mut TransferPolicy, + policy: &TransferPolicy, request: &mut TransferRequest ) { let config: &Config = policy::get_rule(Rule {}, policy); diff --git a/kiosk/sources/rules/royalty_rule.move b/kiosk/sources/rules/royalty_rule.move index 39c54cd..737b5e3 100644 --- a/kiosk/sources/rules/royalty_rule.move +++ b/kiosk/sources/rules/royalty_rule.move @@ -63,7 +63,7 @@ module kiosk::royalty_rule { /// Creator action: Add the Royalty Rule for the `T`. /// Pass in the `TransferPolicy`, `TransferPolicyCap` and the configuration /// for the policy: `amount_bp` and `min_amount`. - public fun add( + public fun add( policy: &mut TransferPolicy, cap: &TransferPolicyCap, amount_bp: u16, @@ -74,7 +74,7 @@ module kiosk::royalty_rule { } /// Buyer action: Pay the royalty fee for the transfer. - public fun pay( + public fun pay( policy: &mut TransferPolicy, request: &mut TransferRequest, payment: Coin @@ -90,7 +90,7 @@ module kiosk::royalty_rule { /// Helper function to calculate the amount to be paid for the transfer. /// Can be used dry-runned to estimate the fee amount based on the Kiosk listing price. - public fun fee_amount(policy: &TransferPolicy, paid: u64): u64 { + public fun fee_amount(policy: &TransferPolicy, paid: u64): u64 { let config: &Config = policy::get_rule(Rule {}, policy); let amount = (((paid as u128) * (config.amount_bp as u128) / 10_000) as u64); diff --git a/kiosk/sources/rules/witness_rule.move b/kiosk/sources/rules/witness_rule.move index e5e9168..119e543 100644 --- a/kiosk/sources/rules/witness_rule.move +++ b/kiosk/sources/rules/witness_rule.move @@ -31,7 +31,7 @@ module kiosk::witness_rule { /// Creator action: adds the Rule. /// Requires a "Proof" witness confirmation on every transfer. - public fun add( + public fun add( policy: &mut TransferPolicy, cap: &TransferPolicyCap ) { @@ -40,7 +40,7 @@ module kiosk::witness_rule { /// Buyer action: follow the policy. /// Present the required "Proof" instance to get a receipt. - public fun prove( + public fun prove( _proof: Proof, policy: &TransferPolicy, request: &mut TransferRequest diff --git a/kiosk/sources/utils/compliance.move b/kiosk/sources/utils/compliance.move new file mode 100644 index 0000000..eee46d6 --- /dev/null +++ b/kiosk/sources/utils/compliance.move @@ -0,0 +1,50 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module provides a single function to comply with the given policy and +/// complete all the required Rules in a single call. +module kiosk::ruleset { + use sui::transfer_policy::{Self as policy, TransferRequest, TransferPolicy}; + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::tx_context::TxContext; + use sui::coin::{Self, Coin}; + use sui::sui::SUI; + + use kiosk::kiosk_lock_rule::{Self, Rule as LockRule}; + use kiosk::floor_price_rule::{Self, Rule as FloorPriceRule}; + use kiosk::personal_kiosk_rule::{Self, Rule as PersonalKioskRule}; + use kiosk::royalty_rule::{Self, Rule as RoyaltyRule}; + + public fun complete( + policy: &mut TransferPolicy, + request: TransferRequest, + kiosk: &mut Kiosk, + kiosk_cap: &KioskOwnerCap, + item: T, + payment: &mut Coin, + ctx: &mut TxContext, + ) { + if (policy::has_rule(policy)) { + personal_kiosk_rule::prove(kiosk, &mut request); + }; + + if (policy::has_rule(policy)) { + let amount = royalty_rule::fee_amount(policy, policy::paid(&request)); + let royalty = coin::split(payment, amount, ctx); + royalty_rule::pay(policy, &mut request, royalty); + }; + + if (policy::has_rule(policy)) { + floor_price_rule::prove(policy, &mut request); + }; + + if (policy::has_rule(policy)) { + kiosk::lock(kiosk, kiosk_cap, policy, item); + kiosk_lock_rule::prove(&mut request, kiosk); + } else { + kiosk::place(kiosk, kiosk_cap, item); + }; + + policy::confirm_request(policy, request); + } +} From 3049ef93fad851847e738f91ed428db7cb8408a7 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Wed, 6 Mar 2024 22:31:45 +0300 Subject: [PATCH 09/12] adds min_price protection for the seller --- kiosk-marketplace/sources/collection_bidding.move | 6 ++++++ kiosk-marketplace/tests/collection_bidding_tests.move | 1 + 2 files changed, 7 insertions(+) diff --git a/kiosk-marketplace/sources/collection_bidding.move b/kiosk-marketplace/sources/collection_bidding.move index 47ba2d6..7dcdce0 100644 --- a/kiosk-marketplace/sources/collection_bidding.move +++ b/kiosk-marketplace/sources/collection_bidding.move @@ -42,6 +42,8 @@ module mkt::collection_bidding { const ENoCoinsPassed: u64 = 4; /// Trying to access the extension without installing it. const EExtensionNotInstalled: u64 = 5; + /// Trying to accept a bid that doesn't match the seller's expectation. + const EBidDoesntMatchExpected: u64 = 6; /// A key for Extension storage - a single bid on an item of type `T` on a `Market`. struct Bid has copy, store, drop {} @@ -143,6 +145,8 @@ module mkt::collection_bidding { seller_cap: &KioskOwnerCap, policy: &TransferPolicy, item_id: ID, + // for race conditions protection + min_bid_amount: u64, // keeping these arguments for extendability _lock: bool, ctx: &mut TxContext @@ -157,6 +161,8 @@ module mkt::collection_bidding { // make sure of it). let bid = vector::pop_back(bag::borrow_mut(storage, Bid {})); + assert!(coin::value(&bid) >= min_bid_amount, EBidDoesntMatchExpected); + // If there are no bids left, remove the bag and the key from the storage. if (bid_count(buyer) == 0) { vector::destroy_empty>( diff --git a/kiosk-marketplace/tests/collection_bidding_tests.move b/kiosk-marketplace/tests/collection_bidding_tests.move index 650d9d1..4ef1c41 100644 --- a/kiosk-marketplace/tests/collection_bidding_tests.move +++ b/kiosk-marketplace/tests/collection_bidding_tests.move @@ -55,6 +55,7 @@ module mkt::collection_bidding_tests { &seller_cap, &asset_policy, asset_id, + 300, false, ctx ); From e2a999e41cc9ca755c305a06873a120d1cff56e0 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Tue, 12 Mar 2024 01:24:57 +0300 Subject: [PATCH 10/12] edition = 2024 --- kiosk-marketplace/Move.lock | 8 ++-- kiosk-marketplace/Move.toml | 3 +- kiosk-marketplace/sources/adapter.move | 28 ++++++------ .../sources/collection_bidding.move | 43 +++++++++---------- kiosk-marketplace/sources/extension.move | 2 +- kiosk-marketplace/sources/fixed_trading.move | 24 ++++++----- kiosk-marketplace/sources/single_bid.move | 42 ++++++++---------- kiosk-marketplace/tests/adapter_tests.move | 18 ++++---- .../tests/collection_bidding_tests.move | 32 +++++++------- .../tests/trading_ext_tests.move | 25 ++++++----- 10 files changed, 111 insertions(+), 114 deletions(-) diff --git a/kiosk-marketplace/Move.lock b/kiosk-marketplace/Move.lock index 97847d3..a1d11ef 100644 --- a/kiosk-marketplace/Move.lock +++ b/kiosk-marketplace/Move.lock @@ -2,7 +2,7 @@ [move] version = 0 -manifest_digest = "BC5D4C89EC5A13FDFB57BFED7C1EAD242E385EEB00DFA4B11B9294D023CA8493" +manifest_digest = "0B26186432FECAA89587FCB88D7D71F62A085F4EE2FE186CBB6DEE4752AF791B" deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" dependencies = [ @@ -20,17 +20,17 @@ dependencies = [ [[move.package]] name = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/move-stdlib" } [[move.package]] name = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } +source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/testnet", subdir = "crates/sui-framework/packages/sui-framework" } dependencies = [ { name = "MoveStdlib" }, ] [move.toolchain-version] -compiler-version = "1.19.0" +compiler-version = "1.21.0" edition = "legacy" flavor = "sui" diff --git a/kiosk-marketplace/Move.toml b/kiosk-marketplace/Move.toml index a98d967..bfb5cb6 100644 --- a/kiosk-marketplace/Move.toml +++ b/kiosk-marketplace/Move.toml @@ -1,12 +1,13 @@ [package] name = "kiosk-marketplace" +edition = "2024.alpha" # edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha # license = "" # e.g., "MIT", "GPL", "Apache 2.0" # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } +Sui = { override = true, git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } Kiosk = { local = "../kiosk" } # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. diff --git a/kiosk-marketplace/sources/adapter.move b/kiosk-marketplace/sources/adapter.move index 344ad12..5f7d5a4 100644 --- a/kiosk-marketplace/sources/adapter.move +++ b/kiosk-marketplace/sources/adapter.move @@ -18,7 +18,7 @@ /// Adapter. module mkt::adapter { use sui::transfer_policy::{Self as policy, TransferRequest}; - use sui::kiosk::{Self, Kiosk, KioskOwnerCap, PurchaseCap}; + use sui::kiosk::{Kiosk, KioskOwnerCap, PurchaseCap}; use sui::tx_context::TxContext; use sui::object::ID; use sui::coin::Coin; @@ -31,11 +31,11 @@ module mkt::adapter { /// The `NoMarket` type is used to provide a default `Market` type parameter /// for a scenario when the `MarketplaceAdapter` is not used and extensions /// maintain uniformity of emitted events. NoMarket = no marketplace. - struct NoMarket {} + public struct NoMarket {} /// The `MarketPurchaseCap` wraps the `PurchaseCap` and forces the unlocking /// party to satisfy the `TransferPolicy` requirements. - struct MarketPurchaseCap has store { + public struct MarketPurchaseCap has store { purchase_cap: PurchaseCap } @@ -47,11 +47,11 @@ module mkt::adapter { min_price: u64, ctx: &mut TxContext ): MarketPurchaseCap { - MarketPurchaseCap { - purchase_cap: kiosk::list_with_purchase_cap( - kiosk, cap, item_id, min_price, ctx - ) - } + let purchase_cap = kiosk.list_with_purchase_cap( + cap, item_id, min_price, ctx + ); + + MarketPurchaseCap { purchase_cap } } /// Return the `MarketPurchaseCap` to the `Kiosk`. Similar to how the @@ -63,7 +63,7 @@ module mkt::adapter { _ctx: &mut TxContext ) { let MarketPurchaseCap { purchase_cap } = cap; - kiosk::return_purchase_cap(kiosk, purchase_cap); + kiosk.return_purchase_cap(purchase_cap); } /// Use the `MarketPurchaseCap` to purchase an item from the `Kiosk`. Unlike @@ -76,7 +76,7 @@ module mkt::adapter { _ctx: &mut TxContext ): (T, TransferRequest, TransferRequest) { let MarketPurchaseCap { purchase_cap } = cap; - let (item, request) = kiosk::purchase_with_cap(kiosk, purchase_cap, coin); + let (item, request) = kiosk.purchase_with_cap(purchase_cap, coin); let market_request = policy::new_request( policy::item(&request), policy::paid(&request), @@ -95,24 +95,24 @@ module mkt::adapter { _ctx: &mut TxContext ): (T, TransferRequest) { let MarketPurchaseCap { purchase_cap } = cap; - kiosk::purchase_with_cap(kiosk, purchase_cap, coin) + kiosk.purchase_with_cap(purchase_cap, coin) } // === Getters === /// Handy wrapper to read the `kiosk` field of the inner `PurchaseCap` public(friend) fun kiosk(self: &MarketPurchaseCap): ID { - kiosk::purchase_cap_kiosk(&self.purchase_cap) + self.purchase_cap.purchase_cap_kiosk() } /// Handy wrapper to read the `item` field of the inner `PurchaseCap` public(friend) fun item(self: &MarketPurchaseCap): ID { - kiosk::purchase_cap_item(&self.purchase_cap) + self.purchase_cap.purchase_cap_item() } /// Handy wrapper to read the `min_price` field of the inner `PurchaseCap` public(friend) fun min_price(self: &MarketPurchaseCap): u64 { - kiosk::purchase_cap_min_price(&self.purchase_cap) + self.purchase_cap.purchase_cap_min_price() } // === Test === diff --git a/kiosk-marketplace/sources/collection_bidding.move b/kiosk-marketplace/sources/collection_bidding.move index 7dcdce0..58ff836 100644 --- a/kiosk-marketplace/sources/collection_bidding.move +++ b/kiosk-marketplace/sources/collection_bidding.move @@ -46,19 +46,19 @@ module mkt::collection_bidding { const EBidDoesntMatchExpected: u64 = 6; /// A key for Extension storage - a single bid on an item of type `T` on a `Market`. - struct Bid has copy, store, drop {} + public struct Bid has copy, store, drop {} // === Events === /// An event that is emitted when a new bid is placed. - struct NewBid has copy, drop { + public struct NewBid has copy, drop { kiosk_id: ID, bids: vector, is_personal: bool, } /// An event that is emitted when a bid is accepted. - struct BidAccepted has copy, drop { + public struct BidAccepted has copy, drop { seller_kiosk_id: ID, buyer_kiosk_id: ID, item_id: ID, @@ -68,7 +68,7 @@ module mkt::collection_bidding { } /// An event that is emitted when a bid is canceled. - struct BidCanceled has copy, drop { + public struct BidCanceled has copy, drop { kiosk_id: ID, kiosk_owner: Option
, } @@ -86,14 +86,14 @@ module mkt::collection_bidding { bids: vector>, _ctx: &mut TxContext ) { - assert!(vector::length(&bids) > 0, ENoCoinsPassed); + assert!(bids.length() > 0, ENoCoinsPassed); assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); assert!(ext::is_installed(kiosk), EExtensionNotInstalled); - let amounts = vector[]; - let (i, count) = (0, vector::length(&bids)); + let mut amounts = vector[]; + let (mut i, count) = (0, vector::length(&bids)); while (i < count) { - vector::push_back(&mut amounts, coin::value(vector::borrow(&bids, i))); + vector::push_back(&mut amounts, vector::borrow(&bids, i).value()); i = i + 1; }; @@ -118,8 +118,8 @@ module mkt::collection_bidding { kiosk_owner: personal_kiosk::try_owner(kiosk) }); - let coins = bag::remove(ext::storage_mut(kiosk), Bid {}); - let total = coin::zero(ctx); + let coins = ext::storage_mut(kiosk).remove(Bid {}); + let mut total = coin::zero(ctx); pay::join_vec(&mut total, coins); total } @@ -155,25 +155,22 @@ module mkt::collection_bidding { assert!(kiosk::has_access(seller, seller_cap), ENotAuthorized); let storage = ext::storage_mut(buyer); - assert!(bag::contains(storage, Bid {}), EBidNotFound); + assert!(storage.contains(Bid {}), EBidNotFound); // Take 1 Coin from the bag - this is our bid (bids can't be empty, we // make sure of it). - let bid = vector::pop_back(bag::borrow_mut(storage, Bid {})); + let bid: Coin = vector::pop_back(bag::borrow_mut(storage, Bid {})); - assert!(coin::value(&bid) >= min_bid_amount, EBidDoesntMatchExpected); + assert!(bid.value() >= min_bid_amount, EBidDoesntMatchExpected); // If there are no bids left, remove the bag and the key from the storage. if (bid_count(buyer) == 0) { vector::destroy_empty>( - bag::remove( - ext::storage_mut(buyer), - Bid {} - ) + ext::storage_mut(buyer).remove(Bid {}) ); }; - let amount = coin::value(&bid); + let amount = bid.value(); assert!(ext::is_enabled(buyer), EExtensionDisabled); // assert!(mkt::kiosk(&mkt_cap) == object::id(seller), EIncorrectKiosk); @@ -206,14 +203,16 @@ module mkt::collection_bidding { /// Number of bids on an item of type `T` on a `Market` in a `Kiosk`. public fun bid_count(kiosk: &Kiosk): u64 { - let coins = bag::borrow(ext::storage(kiosk), Bid {}); - vector::length>(coins) + let coins: &vector> = ext::storage(kiosk) + .borrow(Bid {}); + + coins.length() } /// Returns the amount of the bid on an item of type `T` on a `Market`. /// The `NoMarket` generic can be used to check an item listed off the market. public fun bid_amount(kiosk: &Kiosk): u64 { - let coins = bag::borrow(ext::storage(kiosk), Bid {}); - coin::value(vector::borrow>(coins, 0)) + let coins: &vector> = ext::storage(kiosk).borrow(Bid {}); + coins.borrow(0).value() } } diff --git a/kiosk-marketplace/sources/extension.move b/kiosk-marketplace/sources/extension.move index 1e8e235..15bd43d 100644 --- a/kiosk-marketplace/sources/extension.move +++ b/kiosk-marketplace/sources/extension.move @@ -19,7 +19,7 @@ module mkt::extension { friend mkt::single_bid; /// The extension Witness. - struct Extension has drop {} + public struct Extension has drop {} /// Place and Lock permissions. const PERMISSIONS: u128 = 3; diff --git a/kiosk-marketplace/sources/fixed_trading.move b/kiosk-marketplace/sources/fixed_trading.move index ba80fc4..814d91b 100644 --- a/kiosk-marketplace/sources/fixed_trading.move +++ b/kiosk-marketplace/sources/fixed_trading.move @@ -33,7 +33,7 @@ module mkt::fixed_trading { // === Events === /// An item has been listed on a Marketplace. - struct ItemListed has copy, drop { + public struct ItemListed has copy, drop { kiosk_id: ID, item_id: ID, price: u64, @@ -41,14 +41,14 @@ module mkt::fixed_trading { } /// An item has been delisted from a Marketplace. - struct ItemDelisted has copy, drop { + public struct ItemDelisted has copy, drop { kiosk_id: ID, item_id: ID, is_personal: bool, } /// An item has been purchased from a Marketplace. - struct ItemPurchased has copy, drop { + public struct ItemPurchased has copy, drop { kiosk_id: ID, item_id: ID, /// The seller address if the Kiosk is personal. @@ -65,10 +65,10 @@ module mkt::fixed_trading { price: u64, ctx: &mut TxContext ) { - assert!(kiosk::has_access(kiosk, cap), ENotOwner); + assert!(kiosk.has_access(cap), ENotOwner); let mkt_cap = mkt::new(kiosk, cap, item_id, price, ctx); - bag::add(ext::storage_mut(kiosk), item_id, mkt_cap); + ext::storage_mut(kiosk).add(item_id, mkt_cap); event::emit(ItemListed { is_personal: personal_kiosk::is_personal(kiosk), @@ -85,10 +85,10 @@ module mkt::fixed_trading { item_id: ID, ctx: &mut TxContext ) { - assert!(kiosk::has_access(kiosk, cap), ENotOwner); - assert!(is_listed(kiosk, item_id), ENotListed); + assert!(kiosk.has_access(cap), ENotOwner); + assert!(kiosk.is_listed(item_id), ENotListed); - let mkt_cap = bag::remove(ext::storage_mut(kiosk), item_id); + let mkt_cap = ext::storage_mut(kiosk).remove(item_id); mkt::return_cap(kiosk, mkt_cap, ctx); event::emit(ItemDelisted { @@ -105,10 +105,10 @@ module mkt::fixed_trading { payment: Coin, ctx: &mut TxContext ): (T, TransferRequest, TransferRequest) { - assert!(is_listed(kiosk, item_id), ENotListed); + assert!(kiosk.is_listed(item_id), ENotListed); - let mkt_cap = bag::remove(ext::storage_mut(kiosk), item_id); - assert!(coin::value(&payment) == mkt::min_price(&mkt_cap), EIncorrectAmount); + let mkt_cap = ext::storage_mut(kiosk).remove>(item_id); + assert!(payment.value() == mkt_cap.min_price(), EIncorrectAmount); event::emit(ItemPurchased { seller: personal_kiosk::try_owner(kiosk), @@ -121,6 +121,8 @@ module mkt::fixed_trading { // === Getters === + use fun is_listed as Kiosk.is_listed; + /// Check if an item is currently listed on a specified Marketplace. public fun is_listed(kiosk: &Kiosk, item_id: ID): bool { bag::contains_with_type>( diff --git a/kiosk-marketplace/sources/single_bid.move b/kiosk-marketplace/sources/single_bid.move index ed15a8e..e92e23b 100644 --- a/kiosk-marketplace/sources/single_bid.move +++ b/kiosk-marketplace/sources/single_bid.move @@ -35,12 +35,12 @@ module mkt::single_bid { const ENoBid: u64 = 4; /// The dynamic field key for the Bid. - struct Bid has copy, store, drop { item_id: ID } + public struct Bid has copy, store, drop { item_id: ID } // === Events === /// Event emitted when a bid is placed. - struct NewBid has copy, drop { + public struct NewBid has copy, drop { kiosk_id: ID, item_id: ID, bid: u64, @@ -48,7 +48,7 @@ module mkt::single_bid { } /// Event emitted when a bid is accepted. - struct BidAccepted has copy, drop { + public struct BidAccepted has copy, drop { kiosk_id: ID, item_id: ID, bid: u64, @@ -57,7 +57,7 @@ module mkt::single_bid { } /// Event emitted when a bid is cancelled. - struct BidCancelled has copy, drop { + public struct BidCancelled has copy, drop { kiosk_id: ID, item_id: ID, bid: u64, @@ -74,18 +74,18 @@ module mkt::single_bid { item_id: ID, _ctx: &mut TxContext ) { - assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); + assert!(kiosk.has_access(cap), ENotAuthorized); assert!(ext::is_installed(kiosk), EExtensionNotInstalled); event::emit(NewBid { kiosk_id: object::id(kiosk), - bid: coin::value(&bid), + bid: bid.value(), is_personal: personal_kiosk::is_personal(kiosk), item_id, }); - bag::add( - ext::storage_mut(kiosk), + + ext::storage_mut(kiosk).add( Bid { item_id }, bid ) @@ -104,16 +104,14 @@ module mkt::single_bid { ctx: &mut TxContext ): (TransferRequest, TransferRequest) { assert!(ext::is_enabled(buyer), EExtensionNotInstalled); - assert!(bag::contains(ext::storage(buyer), Bid { item_id }), ENoBid); - assert!(kiosk::has_item(seller, item_id), EItemNotFound); - assert!(!kiosk::is_listed(seller, item_id), EAlreadyListed); + assert!(ext::storage(buyer).contains(Bid { item_id }), ENoBid); + assert!(seller.has_item(item_id), EItemNotFound); + assert!(!seller.is_listed(item_id), EAlreadyListed); - let coin: Coin = bag::remove( - ext::storage_mut(buyer), - Bid { item_id }, - ); + let coin: Coin = ext::storage_mut(buyer) + .remove(Bid { item_id }); - let amount = coin::value(&coin); + let amount = coin.value(); let mkt_cap = mkt::new(seller, seller_cap, item_id, amount, ctx); let (item, req, mkt_req) = mkt::purchase(seller, mkt_cap, coin, ctx); @@ -136,18 +134,16 @@ module mkt::single_bid { item_id: ID, _ctx: &mut TxContext ): Coin { - assert!(kiosk::has_access(kiosk, kiosk_cap), ENotAuthorized); + assert!(kiosk.has_access(kiosk_cap), ENotAuthorized); assert!(ext::is_installed(kiosk), EExtensionNotInstalled); - assert!(bag::contains(ext::storage(kiosk), Bid { item_id }), ENoBid); + assert!(ext::storage(kiosk).contains(Bid { item_id }), ENoBid); - let coin: Coin = bag::remove( - ext::storage_mut(kiosk), - Bid { item_id }, - ); + let coin: Coin = ext::storage_mut(kiosk) + .remove(Bid { item_id }); event::emit(BidCancelled { kiosk_id: object::id(kiosk), - bid: coin::value(&coin), + bid: coin.value(), is_personal: personal_kiosk::is_personal(kiosk), item_id, }); diff --git a/kiosk-marketplace/tests/adapter_tests.move b/kiosk-marketplace/tests/adapter_tests.move index cf19ec8..bcbf70c 100644 --- a/kiosk-marketplace/tests/adapter_tests.move +++ b/kiosk-marketplace/tests/adapter_tests.move @@ -14,17 +14,17 @@ module mkt::adapter_tests { use mkt::adapter as mkt; /// The Marketplace witness. - struct MyMarket has drop {} + public struct MyMarket has drop {} /// The witness to use in tests. - struct OTW has drop {} + public struct OTW has drop {} // Performs a test of the `new` and `return_cap` functions. Not supposed to // abort, and there's only so many scenarios where it can fail due to strict // type requirements. #[test] fun test_new_return_flow() { let ctx = &mut test::ctx(); - let (kiosk, kiosk_cap) = test::get_kiosk(ctx); + let (mut kiosk, kiosk_cap) = test::get_kiosk(ctx); let (asset, asset_id) = test::get_asset(ctx); kiosk::place(&mut kiosk, &kiosk_cap, asset); @@ -49,10 +49,10 @@ module mkt::adapter_tests { // fail scenarios is limited and already covered by the base Kiosk #[test] fun test_new_purchase_flow() { let ctx = &mut test::ctx(); - let (kiosk, kiosk_cap) = test::get_kiosk(ctx); + let (mut kiosk, kiosk_cap) = test::get_kiosk(ctx); let (asset, asset_id) = test::get_asset(ctx); - kiosk::place(&mut kiosk, &kiosk_cap, asset); + kiosk.place(&kiosk_cap, asset); // Lock an item in the Marketplace let mkt_cap = mkt::new( @@ -67,14 +67,14 @@ module mkt::adapter_tests { // Get Policy for the Asset, use it and clean up. let (policy, policy_cap) = test::get_policy(ctx); - policy::confirm_request(&policy, req); + policy.confirm_request(req); test::return_policy(policy, policy_cap, ctx); // Get Policy for the Marketplace, use it and clean up. let (policy, policy_cap) = policy::new_for_testing(ctx); - policy::confirm_request(&policy, mkt_req); - let proceeds = policy::destroy_and_withdraw(policy, policy_cap, ctx); - coin::destroy_zero(proceeds); + policy.confirm_request(mkt_req); + policy.destroy_and_withdraw(policy_cap, ctx) + .destroy_zero(); // Now deal with the item and with the Kiosk. test::return_assets(vector[ item ]); diff --git a/kiosk-marketplace/tests/collection_bidding_tests.move b/kiosk-marketplace/tests/collection_bidding_tests.move index 4ef1c41..4d9e153 100644 --- a/kiosk-marketplace/tests/collection_bidding_tests.move +++ b/kiosk-marketplace/tests/collection_bidding_tests.move @@ -4,7 +4,6 @@ #[test_only] module mkt::collection_bidding_tests { use sui::coin; - use sui::kiosk; use sui::test_utils; use sui::tx_context::TxContext; use sui::kiosk_test_utils::{Self as test, Asset}; @@ -17,12 +16,12 @@ module mkt::collection_bidding_tests { use mkt::collection_bidding::{Self as bidding}; /// The Marketplace witness. - struct MyMarket has drop {} + public struct MyMarket has drop {} #[test] fun test_simple_bid() { let ctx = &mut test::ctx(); - let (buyer_kiosk, buyer_cap) = test::get_kiosk(ctx); + let (mut buyer_kiosk, buyer_cap) = test::get_kiosk(ctx); mkt::extension::add(&mut buyer_kiosk, &buyer_cap, ctx); @@ -38,12 +37,12 @@ module mkt::collection_bidding_tests { ); // prepare the seller Kiosk - let (seller_kiosk, seller_cap) = test::get_kiosk(ctx); + let (mut seller_kiosk, seller_cap) = test::get_kiosk(ctx); let (asset, asset_id) = test::get_asset(ctx); // place the asset and create a MarketPurchaseCap // bidding::add(&mut seller_kiosk, &seller_cap, ctx); - kiosk::place(&mut seller_kiosk, &seller_cap, asset); + seller_kiosk.place(&seller_cap, asset); let (asset_policy, asset_policy_cap) = get_policy(ctx); let (mkt_policy, mkt_policy_cap) = get_policy(ctx); @@ -60,16 +59,16 @@ module mkt::collection_bidding_tests { ctx ); - policy::confirm_request(&asset_policy, asset_request); - policy::confirm_request(&mkt_policy, mkt_request); + asset_policy.confirm_request(asset_request); + mkt_policy.confirm_request(mkt_request); - assert!(kiosk::has_item(&buyer_kiosk, asset_id), 0); - assert!(!kiosk::has_item(&seller_kiosk, asset_id), 1); - assert!(kiosk::profits_amount(&seller_kiosk) == 300, 2); + assert!(buyer_kiosk.has_item(asset_id), 0); + assert!(!seller_kiosk.has_item(asset_id), 1); + assert!(seller_kiosk.profits_amount() == 300, 2); // do it all over again let (asset, asset_id) = test::get_asset(ctx); - kiosk::place(&mut seller_kiosk, &seller_cap, asset); + seller_kiosk.place(&seller_cap, asset); // second bid let (asset_request, mkt_request) = bidding::accept_market_bid( @@ -78,16 +77,17 @@ module mkt::collection_bidding_tests { &seller_cap, &asset_policy, asset_id, + 400, false, ctx ); - policy::confirm_request(&asset_policy, asset_request); - policy::confirm_request(&mkt_policy, mkt_request); + asset_policy.confirm_request(asset_request); + mkt_policy.confirm_request(mkt_request); - assert!(kiosk::has_item(&buyer_kiosk, asset_id), 3); - assert!(!kiosk::has_item(&seller_kiosk, asset_id), 4); - assert!(kiosk::profits_amount(&seller_kiosk) == 400, 5); + assert!(buyer_kiosk.has_item(asset_id), 3); + assert!(!seller_kiosk.has_item(asset_id), 4); + assert!(seller_kiosk.profits_amount() == 400, 5); test_utils::destroy(seller_kiosk); test_utils::destroy(buyer_kiosk); diff --git a/kiosk-marketplace/tests/trading_ext_tests.move b/kiosk-marketplace/tests/trading_ext_tests.move index 32d1cb5..6c67b89 100644 --- a/kiosk-marketplace/tests/trading_ext_tests.move +++ b/kiosk-marketplace/tests/trading_ext_tests.move @@ -4,23 +4,22 @@ #[test_only] /// Tests for the marketplace `marketplace_trading_ext`. module mkt::fixed_trading_tests { - use sui::coin; use sui::object::ID; use sui::kiosk_extension; use sui::tx_context::TxContext; use sui::transfer_policy as policy; - use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::kiosk::{Kiosk, KioskOwnerCap}; use sui::kiosk_test_utils::{Self as test, Asset}; use mkt::fixed_trading as ext; const PRICE: u64 = 100_000; /// Marketplace type. - struct MyMarket has drop {} + public struct MyMarket has drop {} #[test] fun test_list_and_delist() { let ctx = &mut test::ctx(); - let (kiosk, kiosk_cap, asset_id) = prepare(ctx); + let (mut kiosk, kiosk_cap, asset_id) = prepare(ctx); ext::list(&mut kiosk, &kiosk_cap, asset_id, PRICE, ctx); @@ -29,14 +28,14 @@ module mkt::fixed_trading_tests { ext::delist(&mut kiosk, &kiosk_cap, asset_id, ctx); - let asset = kiosk::take(&mut kiosk, &kiosk_cap, asset_id); + let asset = kiosk.take(&kiosk_cap, asset_id); test::return_assets(vector[ asset ]); wrapup(kiosk, kiosk_cap, ctx); } #[test] fun test_list_and_purchase() { let ctx = &mut test::ctx(); - let (kiosk, kiosk_cap, asset_id) = prepare(ctx); + let (mut kiosk, kiosk_cap, asset_id) = prepare(ctx); ext::list(&mut kiosk, &kiosk_cap, asset_id, PRICE, ctx); @@ -47,14 +46,14 @@ module mkt::fixed_trading_tests { // Resolve creator's Policy let (policy, policy_cap) = test::get_policy(ctx); - policy::confirm_request(&policy, req); + policy.confirm_request(req); test::return_policy(policy, policy_cap, ctx); // Resolve marketplace's Policy let (policy, policy_cap) = policy::new_for_testing(ctx); - policy::confirm_request(&policy, mkt_req); - let proceeds = policy::destroy_and_withdraw(policy, policy_cap, ctx); - coin::destroy_zero(proceeds); + policy.confirm_request(mkt_req); + policy.destroy_and_withdraw(policy_cap, ctx) + .destroy_zero(); // Deal with the Asset + Kiosk, KioskOwnerCap test::return_assets(vector[ item ]); @@ -65,16 +64,16 @@ module mkt::fixed_trading_tests { /// - extension installed /// - an asset inside fun prepare(ctx: &mut TxContext): (Kiosk, KioskOwnerCap, ID) { - let (kiosk, kiosk_cap) = test::get_kiosk(ctx); + let (mut kiosk, kiosk_cap) = test::get_kiosk(ctx); let (asset, asset_id) = test::get_asset(ctx); - kiosk::place(&mut kiosk, &kiosk_cap, asset); + kiosk.place(&kiosk_cap, asset); mkt::extension::add(&mut kiosk, &kiosk_cap, ctx); (kiosk, kiosk_cap, asset_id) } /// Wrap everything up; remove the extension and the asset. - fun wrapup(kiosk: Kiosk, cap: KioskOwnerCap, ctx: &mut TxContext) { + fun wrapup(mut kiosk: Kiosk, cap: KioskOwnerCap, ctx: &mut TxContext) { kiosk_extension::remove(&mut kiosk, &cap); test::return_kiosk(kiosk, cap, ctx); } From b421476ad7133571141b358108ead60cd55a2ce2 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Fri, 15 Mar 2024 15:30:44 +0300 Subject: [PATCH 11/12] updated version --- kiosk-marketplace/sources/adapter.move | 24 ++--- .../sources/collection_bidding.move | 102 ++++++++++-------- kiosk-marketplace/sources/extension.move | 3 + kiosk-marketplace/sources/fixed_trading.move | 87 +++++++++------ kiosk-marketplace/sources/single_bid.move | 74 +++++++------ kiosk-marketplace/tests/adapter_tests.move | 12 +-- .../tests/collection_bidding_tests.move | 75 +++++-------- kiosk-marketplace/tests/test_utils.move | 57 ++++++++++ .../tests/trading_ext_tests.move | 79 ++++++-------- 9 files changed, 289 insertions(+), 224 deletions(-) create mode 100644 kiosk-marketplace/tests/test_utils.move diff --git a/kiosk-marketplace/sources/adapter.move b/kiosk-marketplace/sources/adapter.move index 5f7d5a4..220b08d 100644 --- a/kiosk-marketplace/sources/adapter.move +++ b/kiosk-marketplace/sources/adapter.move @@ -35,31 +35,31 @@ module mkt::adapter { /// The `MarketPurchaseCap` wraps the `PurchaseCap` and forces the unlocking /// party to satisfy the `TransferPolicy` requirements. - public struct MarketPurchaseCap has store { + public struct MarketPurchaseCap has store { purchase_cap: PurchaseCap } /// Create a new `PurchaseCap` and wrap it into the `MarketPurchaseCap`. - public(friend) fun new( + public(friend) fun new( kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: ID, min_price: u64, ctx: &mut TxContext - ): MarketPurchaseCap { + ): MarketPurchaseCap { let purchase_cap = kiosk.list_with_purchase_cap( cap, item_id, min_price, ctx ); - MarketPurchaseCap { purchase_cap } + MarketPurchaseCap { purchase_cap } } /// Return the `MarketPurchaseCap` to the `Kiosk`. Similar to how the /// `PurchaseCap` can be returned at any moment. But it can't be unwrapped /// into the `PurchaseCap` because that would allow cheating on a `Market`. - public(friend) fun return_cap( + public(friend) fun return_cap( kiosk: &mut Kiosk, - cap: MarketPurchaseCap, + cap: MarketPurchaseCap, _ctx: &mut TxContext ) { let MarketPurchaseCap { purchase_cap } = cap; @@ -69,9 +69,9 @@ module mkt::adapter { /// Use the `MarketPurchaseCap` to purchase an item from the `Kiosk`. Unlike /// the default flow, this function adds a `TransferRequest` which /// forces the unlocking party to satisfy the `TransferPolicy` - public(friend) fun purchase( + public(friend) fun purchase( kiosk: &mut Kiosk, - cap: MarketPurchaseCap, + cap: MarketPurchaseCap, coin: Coin, _ctx: &mut TxContext ): (T, TransferRequest, TransferRequest) { @@ -90,7 +90,7 @@ module mkt::adapter { /// the `Market` type parameter and returns only a `TransferRequest`. public(friend) fun purchase_no_market( kiosk: &mut Kiosk, - cap: MarketPurchaseCap, + cap: MarketPurchaseCap, coin: Coin, _ctx: &mut TxContext ): (T, TransferRequest) { @@ -101,17 +101,17 @@ module mkt::adapter { // === Getters === /// Handy wrapper to read the `kiosk` field of the inner `PurchaseCap` - public(friend) fun kiosk(self: &MarketPurchaseCap): ID { + public(friend) fun kiosk(self: &MarketPurchaseCap): ID { self.purchase_cap.purchase_cap_kiosk() } /// Handy wrapper to read the `item` field of the inner `PurchaseCap` - public(friend) fun item(self: &MarketPurchaseCap): ID { + public(friend) fun item(self: &MarketPurchaseCap): ID { self.purchase_cap.purchase_cap_item() } /// Handy wrapper to read the `min_price` field of the inner `PurchaseCap` - public(friend) fun min_price(self: &MarketPurchaseCap): u64 { + public(friend) fun min_price(self: &MarketPurchaseCap): u64 { self.purchase_cap.purchase_cap_min_price() } diff --git a/kiosk-marketplace/sources/collection_bidding.move b/kiosk-marketplace/sources/collection_bidding.move index 58ff836..c8a0d47 100644 --- a/kiosk-marketplace/sources/collection_bidding.move +++ b/kiosk-marketplace/sources/collection_bidding.move @@ -10,9 +10,7 @@ /// by the seller and the item is placed in the buyer's (bidder's) Kiosk. /// 3. The seller resolves the requests for `Market` and creator. module mkt::collection_bidding { - use std::option::Option; use std::type_name; - use std::vector; use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; use sui::tx_context::TxContext; use sui::coin::{Self, Coin}; @@ -24,7 +22,6 @@ module mkt::collection_bidding { use sui::object::{Self, ID}; use sui::event; use sui::pay; - use sui::bag; use kiosk::personal_kiosk; use mkt::adapter::{Self as mkt, NoMarket}; @@ -44,15 +41,28 @@ module mkt::collection_bidding { const EExtensionNotInstalled: u64 = 5; /// Trying to accept a bid that doesn't match the seller's expectation. const EBidDoesntMatchExpected: u64 = 6; + /// The bid is not in the correct order ASC: last one is the most expensive. + const EIncorrectOrder: u64 = 7; + /// The order_id doesn't match the bid. + const EOrderMismatch: u64 = 8; /// A key for Extension storage - a single bid on an item of type `T` on a `Market`. public struct Bid has copy, store, drop {} + /// A struct that holds the `bids` and the `order_id`. + public struct Bids has store { + /// A vector of coins that represent the bids. + bids: vector>, + /// A unique identifier for the bid. + order_id: address, + } + // === Events === /// An event that is emitted when a new bid is placed. public struct NewBid has copy, drop { kiosk_id: ID, + order_id: address, bids: vector, is_personal: bool, } @@ -61,16 +71,16 @@ module mkt::collection_bidding { public struct BidAccepted has copy, drop { seller_kiosk_id: ID, buyer_kiosk_id: ID, + order_id: address, item_id: ID, amount: u64, - buyer_is_personal: bool, - seller_is_personal: bool, } /// An event that is emitted when a bid is canceled. public struct BidCanceled has copy, drop { + order_id: address, kiosk_id: ID, - kiosk_owner: Option
, + kiosk_owner: address, } // === Bidding logic === @@ -84,26 +94,32 @@ module mkt::collection_bidding { kiosk: &mut Kiosk, cap: &KioskOwnerCap, bids: vector>, - _ctx: &mut TxContext - ) { + ctx: &mut TxContext + ): address { assert!(bids.length() > 0, ENoCoinsPassed); assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); assert!(ext::is_installed(kiosk), EExtensionNotInstalled); + let order_id = ctx.fresh_object_address(); let mut amounts = vector[]; - let (mut i, count) = (0, vector::length(&bids)); + let (mut i, count, mut prev) = (0, bids.length(), 0); while (i < count) { - vector::push_back(&mut amounts, vector::borrow(&bids, i).value()); + let amount = bids.borrow(i).value(); + assert!(amount >= prev && amount > 0, EIncorrectOrder); + amounts.push_back(bids.borrow(i).value()); + prev = amount; i = i + 1; }; - event::emit(NewBid { - kiosk_id: object::id(kiosk), + event::emit(NewBid { + order_id, bids: amounts, + kiosk_id: object::id(kiosk), is_personal: personal_kiosk::is_personal(kiosk) }); - bag::add(ext::storage_mut(kiosk), Bid {}, bids); + ext::storage_mut(kiosk).add(Bid {}, Bids { bids, order_id }); + order_id } /// Cancel all bids, return the funds to the owner. @@ -111,16 +127,19 @@ module mkt::collection_bidding { kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext ): Coin { assert!(ext::is_installed(kiosk), EExtensionNotInstalled); - assert!(kiosk::has_access(kiosk, cap), ENotAuthorized); + assert!(kiosk.has_access(cap), ENotAuthorized); + + let Bids { bids, order_id } = ext::storage_mut(kiosk) + .remove(Bid {}); - event::emit(BidCanceled { + event::emit(BidCanceled { + order_id, kiosk_id: object::id(kiosk), - kiosk_owner: personal_kiosk::try_owner(kiosk) + kiosk_owner: personal_kiosk::owner(kiosk) }); - let coins = ext::storage_mut(kiosk).remove(Bid {}); let mut total = coin::zero(ctx); - pay::join_vec(&mut total, coins); + pay::join_vec(&mut total, bids); total } @@ -146,51 +165,51 @@ module mkt::collection_bidding { policy: &TransferPolicy, item_id: ID, // for race conditions protection + bid_order_id: address, min_bid_amount: u64, // keeping these arguments for extendability _lock: bool, ctx: &mut TxContext ): (TransferRequest, TransferRequest) { assert!(ext::is_enabled(buyer), EExtensionNotInstalled); - assert!(kiosk::has_access(seller, seller_cap), ENotAuthorized); + assert!(ext::is_enabled(buyer), EExtensionDisabled); + assert!(seller.has_access(seller_cap), ENotAuthorized); + assert!(type_name::get() != type_name::get(), EIncorrectMarketArg); let storage = ext::storage_mut(buyer); - assert!(storage.contains(Bid {}), EBidNotFound); + assert!(storage.contains(Bid {}), EBidNotFound); // Take 1 Coin from the bag - this is our bid (bids can't be empty, we // make sure of it). - let bid: Coin = vector::pop_back(bag::borrow_mut(storage, Bid {})); + let Bids { bids, order_id } = storage.borrow_mut(Bid {}); + let order_id = *order_id; + let bid = bids.pop_back(); + let left = bids.length(); + assert!(order_id == &bid_order_id, EOrderMismatch); assert!(bid.value() >= min_bid_amount, EBidDoesntMatchExpected); // If there are no bids left, remove the bag and the key from the storage. - if (bid_count(buyer) == 0) { - vector::destroy_empty>( - ext::storage_mut(buyer).remove(Bid {}) - ); + if (left == 0) { + let Bids { order_id: _, bids } = storage.remove(Bid {}); + bids.destroy_empty(); }; let amount = bid.value(); - assert!(ext::is_enabled(buyer), EExtensionDisabled); // assert!(mkt::kiosk(&mkt_cap) == object::id(seller), EIncorrectKiosk); // assert!(mkt::min_price(&mkt_cap) <= amount, EBidDoesntMatchExpectation); - assert!(type_name::get() != type_name::get(), EIncorrectMarketArg); - - let mkt_cap = mkt::new( - seller, seller_cap, item_id, amount, ctx - ); // Perform the purchase operation in the seller's Kiosk using the `Bid`. + let mkt_cap = mkt::new(seller, seller_cap, item_id, amount, ctx); let (item, request, market_request) = mkt::purchase(seller, mkt_cap, bid, ctx); - event::emit(BidAccepted { + event::emit(BidAccepted { amount, + order_id, item_id: object::id(&item), buyer_kiosk_id: object::id(buyer), seller_kiosk_id: object::id(seller), - buyer_is_personal: personal_kiosk::is_personal(buyer), - seller_is_personal: personal_kiosk::is_personal(seller) }); // Place or lock the item in the `source` Kiosk. @@ -202,17 +221,10 @@ module mkt::collection_bidding { // === Getters === /// Number of bids on an item of type `T` on a `Market` in a `Kiosk`. - public fun bid_count(kiosk: &Kiosk): u64 { - let coins: &vector> = ext::storage(kiosk) - .borrow(Bid {}); - - coins.length() - } + public fun bid_count(kiosk: &Kiosk): u64 { + let Bids { bids, order_id: _ } = ext::storage(kiosk) + .borrow(Bid {}); - /// Returns the amount of the bid on an item of type `T` on a `Market`. - /// The `NoMarket` generic can be used to check an item listed off the market. - public fun bid_amount(kiosk: &Kiosk): u64 { - let coins: &vector> = ext::storage(kiosk).borrow(Bid {}); - coins.borrow(0).value() + bids.length() } } diff --git a/kiosk-marketplace/sources/extension.move b/kiosk-marketplace/sources/extension.move index 15bd43d..2e21281 100644 --- a/kiosk-marketplace/sources/extension.move +++ b/kiosk-marketplace/sources/extension.move @@ -13,6 +13,7 @@ module mkt::extension { use sui::vec_set; use kiosk::kiosk_lock_rule::Rule as LockRule; + use kiosk::personal_kiosk; friend mkt::collection_bidding; friend mkt::fixed_trading; @@ -26,6 +27,8 @@ module mkt::extension { /// Install the Marketplace Extension into the Kiosk. public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { + // TODO: check that the kiosk is a personal kiosk + personal_kiosk::is_personal(kiosk); ext::add(Extension {}, kiosk, cap, PERMISSIONS, ctx) } diff --git a/kiosk-marketplace/sources/fixed_trading.move b/kiosk-marketplace/sources/fixed_trading.move index 814d91b..e1570b2 100644 --- a/kiosk-marketplace/sources/fixed_trading.move +++ b/kiosk-marketplace/sources/fixed_trading.move @@ -9,15 +9,13 @@ /// - delist /// - purchase module mkt::fixed_trading { - use std::option::Option; - use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::kiosk::{Kiosk, KioskOwnerCap}; use sui::transfer_policy::TransferRequest; use sui::tx_context::TxContext; use sui::object::{Self, ID}; - use sui::coin::{Self, Coin}; + use sui::coin::Coin; use sui::sui::SUI; use sui::event; - use sui::bag; use kiosk::personal_kiosk; use mkt::adapter::{Self as mkt, MarketPurchaseCap}; @@ -30,6 +28,11 @@ module mkt::fixed_trading { /// The payment is not enough to purchase the item. const EIncorrectAmount: u64 = 2; + public struct Listing has store { + market_cap: MarketPurchaseCap, + order_id: address, + } + // === Events === /// An item has been listed on a Marketplace. @@ -37,86 +40,101 @@ module mkt::fixed_trading { kiosk_id: ID, item_id: ID, price: u64, - is_personal: bool, + order_id: address, } /// An item has been delisted from a Marketplace. public struct ItemDelisted has copy, drop { kiosk_id: ID, item_id: ID, - is_personal: bool, + order_id: address, } /// An item has been purchased from a Marketplace. public struct ItemPurchased has copy, drop { kiosk_id: ID, item_id: ID, + order_id: address, /// The seller address if the Kiosk is personal. - seller: Option
, + seller: address, } // === Trading Functions === /// List an item on a specified Marketplace. - public fun list( + public fun list( kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: ID, price: u64, ctx: &mut TxContext - ) { + ): address { assert!(kiosk.has_access(cap), ENotOwner); - let mkt_cap = mkt::new(kiosk, cap, item_id, price, ctx); - ext::storage_mut(kiosk).add(item_id, mkt_cap); + let order_id = ctx.fresh_object_address(); + let market_cap = mkt::new(kiosk, cap, item_id, price, ctx); + + ext::storage_mut(kiosk).add(item_id, Listing { + market_cap, + order_id, + }); - event::emit(ItemListed { - is_personal: personal_kiosk::is_personal(kiosk), + event::emit(ItemListed { kiosk_id: object::id(kiosk), + order_id, item_id, price, }); + + order_id } /// Delist an item from a specified Marketplace. - public fun delist( + public fun delist( kiosk: &mut Kiosk, cap: &KioskOwnerCap, item_id: ID, ctx: &mut TxContext ) { assert!(kiosk.has_access(cap), ENotOwner); - assert!(kiosk.is_listed(item_id), ENotListed); + assert!(kiosk.is_listed(item_id), ENotListed); - let mkt_cap = ext::storage_mut(kiosk).remove(item_id); - mkt::return_cap(kiosk, mkt_cap, ctx); + let Listing { market_cap, order_id } = ext::storage_mut(kiosk).remove(item_id); + mkt::return_cap(kiosk, market_cap, ctx); - event::emit(ItemDelisted { - is_personal: personal_kiosk::is_personal(kiosk), + event::emit(ItemDelisted { kiosk_id: object::id(kiosk), + order_id, item_id }); } /// Purchase an item from a specified Marketplace. - public fun purchase( + public fun purchase( kiosk: &mut Kiosk, item_id: ID, + list_order_id: address, payment: Coin, ctx: &mut TxContext ): (T, TransferRequest, TransferRequest) { - assert!(kiosk.is_listed(item_id), ENotListed); + assert!(kiosk.is_listed(item_id), ENotListed); + + let Listing { + market_cap, + order_id + } = ext::storage_mut(kiosk).remove(item_id); - let mkt_cap = ext::storage_mut(kiosk).remove>(item_id); - assert!(payment.value() == mkt_cap.min_price(), EIncorrectAmount); + assert!(payment.value() == market_cap.min_price(), EIncorrectAmount); + assert!(order_id == list_order_id, EIncorrectAmount); - event::emit(ItemPurchased { - seller: personal_kiosk::try_owner(kiosk), + event::emit(ItemPurchased { + seller: personal_kiosk::owner(kiosk), kiosk_id: object::id(kiosk), + order_id, item_id }); - mkt::purchase(kiosk, mkt_cap, payment, ctx) + mkt::purchase(kiosk, market_cap, payment, ctx) } // === Getters === @@ -124,16 +142,17 @@ module mkt::fixed_trading { use fun is_listed as Kiosk.is_listed; /// Check if an item is currently listed on a specified Marketplace. - public fun is_listed(kiosk: &Kiosk, item_id: ID): bool { - bag::contains_with_type>( - ext::storage(kiosk), - item_id - ) + public fun is_listed(kiosk: &Kiosk, item_id: ID): bool { + ext::storage(kiosk).contains_with_type>(item_id) } /// Get the price of a currently listed item from a specified Marketplace. - public fun price(kiosk: &Kiosk, item_id: ID): u64 { - let mkt_cap = bag::borrow(ext::storage(kiosk), item_id); - mkt::min_price(mkt_cap) + public fun price(kiosk: &Kiosk, item_id: ID): u64 { + let Listing { + market_cap, + order_id: _ + } = ext::storage(kiosk).borrow(item_id); + + market_cap.min_price() } } diff --git a/kiosk-marketplace/sources/single_bid.move b/kiosk-marketplace/sources/single_bid.move index e92e23b..57bb116 100644 --- a/kiosk-marketplace/sources/single_bid.move +++ b/kiosk-marketplace/sources/single_bid.move @@ -10,16 +10,15 @@ /// 2. A seller accepts the bid and sells the item to the buyer in a single action. /// 3. The seller resolves the requests for `Market` and creator. module mkt::single_bid { - use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::kiosk::{Kiosk, KioskOwnerCap}; use sui::transfer_policy::{TransferPolicy, TransferRequest}; use sui::tx_context::TxContext; use sui::object::{Self, ID}; use sui::coin::{Self, Coin}; + use sui::balance::Balance; use sui::sui::SUI; use sui::event; - use sui::bag; - use kiosk::personal_kiosk; use mkt::extension as ext; use mkt::adapter as mkt; @@ -33,10 +32,17 @@ module mkt::single_bid { const EExtensionNotInstalled: u64 = 3; /// No bid found for the item. const ENoBid: u64 = 4; + /// Order ID mismatch + const EOrderMismatch: u64 = 5; /// The dynamic field key for the Bid. public struct Bid has copy, store, drop { item_id: ID } + public struct PlacedBid has store { + bid: Balance, + order_id: address, + } + // === Events === /// Event emitted when a bid is placed. @@ -44,7 +50,7 @@ module mkt::single_bid { kiosk_id: ID, item_id: ID, bid: u64, - is_personal: bool, + order_id: address, } /// Event emitted when a bid is accepted. @@ -52,8 +58,7 @@ module mkt::single_bid { kiosk_id: ID, item_id: ID, bid: u64, - buyer_is_personal: bool, - seller_is_personal: bool, + order_id: address, } /// Event emitted when a bid is cancelled. @@ -61,7 +66,7 @@ module mkt::single_bid { kiosk_id: ID, item_id: ID, bid: u64, - is_personal: bool, + order_id: address, } // === Bidding Logic === @@ -72,23 +77,26 @@ module mkt::single_bid { cap: &KioskOwnerCap, bid: Coin, item_id: ID, - _ctx: &mut TxContext - ) { + ctx: &mut TxContext + ): address { assert!(kiosk.has_access(cap), ENotAuthorized); assert!(ext::is_installed(kiosk), EExtensionNotInstalled); - event::emit(NewBid { + let order_id = ctx.fresh_object_address(); + + event::emit(NewBid { kiosk_id: object::id(kiosk), bid: bid.value(), - is_personal: personal_kiosk::is_personal(kiosk), + order_id, item_id, }); - ext::storage_mut(kiosk).add( - Bid { item_id }, - bid - ) + Bid { item_id }, + PlacedBid { bid: bid.into_balance(), order_id } + ); + + order_id } /// Accept a single bid for an item with a specified `ID`. For that the @@ -100,26 +108,29 @@ module mkt::single_bid { seller_cap: &KioskOwnerCap, policy: &TransferPolicy, item_id: ID, + bid_order_id: address, _lock: bool, ctx: &mut TxContext ): (TransferRequest, TransferRequest) { assert!(ext::is_enabled(buyer), EExtensionNotInstalled); - assert!(ext::storage(buyer).contains(Bid { item_id }), ENoBid); + assert!(ext::storage(buyer).contains(Bid { item_id }), ENoBid); assert!(seller.has_item(item_id), EItemNotFound); assert!(!seller.is_listed(item_id), EAlreadyListed); - let coin: Coin = ext::storage_mut(buyer) - .remove(Bid { item_id }); + let PlacedBid { + bid, order_id + } = ext::storage_mut(buyer).remove(Bid { item_id }); + + assert!(order_id == bid_order_id, EOrderMismatch); - let amount = coin.value(); + let amount = bid.value(); let mkt_cap = mkt::new(seller, seller_cap, item_id, amount, ctx); - let (item, req, mkt_req) = mkt::purchase(seller, mkt_cap, coin, ctx); + let (item, req, mkt_req) = mkt::purchase(seller, mkt_cap, coin::from_balance(bid, ctx), ctx); - event::emit(BidAccepted { + event::emit(BidAccepted { kiosk_id: object::id(buyer), bid: amount, - buyer_is_personal: personal_kiosk::is_personal(buyer), - seller_is_personal: personal_kiosk::is_personal(seller), + order_id, item_id, }); @@ -132,22 +143,23 @@ module mkt::single_bid { kiosk: &mut Kiosk, kiosk_cap: &KioskOwnerCap, item_id: ID, - _ctx: &mut TxContext + ctx: &mut TxContext ): Coin { assert!(kiosk.has_access(kiosk_cap), ENotAuthorized); assert!(ext::is_installed(kiosk), EExtensionNotInstalled); - assert!(ext::storage(kiosk).contains(Bid { item_id }), ENoBid); + assert!(ext::storage(kiosk).contains(Bid { item_id }), ENoBid); - let coin: Coin = ext::storage_mut(kiosk) - .remove(Bid { item_id }); + let PlacedBid { + order_id, bid, + } = ext::storage_mut(kiosk).remove(Bid { item_id }); - event::emit(BidCancelled { + event::emit(BidCancelled { kiosk_id: object::id(kiosk), - bid: coin.value(), - is_personal: personal_kiosk::is_personal(kiosk), + bid: bid.value(), + order_id, item_id, }); - coin + coin::from_balance(bid, ctx) } } diff --git a/kiosk-marketplace/tests/adapter_tests.move b/kiosk-marketplace/tests/adapter_tests.move index bcbf70c..3ddec07 100644 --- a/kiosk-marketplace/tests/adapter_tests.move +++ b/kiosk-marketplace/tests/adapter_tests.move @@ -29,13 +29,13 @@ module mkt::adapter_tests { kiosk::place(&mut kiosk, &kiosk_cap, asset); - let mkt_cap = mkt::new( + let mkt_cap = mkt::new( &mut kiosk, &kiosk_cap, asset_id, 100000, ctx ); - assert!(mkt::item(&mkt_cap) == asset_id, 0); - assert!(mkt::min_price(&mkt_cap) == 100000, 1); - assert!(mkt::kiosk(&mkt_cap) == object::id(&kiosk), 2); + assert!(mkt_cap.item() == asset_id, 0); + assert!(mkt_cap.min_price() == 100000, 1); + assert!(mkt_cap.kiosk() == object::id(&kiosk), 2); mkt::return_cap(&mut kiosk, mkt_cap, ctx); @@ -55,13 +55,13 @@ module mkt::adapter_tests { kiosk.place(&kiosk_cap, asset); // Lock an item in the Marketplace - let mkt_cap = mkt::new( + let mkt_cap = mkt::new( &mut kiosk, &kiosk_cap, asset_id, 100000, ctx ); // Mint a Coin and make a purchase let coin = coin::mint_for_testing(100000, ctx); - let (item, req, mkt_req) = mkt::purchase( + let (item, req, mkt_req) = mkt::purchase( &mut kiosk, mkt_cap, coin, ctx ); diff --git a/kiosk-marketplace/tests/collection_bidding_tests.move b/kiosk-marketplace/tests/collection_bidding_tests.move index 4d9e153..c33e2b0 100644 --- a/kiosk-marketplace/tests/collection_bidding_tests.move +++ b/kiosk-marketplace/tests/collection_bidding_tests.move @@ -3,58 +3,47 @@ #[test_only] module mkt::collection_bidding_tests { - use sui::coin; use sui::test_utils; - use sui::tx_context::TxContext; - use sui::kiosk_test_utils::{Self as test, Asset}; - use sui::transfer_policy::{ - Self as policy, - TransferPolicy, - TransferPolicyCap - }; + use sui::kiosk_test_utils::Asset; use mkt::collection_bidding::{Self as bidding}; + use mkt::test_utils as test; /// The Marketplace witness. public struct MyMarket has drop {} #[test] fun test_simple_bid() { - let ctx = &mut test::ctx(); - let (mut buyer_kiosk, buyer_cap) = test::get_kiosk(ctx); - - mkt::extension::add(&mut buyer_kiosk, &buyer_cap, ctx); + let mut test = test::new(); + let ctx = &mut test.next_tx(@0x1); + let (mut buyer_kiosk, buyer_cap, _) = test.kiosk(ctx); // place bids on an Asset: 100 MIST - bidding::place_bids( + let order_id = bidding::place_bids( &mut buyer_kiosk, - &buyer_cap, + buyer_cap.borrow(), vector[ - test::get_sui(100, ctx), - test::get_sui(300, ctx) + test.mint_sui(300, ctx), + test.mint_sui(400, ctx) ], ctx ); // prepare the seller Kiosk - let (mut seller_kiosk, seller_cap) = test::get_kiosk(ctx); - let (asset, asset_id) = test::get_asset(ctx); - - // place the asset and create a MarketPurchaseCap - // bidding::add(&mut seller_kiosk, &seller_cap, ctx); - seller_kiosk.place(&seller_cap, asset); - - let (asset_policy, asset_policy_cap) = get_policy(ctx); - let (mkt_policy, mkt_policy_cap) = get_policy(ctx); + let ctx = &mut test.next_tx(@0x2); + let (mut seller_kiosk, seller_cap, asset_id) = test.kiosk(ctx); + let asset_policy = test.policy(ctx); + let mkt_policy = test.policy(ctx); - // take the bid and perform the purchase + // take the bid and perform the purchase (400 SUI) let (asset_request, mkt_request) = bidding::accept_market_bid( &mut buyer_kiosk, &mut seller_kiosk, - &seller_cap, + seller_cap.borrow(), &asset_policy, asset_id, - 300, + order_id, + 400, false, ctx ); @@ -64,20 +53,21 @@ module mkt::collection_bidding_tests { assert!(buyer_kiosk.has_item(asset_id), 0); assert!(!seller_kiosk.has_item(asset_id), 1); - assert!(seller_kiosk.profits_amount() == 300, 2); + assert!(seller_kiosk.profits_amount() == 400, 2); // do it all over again - let (asset, asset_id) = test::get_asset(ctx); - seller_kiosk.place(&seller_cap, asset); + let (asset, asset_id) = test.asset(ctx); + seller_kiosk.place(seller_cap.borrow(), asset); - // second bid + // second bid (smaller) let (asset_request, mkt_request) = bidding::accept_market_bid( &mut buyer_kiosk, &mut seller_kiosk, - &seller_cap, + seller_cap.borrow(), &asset_policy, asset_id, - 400, + order_id, + 300, false, ctx ); @@ -87,26 +77,13 @@ module mkt::collection_bidding_tests { assert!(buyer_kiosk.has_item(asset_id), 3); assert!(!seller_kiosk.has_item(asset_id), 4); - assert!(seller_kiosk.profits_amount() == 400, 5); + assert!(seller_kiosk.profits_amount() == 700, 5); test_utils::destroy(seller_kiosk); test_utils::destroy(buyer_kiosk); test_utils::destroy(seller_cap); test_utils::destroy(buyer_cap); - return_policy(asset_policy, asset_policy_cap, ctx); - return_policy(mkt_policy, mkt_policy_cap, ctx); - } - - fun get_policy(ctx: &mut TxContext): (TransferPolicy, TransferPolicyCap) { - policy::new_for_testing(ctx) - } - - fun return_policy( - policy: TransferPolicy, policy_cap: TransferPolicyCap, ctx: &mut TxContext - ): u64 { - coin::burn_for_testing( - policy::destroy_and_withdraw(policy, policy_cap, ctx) - ) + test.destroy(asset_policy).destroy(mkt_policy); } } diff --git a/kiosk-marketplace/tests/test_utils.move b/kiosk-marketplace/tests/test_utils.move new file mode 100644 index 0000000..b945e2c --- /dev/null +++ b/kiosk-marketplace/tests/test_utils.move @@ -0,0 +1,57 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module mkt::test_utils { + use sui::object::ID; + use sui::sui::SUI; + use sui::coin::{Self, Coin}; + use sui::kiosk::{Self, Kiosk}; + use sui::kiosk_test_utils::{Self as test, Asset}; + use sui::transfer_policy::{Self, TransferPolicy}; + use kiosk::personal_kiosk::{Self, PersonalKioskCap}; + + public struct TestRunner has drop { seq: u64 } + + public fun new(): TestRunner { + TestRunner { seq: 1 } + } + + public fun next_tx(self: &mut TestRunner, sender: address): TxContext { + self.seq = self.seq + 1; + tx_context::new_from_hint( + sender, + self.seq, + 0, 0, 0 + ) + } + + public fun kiosk(_self: &TestRunner, ctx: &mut TxContext): (Kiosk, PersonalKioskCap, ID) { + let (mut kiosk, cap) = kiosk::new(ctx); + let (asset, asset_id) = test::get_asset(ctx); + + kiosk.place(&cap, asset); + let cap = personal_kiosk::new(&mut kiosk, cap, ctx); + mkt::extension::add(&mut kiosk, cap.borrow(), ctx); + (kiosk, cap, asset_id) + } + + public fun policy(self: &TestRunner, ctx: &mut TxContext): TransferPolicy { + let (policy, cap) = transfer_policy::new_for_testing(ctx); + self.destroy(cap); + policy + } + + public fun asset(_self: &TestRunner, ctx: &mut TxContext): (Asset, ID) { + test::get_asset(ctx) + } + + public fun mint_sui(_self: &mut TestRunner, amount: u64, ctx: &mut TxContext): Coin { + coin::mint_for_testing(amount, ctx) + } + + public fun destroy(self: &TestRunner, t: T): &TestRunner { + sui::test_utils::destroy(t); + self + } +} diff --git a/kiosk-marketplace/tests/trading_ext_tests.move b/kiosk-marketplace/tests/trading_ext_tests.move index 6c67b89..9648781 100644 --- a/kiosk-marketplace/tests/trading_ext_tests.move +++ b/kiosk-marketplace/tests/trading_ext_tests.move @@ -4,12 +4,10 @@ #[test_only] /// Tests for the marketplace `marketplace_trading_ext`. module mkt::fixed_trading_tests { - use sui::object::ID; - use sui::kiosk_extension; - use sui::tx_context::TxContext; - use sui::transfer_policy as policy; - use sui::kiosk::{Kiosk, KioskOwnerCap}; - use sui::kiosk_test_utils::{Self as test, Asset}; + use sui::test_utils::destroy; + use sui::kiosk_test_utils::Asset; + + use mkt::test_utils as test; use mkt::fixed_trading as ext; const PRICE: u64 = 100_000; @@ -18,63 +16,50 @@ module mkt::fixed_trading_tests { public struct MyMarket has drop {} #[test] fun test_list_and_delist() { - let ctx = &mut test::ctx(); - let (mut kiosk, kiosk_cap, asset_id) = prepare(ctx); + let mut test = test::new(); + let ctx = &mut test.next_tx(@0x1); + let (mut kiosk, cap, asset_id) = test.kiosk(ctx); + + let _order_id = ext::list(&mut kiosk, cap.borrow(), asset_id, PRICE, ctx); - ext::list(&mut kiosk, &kiosk_cap, asset_id, PRICE, ctx); + assert!(ext::is_listed(&kiosk, asset_id), 0); + assert!(ext::price(&kiosk, asset_id) == PRICE, 1); - assert!(ext::is_listed(&kiosk, asset_id), 0); - assert!(ext::price(&kiosk, asset_id) == PRICE, 1); + ext::delist(&mut kiosk, cap.borrow(), asset_id, ctx); - ext::delist(&mut kiosk, &kiosk_cap, asset_id, ctx); + let asset: Asset = kiosk.take(cap.borrow(), asset_id); - let asset = kiosk.take(&kiosk_cap, asset_id); - test::return_assets(vector[ asset ]); - wrapup(kiosk, kiosk_cap, ctx); + destroy(kiosk); + destroy(asset); + destroy(cap); } #[test] fun test_list_and_purchase() { - let ctx = &mut test::ctx(); - let (mut kiosk, kiosk_cap, asset_id) = prepare(ctx); + let mut test = test::new(); + let ctx = &mut test.next_tx(@0x1); + let (mut kiosk, cap, asset_id) = test.kiosk(ctx); - ext::list(&mut kiosk, &kiosk_cap, asset_id, PRICE, ctx); + let order_id = ext::list( + &mut kiosk, cap.borrow(), asset_id, PRICE, ctx + ); - let coin = test::get_sui(PRICE, ctx); - let (item, req, mkt_req) = ext::purchase( - &mut kiosk, asset_id, coin, ctx + let coin = test.mint_sui(PRICE, ctx); + let (item, req, mkt_req) = ext::purchase( + &mut kiosk, asset_id, order_id, coin, ctx ); // Resolve creator's Policy - let (policy, policy_cap) = test::get_policy(ctx); + let policy = test.policy(ctx); policy.confirm_request(req); - test::return_policy(policy, policy_cap, ctx); + test.destroy(policy); // Resolve marketplace's Policy - let (policy, policy_cap) = policy::new_for_testing(ctx); + let policy = test.policy(ctx); policy.confirm_request(mkt_req); - policy.destroy_and_withdraw(policy_cap, ctx) - .destroy_zero(); - - // Deal with the Asset + Kiosk, KioskOwnerCap - test::return_assets(vector[ item ]); - wrapup(kiosk, kiosk_cap, ctx); - } - - /// Prepare a Kiosk with: - /// - extension installed - /// - an asset inside - fun prepare(ctx: &mut TxContext): (Kiosk, KioskOwnerCap, ID) { - let (mut kiosk, kiosk_cap) = test::get_kiosk(ctx); - let (asset, asset_id) = test::get_asset(ctx); - - kiosk.place(&kiosk_cap, asset); - mkt::extension::add(&mut kiosk, &kiosk_cap, ctx); - (kiosk, kiosk_cap, asset_id) - } + test.destroy(policy); - /// Wrap everything up; remove the extension and the asset. - fun wrapup(mut kiosk: Kiosk, cap: KioskOwnerCap, ctx: &mut TxContext) { - kiosk_extension::remove(&mut kiosk, &cap); - test::return_kiosk(kiosk, cap, ctx); + test.destroy(kiosk) + .destroy(item) + .destroy(cap); } } From c2bf2519225419be56b7bf79a33733d2e80f8360 Mon Sep 17 00:00:00 2001 From: Damir Shamanaev Date: Tue, 2 Apr 2024 12:22:11 +0300 Subject: [PATCH 12/12] force kiosk to be personal --- kiosk-marketplace/Move.toml | 2 +- kiosk-marketplace/sources/extension.move | 6 ++++-- kiosk/sources/extensions/personal_kiosk.move | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/kiosk-marketplace/Move.toml b/kiosk-marketplace/Move.toml index bfb5cb6..48c6ff3 100644 --- a/kiosk-marketplace/Move.toml +++ b/kiosk-marketplace/Move.toml @@ -1,5 +1,5 @@ [package] -name = "kiosk-marketplace" +name = "KioskMarketplace" edition = "2024.alpha" # edition = "2024.alpha" # To use the Move 2024 edition, currently in alpha diff --git a/kiosk-marketplace/sources/extension.move b/kiosk-marketplace/sources/extension.move index 2e21281..1ad1bdd 100644 --- a/kiosk-marketplace/sources/extension.move +++ b/kiosk-marketplace/sources/extension.move @@ -19,6 +19,9 @@ module mkt::extension { friend mkt::fixed_trading; friend mkt::single_bid; + /// Extension can't be installed in a non-personal Kiosk. + const ENotPersonal: u64 = 0; + /// The extension Witness. public struct Extension has drop {} @@ -27,8 +30,7 @@ module mkt::extension { /// Install the Marketplace Extension into the Kiosk. public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) { - // TODO: check that the kiosk is a personal kiosk - personal_kiosk::is_personal(kiosk); + assert!(personal_kiosk::is_personal(kiosk), ENotPersonal); ext::add(Extension {}, kiosk, cap, PERMISSIONS, ctx) } diff --git a/kiosk/sources/extensions/personal_kiosk.move b/kiosk/sources/extensions/personal_kiosk.move index b9ce890..fe139d5 100644 --- a/kiosk/sources/extensions/personal_kiosk.move +++ b/kiosk/sources/extensions/personal_kiosk.move @@ -19,7 +19,7 @@ module kiosk::personal_kiosk { const EIncorrectOwnedObject: u64 = 1; /// Trying to get the owner of a non-personal Kiosk. const EKioskNotOwned: u64 = 2; - /// Trying to make a someone else's Kiosk "personal". + /// Trying to make someone else's Kiosk "personal". const EWrongKiosk: u64 = 3; /// A key-only wrapper for the KioskOwnerCap. Makes sure that the Kiosk can @@ -42,7 +42,7 @@ module kiosk::personal_kiosk { /// The default setup for the PersonalKioskCap. entry fun default(kiosk: &mut Kiosk, cap: KioskOwnerCap, ctx: &mut TxContext) { - transfer_to_sender(new(kiosk, cap, ctx), ctx); + transfer_to_sender(new(kiosk, cap, ctx), ctx) } /// Wrap the KioskOwnerCap making the Kiosk "owned" and non-transferable. @@ -73,7 +73,7 @@ module kiosk::personal_kiosk { // wrap the Cap in the `PersonalKioskCap` PersonalKioskCap { id: object::new(ctx), - cap: option::some(cap) + cap: option::some(cap), } }