0x0::orderbook

Orderbook where bids are fungible tokens and asks are NFTs. A bid is a request to buy one NFT from a specific collection. An ask is one NFT with a min price condition.

One can

  • create a new orderbook between a given collection and a bid token;
  • set publicly accessible actions to be witness protected;
  • open a new bid;
  • cancel an existing bid they own;
  • offer an NFT if collection matches OB collection;
  • cancel an existing NFT offer;
  • instantly buy a specific NFT;
  • open bids and asks with a commission on behalf of a user;
  • trade both native and 3rd party collections.

Other resources

  • https://docs.originbyte.io/origin-byte/about-our-programs/liquidity-layer/orderbook
  • https://origin-byte.github.io/ob.html

Structs

orderbook::Witness has drop

Witness used to authenticate witness protected endpoints

orderbook::Orderbook<C, FT> has key

Fields:

Name Type Description
id object::UID
protected_actions orderbook::WitnessProtectedActions

Actions which have a flag set to true can only be called via a witness protected implementation.

asks crit_bit_u64::CB<vector<orderbook::Ask>>

An ask order stores an NFT to be traded. The price associated with such an order is saying:

for this NFT, I want to receive at least this amount of FT.

bids crit_bit_u64::CB<vector<orderbook::Bid<FT>>>

A bid order stores amount of tokens of type "B"(id) to trade. A bid order is saying:

for any NFT in this collection, I will spare this many tokens

A critbit order book implementation. Contains two ordered trees:

  1. bids ASC
  2. asks DESC

orderbook::WitnessProtectedActions has drop, store

Fields:

Name Type Description
buy_nft bool
cancel_ask bool
cancel_bid bool
create_ask bool
create_bid bool

The contract which creates the orderbook can restrict specific actions to be only callable with a witness pattern and not via the entry point function.

This means contracts can build on top of this orderbook their custom logic if they desire so, or they can just use the entry point functions which might be good enough for most use cases.

Important

If a method is protected, then clients call instead of the relevant endpoint in the orderbook a standardized endpoint in the witness-owning smart contract.

Another way to think about this from marketplace or wallet POV: If I see that an action is protected, I can decide to either call the downstream implementation in the collection smart contract, or just not enable to perform that specific action at all.

orderbook::Bid<FT> has store

Fields:

Name Type Description
offer balance::Balance<FT>

How many token are being offered by the order issuer for one NFT.

owner address

The address of the user who created this bid and who will receive an NFT in exchange for their tokens.

safe object::ID

Points to Safe shared object into which to deposit NFT.

commission option::Option<trading::BidCommission<FT>>

If the NFT is offered via a marketplace or a wallet, the facilitator can optionally set how many tokens they want to claim on top of the offer.

An offer for a single NFT in a collection.

orderbook::Ask has store

Fields:

Name Type Description
price u64

How many tokens does the seller want for their NFT in exchange.

transfer_cap safe::TransferCap

Capability to get an NFT from a safe.

owner address

Who owns the NFT.

commission option::Option<trading::AskCommission>

If the NFT is offered via a marketplace or a wallet, the facilitator can optionally set how many tokens they want to claim from the price of the NFT for themselves as a commission.

Object which is associated with a single NFT.

When [Ask] is created, we transfer the ownership of the NFT to this new object. When an ask is matched with a bid, we transfer the ownership of the [Ask] object to the bid owner (buyer). The buyer can then claim the NFT via [claim_nft] endpoint.

orderbook::TradeIntermediate<C, FT> has key

Fields:

Name Type Description
id object::UID
transfer_cap option::Option<safe::TransferCap>

in option bcs we want to extract it but cannot destroy shared obj in Sui yet

https://github.com/MystenLabs/sui/issues/2083

seller address
buyer address
buyer_safe object::ID
paid balance::Balance<FT>
commission option::Option<trading::AskCommission>

TradeIntermediate is made a shared object and can be called permissionlessly.

orderbook::OrderbookCreatedEvent has copy, drop

Fields:

Name Type Description
orderbook object::ID
nft_type ascii::String
ft_type ascii::String

orderbook::AskCreatedEvent has copy, drop

Fields:

Name Type Description
nft object::ID
orderbook object::ID
owner address
price u64
safe object::ID
nft_type ascii::String
ft_type ascii::String

orderbook::AskClosedEvent has copy, drop

Fields:

Name Type Description
nft object::ID
orderbook object::ID
owner address
price u64
nft_type ascii::String
ft_type ascii::String

When de-listed, not when sold!

orderbook::BidCreatedEvent has copy, drop

Fields:

