Skip to content

Commit

Permalink
[move] NFT marketplace (MystenLabs#921)
Browse files Browse the repository at this point in the history
- adds nft example
  • Loading branch information
damirka authored Mar 21, 2022
1 parent d021355 commit d4ecd15
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 3 deletions.
2 changes: 1 addition & 1 deletion sui_programmability/examples/nfts/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# NFTs

* Num: Issuing the first ten natural numbers as collectible NFT's.
* Marketplace: An opensea-like marketplace built with shared objects.
* Geniteam: NFTs representing collectible monsters and cosmetics used in a farming game.
* Auction: example implementation of an [English auction](https://en.wikipedia.org/wiki/English_auction) using single-owner objects only.
* SharedAuction: example implementation of an [English auction](https://en.wikipedia.org/wiki/English_auction) using shared objects.
* ImageNFT: an NFT wrapping a URL pointing to an image stored off-chain (coming soon).
* Marketplace: An NFT marketplace (coming soon).
306 changes: 306 additions & 0 deletions sui_programmability/examples/nfts/sources/Marketplace.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
module NFTs::Marketplace {
use Sui::TxContext::{Self, TxContext};
use Sui::ID::{Self, ID, VersionedID};
use Sui::Transfer::{Self};
use Sui::Coin::{Self, Coin};
use Std::Option::{Self, Option};
use Std::Vector;

// For when amount paid does not match the expected.
const EAMOUNT_INCORRECT: u64 = 0;

// For when someone tries to delist without ownership.
const ENOT_OWNER: u64 = 1;

// For when trying to remove object that's not on the Marketplace.
const EOBJECT_NOT_FOUND: u64 = 2;

/// Adding the same object to the markeplace twice is not allowed.
const EOBJECT_DOUBLE_ADD: u64 = 3;

struct Marketplace has key {
id: VersionedID,
objects: vector<ID>,
owner: address,
}

/// A single listing which contains the listed item and its price in [`Coin<C>`].
struct Listing<T: key + store, phantom C> has key, store {
id: VersionedID,
item: T,
ask: u64, // Coin<C>
owner: address,
}

/// Create a new shared Marketplace.
public fun create(ctx: &mut TxContext) {
Transfer::share_object(Marketplace {
id: TxContext::new_id(ctx),
objects: Vector::empty(),
owner: TxContext::sender(ctx),
});
}

/// List an item at the Marketplace.
public fun list<T: key + store, C>(
marketplace: &mut Marketplace,
item: T,
ask: u64,
ctx: &mut TxContext
) {
internal_add(marketplace, Listing<T, C> {
item,
ask,
id: TxContext::new_id(ctx),
owner: TxContext::sender(ctx),
})
}

/// Remove listing and get an item back. Only owner can do that.
public fun delist<T: key + store, C>(
marketplace: &mut Marketplace,
listing: Listing<T, C>,
ctx: &mut TxContext
): T {
let listing = internal_remove(marketplace, listing);
let Listing { id, item, ask: _, owner } = listing;

assert!(TxContext::sender(ctx) == owner, ENOT_OWNER);

ID::delete(id);
item
}

/// Call [`delist`] and transfer item to the sender.
public fun delist_and_take<T: key + store, C>(
market: &mut Marketplace,
listing: Listing<T, C>,
ctx: &mut TxContext
) {
Transfer::transfer(delist(market, listing, ctx), TxContext::sender(ctx))
}

/// Purchase an item using a known Listing. Payment is done in Coin<C>.
/// Amount paid must match the requested amount. If conditions are met,
/// owner of the item gets the payment and buyer receives their item.
public fun buy<T: key + store, C>(
marketplace: &mut Marketplace,
listing: Listing<T, C>,
paid: Coin<C>,
): T {
let listing = internal_remove(marketplace, listing);
let Listing { id, item, ask, owner } = listing;

assert!(ask == Coin::value(&paid), EAMOUNT_INCORRECT);

Transfer::transfer(paid, owner);
ID::delete(id);
item
}

/// Call [`buy`] and transfer item to the sender.
public fun buy_and_take<T: key + store, C>(
market: &mut Marketplace,
listing: Listing<T, C>,
paid: Coin<C>,
ctx: &mut TxContext
) {
Transfer::transfer(buy(market, listing, paid), TxContext::sender(ctx))
}

/// Check whether an object was listed on a Marketplace.
public fun contains(c: &Marketplace, id: &ID): bool {
Option::is_some(&find(c, id))
}

/// Returns the size of the Marketplace.
public fun size(c: &Marketplace): u64 {
Vector::length(&c.objects)
}

/// Rough clone of [`Sui::Bag::add`] to make Marketlace a Bag like object.
fun internal_add<T: key>(c: &mut Marketplace, object: T) {
let id = ID::id(&object);
if (contains(c, id)) {
abort EOBJECT_DOUBLE_ADD
};
Vector::push_back(&mut c.objects, *id);
Transfer::transfer_to_object_unsafe(object, c);
}

/// Rough clone of [`Sui::Bag::remove`].
fun internal_remove<T: key>(c: &mut Marketplace, object: T): T {
let idx = find(c, ID::id(&object));
if (Option::is_none(&idx)) {
abort EOBJECT_NOT_FOUND
};
Vector::remove(&mut c.objects, *Option::borrow(&idx));
object
}

/// Rough clone of [`Sui::Bag::find`].
fun find(c: &Marketplace, id: &ID): Option<u64> {
let i = 0;
let len = size(c);
while (i < len) {
if (Vector::borrow(&c.objects, i) == id) {
return Option::some(i)
};
i = i + 1;
};
Option::none()
}
}

#[test_only]
module NFTs::MarketplaceTests {
use Sui::Transfer;
use Sui::NFT::{Self, NFT};
use Sui::Coin::{Self, Coin};
use Sui::GAS::GAS;
use Sui::TestScenario::{Self, Scenario};
use NFTs::Marketplace::{Self, Marketplace, Listing};

// Simple KITTY-NFT data structure.
struct KITTY has store, drop {
id: u8
}

const ADMIN: address = @0xA55;
const SELLER: address = @0x00A;
const BUYER: address = @0x00B;

/// Create a shared [`Marketplace`].
fun create_marketplace(scenario: &mut Scenario) {
TestScenario::next_tx(scenario, &ADMIN);
Marketplace::create(TestScenario::ctx(scenario));
}

/// Mint GAS and send it to BUYER.
fun mint_some_coin(scenario: &mut Scenario) {
TestScenario::next_tx(scenario, &ADMIN);
let coin = Coin::mint_for_testing<GAS>(1000, TestScenario::ctx(scenario));
Transfer::transfer(coin, BUYER);
}

/// Mint KITTY NFT and send it to SELLER.
fun mint_kitty(scenario: &mut Scenario) {
TestScenario::next_tx(scenario, &ADMIN);
let nft = NFT::mint(KITTY { id: 1 }, TestScenario::ctx(scenario));
NFT::transfer(nft, SELLER);
}

// SELLER lists KITTY at the Marketplace for 100 GAS.
fun list_kitty(scenario: &mut Scenario) {
TestScenario::next_tx(scenario, &SELLER);
let mkp = TestScenario::remove_object<Marketplace>(scenario);
let nft = TestScenario::remove_object<NFT<KITTY>>(scenario);

Marketplace::list<NFT<KITTY>, GAS>(&mut mkp, nft, 100, TestScenario::ctx(scenario));
TestScenario::return_object(scenario, mkp);
}

#[test]
fun list_and_delist() {
let scenario = &mut TestScenario::begin(&ADMIN);

create_marketplace(scenario);
mint_kitty(scenario);
list_kitty(scenario);

TestScenario::next_tx(scenario, &SELLER);
{
let mkp = TestScenario::remove_object<Marketplace>(scenario);
let listing = TestScenario::remove_nested_object<Marketplace, Listing<NFT<KITTY>, GAS>>(scenario, &mkp);

// Do the delist operation on a Marketplace.
let nft = Marketplace::delist<NFT<KITTY>, GAS>(&mut mkp, listing, TestScenario::ctx(scenario));
let kitten = NFT::burn<KITTY>(nft);

assert!(kitten.id == 1, 0);

TestScenario::return_object(scenario, mkp);
};
}

#[test]
#[expected_failure(abort_code = 1)]
fun fail_to_delist() {
let scenario = &mut TestScenario::begin(&ADMIN);

create_marketplace(scenario);
mint_some_coin(scenario);
mint_kitty(scenario);
list_kitty(scenario);

// BUYER attempts to delist KITTY and he has no right to do so. :(
TestScenario::next_tx(scenario, &BUYER);
{
let mkp = TestScenario::remove_object<Marketplace>(scenario);
let listing = TestScenario::remove_nested_object<Marketplace, Listing<NFT<KITTY>, GAS>>(scenario, &mkp);

// Do the delist operation on a Marketplace.
let nft = Marketplace::delist<NFT<KITTY>, GAS>(&mut mkp, listing, TestScenario::ctx(scenario));
let _ = NFT::burn<KITTY>(nft);

TestScenario::return_object(scenario, mkp);
};
}

#[test]
fun buy_kitty() {
let scenario = &mut TestScenario::begin(&ADMIN);

create_marketplace(scenario);
mint_some_coin(scenario);
mint_kitty(scenario);
list_kitty(scenario);

// BUYER takes 100 GAS from his wallet and purchases KITTY.
TestScenario::next_tx(scenario, &BUYER);
{
let coin = TestScenario::remove_object<Coin<GAS>>(scenario);
let mkp = TestScenario::remove_object<Marketplace>(scenario);
let listing = TestScenario::remove_nested_object<Marketplace, Listing<NFT<KITTY>, GAS>>(scenario, &mkp);
let payment = Coin::withdraw(&mut coin, 100, TestScenario::ctx(scenario));

// Do the buy call and expect successful purchase.
let nft = Marketplace::buy<NFT<KITTY>, GAS>(&mut mkp, listing, payment);
let kitten = NFT::burn<KITTY>(nft);

assert!(kitten.id == 1, 0);

TestScenario::return_object(scenario, mkp);
TestScenario::return_object(scenario, coin);
};
}

#[test]
#[expected_failure(abort_code = 0)]
fun fail_to_buy() {
let scenario = &mut TestScenario::begin(&ADMIN);

create_marketplace(scenario);
mint_some_coin(scenario);
mint_kitty(scenario);
list_kitty(scenario);

// BUYER takes 100 GAS from his wallet and purchases KITTY.
TestScenario::next_tx(scenario, &BUYER);
{
let coin = TestScenario::remove_object<Coin<GAS>>(scenario);
let mkp = TestScenario::remove_object<Marketplace>(scenario);
let listing = TestScenario::remove_nested_object<Marketplace, Listing<NFT<KITTY>, GAS>>(scenario, &mkp);

// AMOUNT here is 10 while expected is 100.
let payment = Coin::withdraw(&mut coin, 10, TestScenario::ctx(scenario));

// Attempt to buy and expect failure purchase.
let nft = Marketplace::buy<NFT<KITTY>, GAS>(&mut mkp, listing, payment);
let _ = NFT::burn<KITTY>(nft);

TestScenario::return_object(scenario, mkp);
TestScenario::return_object(scenario, coin);
};
}
}
2 changes: 1 addition & 1 deletion sui_programmability/framework/sources/Transfer.move
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ module Sui::Transfer {
/// Similar to transfer_to_object, to transfer an object to another object.
/// However it does not return the ChildRef. This can be unsafe to use since there is
/// no longer guarantee that the ID stored in the parent actually represent ownership.
public(friend) fun transfer_to_object_unsafe<T: key, R: key>(obj: T, owner: &mut R) {
public fun transfer_to_object_unsafe<T: key, R: key>(obj: T, owner: &mut R) {
let ChildRef { child_id: _ } = transfer_to_object(obj, owner);
}

Expand Down
4 changes: 3 additions & 1 deletion sui_programmability/framework/src/natives/test_scenario.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ fn get_inventory_for(
// TODO: We should also be able to include objects indirectly owned by the
// requested address through owning other objects.
// https://github.com/MystenLabs/sui/issues/673
if (obj.owner == Owner::AddressOwner(sui_addr) || obj.owner.is_shared())
if (obj.owner == Owner::AddressOwner(sui_addr)
|| obj.owner == Owner::ObjectOwner(sui_addr)
|| obj.owner.is_shared())
&& &obj.type_ == type_
{
Some(obj.value)
Expand Down

0 comments on commit d4ecd15

Please sign in to comment.