Name Type Description
orderbook object::ID
owner address
price u64
safe object::ID
nft_type ascii::String
ft_type ascii::String

orderbook::BidClosedEvent has copy, drop

Fields:

Name Type Description
orderbook object::ID
owner address
safe object::ID
price u64
nft_type ascii::String
ft_type ascii::String

When de-listed, not when bought!

orderbook::TradeFilledEvent has copy, drop

Fields:

Name Type Description
buyer_safe object::ID
buyer address
nft object::ID
orderbook object::ID
price u64
seller_safe object::ID
seller address
trade_intermediate option::Option<object::ID>

Is None if the NFT was bought directly (buy_nft or buy_generic_nft.)

Is Some if the NFT was bought via create_bid or create_ask.

nft_type ascii::String
ft_type ascii::String

Either an ask is created and immediately matched with a bid, or a bid is created and immediately matched with an ask. In both cases [TradeFilledEvent] is emitted. In such case, the property trade_intermediate is Some.

If the NFT was bought directly (buy_nft or buy_generic_nft), then the property trade_intermediate is None.

Methods

public entry fun create_bid<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    buyer_safe: &mut safe::Safe,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

How many (price) fungible tokens should be taken from sender's wallet and put into the orderbook with the intention of exchanging them for 1 NFT.

If the price is higher than the lowest ask requested price, then we execute a trade straight away. In such a case, a new shared object [TradeIntermediate] is created. Otherwise we add the bid to the orderbook's state.

The client provides the Safe into which they wish to receive an NFT.

public fun create_bid_protected<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    buyer_safe: &mut safe::Safe,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Same as [create_bid] but protected by collection witness.

public entry fun create_safe_and_bid<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Same as [create_bid] but creates a new safe for the sender first

public entry fun create_bid_with_commission<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    buyer_safe: &mut safe::Safe,
    price: u64,
    beneficiary: address,
    commission_ft: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Same as [create_bid] but with a commission.

public fun create_bid_with_commission_protected<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    buyer_safe: &mut safe::Safe,
    price: u64,
    beneficiary: address,
    commission_ft: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Same as [create_bid_protected] but with a commission.

public entry fun create_safe_and_bid_with_commission<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    price: u64,
    beneficiary: address,
    commission_ft: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Same as [create_safe_and_bid] but with a commission.

public entry fun cancel_bid<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    bid_price_level: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Cancel a bid owned by the sender at given price. If there are two bids with the same price, the one created later is cancelled.

public fun cancel_bid_protected<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    bid_price_level: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Same as [cancel_bid] but protected by collection witness.

public entry fun create_ask<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    requested_tokens: u64,
    transfer_cap: safe::TransferCap,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Offer given NFT to be traded for given (requested_tokens) tokens. If there exists a bid with higher offer than requested_tokens, then trade is immediately executed. In such a case, a new shared object [TradeIntermediate] is created. Otherwise the transfer cap is stored in the orderbook.

public entry fun list_nft<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    requested_tokens: u64,
    nft: object::ID,
    owner_cap: &safe::OwnerCap,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Creates exclusive transfer cap and then calls [create_ask].

public entry fun list_multiple_nfts<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nfts: vector<object::ID>,
    prices: vector<u64>,
    owner_cap: &safe::OwnerCap,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Provide list of NFTs and corresponding prices (index # match.)

The NFTs must be deposited in the seller's safe.

Panics

  • If nfts and prices have different lengths
  • If nfts is empty

public entry fun deposit_and_list_nft<T: store + key, C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft: T,
    requested_tokens: u64,
    owner_cap: &safe::OwnerCap,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

  1. Deposits an NFT to safe
  2. Calls [list_nft]

The type T in case of OB collections is Nft<C>. In case of generic collections C == T.

This endpoint is useful mainly for generic collections, because NFTs of OB usually live in a safe in the first place.

public entry fun create_safe_and_deposit_and_list_nft<T: store + key, C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft: T,
    requested_tokens: u64,
    ctx: &mut tx_context::TxContext,
)

  1. Creates a new safe for the sender
  2. Calls [deposit_and_list_nft]

public fun create_ask_protected<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    requested_tokens: u64,
    transfer_cap: safe::TransferCap,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [create_ask] but protected by collection witness.

public entry fun create_ask_with_commission<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    requested_tokens: u64,
    transfer_cap: safe::TransferCap,
    beneficiary: address,
    commission: u64,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [create_ask] but with a commission.

public entry fun list_nft_with_commission<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    requested_tokens: u64,
    nft: object::ID,
    owner_cap: &safe::OwnerCap,
    beneficiary: address,
    commission: u64,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [list_nft] but with a commission.

public entry fun list_multiple_nfts_with_commission<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nfts: vector<object::ID>,
    prices: vector<u64>,
    beneficiary: address,
    commissions: vector<u64>,
    owner_cap: &safe::OwnerCap,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [list_multiple_nfts] but with a commission.

The commission is a vector which is associated with the NFTs by index.

Panics

If the commissions length does not match the NFTs length.

public entry fun deposit_and_list_nft_with_commission<T: store + key, C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft: T,
    requested_tokens: u64,
    owner_cap: &safe::OwnerCap,
    beneficiary: address,
    commission: u64,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [deposit_and_list_nft_with] but with a commission.

public entry fun create_safe_and_deposit_and_list_nft_with_commission<T: store + key, C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft: T,
    requested_tokens: u64,
    beneficiary: address,
    commission: u64,
    ctx: &mut tx_context::TxContext,
)

Same as [create_safe_and_deposit_and_list_nft] but with a commission.

public fun create_ask_with_commission_protected<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    requested_tokens: u64,
    transfer_cap: safe::TransferCap,
    beneficiary: address,
    commission: u64,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [create_ask_protected] but with a commission.

Panics

The commission arg must be less than requested_tokens.

public entry fun cancel_ask<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft_price_level: u64,
    nft_id: object::ID,
    ctx: &mut tx_context::TxContext,
)

To cancel an offer on a specific NFT, the client provides the price they listed it for. The [TransferCap] object is transferred back to the tx sender.

public fun cancel_ask_protected<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    nft_price_level: u64,
    nft_id: object::ID,
    ctx: &mut tx_context::TxContext,
)

Same as [cancel_ask] but protected by collection witness.

public entry fun cancel_ask_and_discard_transfer_cap<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft_price_level: u64,
    nft_id: object::ID,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [cancel_ask] but the [TransferCap] is burned instead of transferred back to the tx sender.

public entry fun edit_ask<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    old_price: u64,
    nft_id: object::ID,
    new_price: u64,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Removes the old ask and creates a new one with the same NFT. Two events are emitted at least: Firstly, we always emit AskRemovedEvent for the old ask. Then either AskCreatedEvent or TradeFilledEvent. Depends on whether the ask is filled immediately or not.

public entry fun edit_bid<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    buyer_safe: &mut safe::Safe,
    old_price: u64,
    new_price: u64,
    wallet: &mut coin::Coin<FT>,
    ctx: &mut tx_context::TxContext,
)

Cancels the old bid and creates a new one with new price.

public entry fun buy_nft<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft_id: object::ID,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    seller_safe: &mut safe::Safe,
    buyer_safe: &mut safe::Safe,
    allowlist: &transfer_allowlist::Allowlist,
    ctx: &mut tx_context::TxContext,
)

To buy a specific NFT listed in the orderbook, the client provides the price for which the NFT is listed.

The NFT is transferred from the seller's Safe to the buyer's Safe.

In this case, it's important to provide both the price and NFT ID to avoid actions such as offering an NFT for a really low price and then quickly changing the price to a higher one.

The provided [Coin] wallet is used to pay for the NFT.

The whitelist is used to check if the orderbook is authorized to trade the collection at all.

This endpoint does not create a new [TradeIntermediate], rather performs he transfer straight away.

public entry fun create_safe_and_buy_nft<C, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft_id: object::ID,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    seller_safe: &mut safe::Safe,
    allowlist: &transfer_allowlist::Allowlist,
    ctx: &mut tx_context::TxContext,
)

  1. Creates a new [Safe] for the sender
  2. Buys the NFT into this new safe
  3. Shares the safe and gives the owner cap to sender

public entry fun buy_generic_nft<C: store + key, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft_id: object::ID,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    seller_safe: &mut safe::Safe,
    buyer_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Similar to [buy_nft] except that this is meant for generic collections, ie. those which aren't native to our protocol.

public entry fun create_safe_and_buy_generic_nft<C: store + key, FT>(
    book: &mut orderbook::Orderbook<C, FT>,
    nft_id: object::ID,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    seller_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

  1. Creates a new [Safe] for the sender
  2. Buys the NFT into this new safe
  3. Shares the safe and gives the owner cap to sender

public fun buy_nft_protected<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    nft_id: object::ID,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    seller_safe: &mut safe::Safe,
    buyer_safe: &mut safe::Safe,
    allowlist: &transfer_allowlist::Allowlist,
    ctx: &mut tx_context::TxContext,
)

Same as [buy_nft] but protected by collection witness.

public fun buy_generic_nft_protected<W: drop, C: store + key, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
    nft_id: object::ID,
    price: u64,
    wallet: &mut coin::Coin<FT>,
    seller_safe: &mut safe::Safe,
    buyer_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Same as [buy_generic_nft] but protected by collection witness.

public entry fun finish_trade<C, FT>(
    trade: &mut orderbook::TradeIntermediate<C, FT>,
    seller_safe: &mut safe::Safe,
    buyer_safe: &mut safe::Safe,
    allowlist: &transfer_allowlist::Allowlist,
    ctx: &mut tx_context::TxContext,
)

When a bid is created and there's an ask with a lower price, then the trade cannot be resolved immediately.

That's because we don't know the Safe ID up front in OB.

Therefore, orderbook creates [TradeIntermediate] which then has to be permissionlessly resolved via this endpoint.

public entry fun finish_trade_of_generic_nft<C: store + key, FT>(
    trade: &mut orderbook::TradeIntermediate<C, FT>,
    seller_safe: &mut safe::Safe,
    buyer_safe: &mut safe::Safe,
    ctx: &mut tx_context::TxContext,
)

Similar to [finish_trade] except that this is meant for generic collections, ie. those which aren't native to our protocol.

public fun new<C, FT>(
    protected_actions: orderbook::WitnessProtectedActions,
    ctx: &mut tx_context::TxContext,
): orderbook::Orderbook<C, FT>

Collection kind of NFTs to be traded, and Fungible Token to be quoted for an NFT in such a collection.

By default, an orderbook has no restriction on actions, ie. all can be called with public entry functions.

To implement specific logic in your smart contract, you can toggle the protection on specific actions. That will make them only accessible via witness protected methods.

public fun new_unprotected<C, FT>(
    ctx: &mut tx_context::TxContext,
): orderbook::Orderbook<C, FT>

Returns a new orderbook without any protection, ie. all endpoints can be called as entry points.

public fun new_with_protected_actions<C, FT>(
    protected_actions: orderbook::WitnessProtectedActions,
    ctx: &mut tx_context::TxContext,
): orderbook::Orderbook<C, FT>

public entry fun create<C, FT>(
    ctx: &mut tx_context::TxContext,
)

Creates a new empty orderbook as a shared object.

All actions can be called as entry points.

public fun share<C, FT>(
    ob: orderbook::Orderbook<C, FT>,
)

public fun no_protection(): orderbook::WitnessProtectedActions

Settings where all endpoints can be called as entry point functions.

public fun custom_protection(
    buy_nft: bool,
    cancel_ask: bool,
    cancel_bid: bool,
    create_ask: bool,
    create_bid: bool,
): orderbook::WitnessProtectedActions

public fun set_protection<W: drop, C, FT>(
    _witness: W,
    ob: &mut orderbook::Orderbook<C, FT>,
    protected_actions: orderbook::WitnessProtectedActions,
)

public fun toggle_protection_on_buy_nft<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
)

public fun toggle_protection_on_cancel_ask<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
)

public fun toggle_protection_on_cancel_bid<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
)

public fun toggle_protection_on_create_ask<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
)

public fun toggle_protection_on_create_bid<W: drop, C, FT>(
    _witness: W,
    book: &mut orderbook::Orderbook<C, FT>,
)

public fun borrow_bids<C, FT>(
    book: &orderbook::Orderbook<C, FT>,
): 
    &crit_bit_u64::CB<vector<orderbook::Bid<FT>>>

public fun bid_offer<FT>(bid: &orderbook::Bid<FT>): 
    &balance::Balance<FT>

public fun bid_owner<FT>(bid: &orderbook::Bid<FT>): 
    address

public fun borrow_asks<C, FT>(
    book: &orderbook::Orderbook<C, FT>,
): &crit_bit_u64::CB<vector<orderbook::Ask>>

public fun ask_price(ask: &orderbook::Ask): u64

public fun ask_nft(ask: &orderbook::Ask): &safe::TransferCap

public fun ask_owner(ask: &orderbook::Ask): address

public fun protected_actions<C, FT>(
    book: &orderbook::Orderbook<C, FT>,
): &orderbook::WitnessProtectedActions

public fun is_create_ask_protected(
    protected_actions: &orderbook::WitnessProtectedActions,
): bool

public fun is_create_bid_protected(
    protected_actions: &orderbook::WitnessProtectedActions,
): bool

public fun is_cancel_ask_protected(
    protected_actions: &orderbook::WitnessProtectedActions,
): bool

public fun is_cancel_bid_protected(
    protected_actions: &orderbook::WitnessProtectedActions,
): bool

public fun is_buy_nft_protected(
    protected_actions: &orderbook::WitnessProtectedActions,
): bool