--- title: 'Documentation for custom marketplaces built on Metaplex' disqus: hackmd --- Metaplex-powered Fixed Price and Highest Bid NFT Sales for Marketplaces === ## Table of Contents [TOC] ## Some Docs and Resources * Solana Developer Docs: https://docs.solana.com/ * SPL Token: https://spl.solana.com/token * Metaplex Docs: https://docs.metaplex.com/ * Metaplex Github: https://github.com/metaplex-foundation/metaplex * Metaplex Developer Guide: https://www.notion.so/Metaplex-Developer-Guide-afefbc19841744c28587ab948a08cfac ## Overview: Sale Types and Basic Concepts As of date (May 2022) two major sale types currently supported are: * **Prints Transfer** (roughly refers to `PrintingV2`) * **Full Rights Transfer** ### Prints Ownership Transfer since rights to create prints are tokenized itself, the owner of the master edition can distribute tokens that allow users to create prints from master editions. A notable difference of *master editions* from *editions* is that **the artwork remains visible in the original owner's wallet as a master edition**, still after **its prints have been moved to the buyer's wallets**. Note, that in this case **the buyer will not be able to modify the metadata** of limited edition NFTs to be different than their master edition's metadata, e.g. set a different image uri and name. ### Full Rights Ownership Transfer Full rights ownership transfer is master edition NFT ownership transfer which additionally implies setting`updateAuthority` on the NFT Metadata to the effect of the **buyer acquiring all the rights and privileges associated with the original owner** (including the right **to modify metadata**, mint new prints, etc) :::info Metaplex's **non-fungible-token standard** is a part of the Solana Program Library (SPL). The basic definition of an NFT as a unique token with a fixed supply of 1 and 0 decimals was further extended on Solana to include **additional metadata** such as URI as defined in ERC-721 on Ethereum. ::: :::info A **master edition token**, when minted, represents both a non-fungible token on Solana and **metadata that allows creators to control the provenance of prints** created from the master edition. ::: :::info A **print** represents a **copy of an NFT**, and is created from a Master Edition. Each print has an **edition number** associated with it. ::: >See more details in official metaplex documentation here: https://docs.metaplex.com/architecture/deep_dive/metaplex#types-of-token-sales Basic Sale Flows (Fixed Price Sale) --- ```sequence Note left of Seller: lists NFT resource Seller->Vault: Transfer NFT ownership on listing Note right of Buyer: claims NFT resource Vault->Buyer: Transfer token ownership on claiming Buyer->Escrow: Transfer price lamports Escrow-Seller: Transfer royalties ``` Stored On-Chain Data --- ### NFT Resource A tokenized NFT resource (either a single token or a collection) built on Metaplex protocol. :::warning Note that from the sale types mentioned (print ownership transfer and full rights ownership transfer) follows that **only master edition tokens** are accepted as valid NFT resources for sales. ::: >Size: 49 bytes ```rs= pub struct NFTResource { /// Mint account for selling NFT Resource pub token_mint: Pubkey, /// Available supply pub supply: u64, /// Max available supply pub max_supply: Option<u64>, } ``` ### Market Data Represents market-specific configuration data bound to an NFT resource for sales. >Size: 177 bytes ```rs= pub struct MarketData { /// Pubkey of the authority with permission to modify market pub authority: Pubkey, /// Name within the range of 32 bytes pub name: Option<MarketName>, /// A resource for sales the market is bound to pub resource: Pubkey, /// Whether or not market data is mutable pub mutable: bool, /// Price value pub price: u64, /// Optional end price for OpenSea-like declining-price listings pub end_price: Option<u64>, /// Optional start date pub start_date: Option<u64>, /// Optional end date (should be calculated from the client-provided 'Duration' field) pub end_date: Option<u64>, /// Aggregated funds to calculate royalties on withdrawal pub funds_collected: u64, /// Optional buyer's pubkey entitled to purchase NFT as soon as it is listed pub reserved_buyer: Option<Pubkey>, } ``` Instructions --- ### CreateMarket Creates a new market account to hold `MarketData` configuaration bound to an NFT resource for sales. #### Instruction Accounts ```rs= /// Create a new market account bound to an NFT selling resource. /// 0. `[signer]` The account of the market creater/owner, authorised to make changes /// 1. `[writable]` Uninitialized market account /// 2. `[]` Mint account of the NFT resource being put on market /// 3. `[]` Rent sysvar /// 4. `[]` System account CreateMarket(CreateMarketArgs), ``` #### Instruction Data ```rs pub struct CreateMarketArgs { /// Pubkey of the authority with permission to modify market pub authority: Pubkey, /// NFT resource bound to market account pub resource: Pubkey, /// Whether or not market data is mutable pub mutable: bool, /// Price value pub price: u64, /// Optional end price for OpenSea-like declining-price listings pub end_price: Option<u64>, /// Optional start date pub start_date: Option<u64>, /// Optional end date (should be calculated from the client-provided 'Duration' field) pub end_date: Option<u64>, /// Optional buyer's pubkey entitled to purchase NFT as soon as it is listed pub reserved_buyer: Option<Pubkey>, /// Name within the range of 32 bytes pub name: Option<MarketName>, } ``` #### Errors and Limitations * `name` should be within the range of **32 bytes** * `start_date` if provided **should not be in the past** * If both `start_date` and `end_date` were provided `end_date` **should not precede** `start_date` * **Market account address** should be derived with the following seeds: ```rs= &[ PREFIX.as_bytes(), &program_id.as_ref(), args.resource.as_ref(), ], ``` `PREFIX` - constant program prefix string `resource` - account address holding NFT resource for sales ### UpdateMarket #### Instruction Accounts ```rs= ``` #### Instruction Data ```rs= ``` #### Errors and Limitations ### ListNFT #### Instruction Accounts Lists NFT resource for sales: * Creates NFT resource account (to store `NFTResource` data) * Transfers master edition ownership from source token account (seller's token account) to vault token account ```rs= /// List NFT resource (implies NFT ME ownership transfer from the seller's token account /// to vault token account). /// 0. `[signer]` Authorised account /// 1. `[writable]` Uninitialized market account /// 2. `[]` NFT resource account put on market (holding market-relevant NFT data) /// 3. `[]` Vault token account to transfer Master Edition ownership to /// 4. `[]` The metadata account, storing information about NFT resource put on market /// 5. `[]` Master Edition account of the NFT resource /// 6. `[]` Source token account to transfer Master Edition ownership from /// 7. `[]` SPL Token Program /// 8. `[]` Rent sysvar /// 9. `[]` System account ListNFT(ListNFTArgs), ``` #### Instruction Data ```rs= pub struct ListNFTArgs { /// Mint account of the NFT resource pub mint: Pubkey, /// Supply provided on listing pub supply: u64, /// Optional max supply provided on listing, should not exceed available supply: /// i.e. ME max_supply - ME supply pub max_supply: Option<u64>, } ``` #### Errors and Limitations * if `primary_sale_happened` of the metadata account has not been set (which serves as an indicator of the post-primary sales) - **at least one creator should be listed** among `metadata.creators` * `max_supply` provided on listing **should not exceed available supply** i.e. `max_supply` of master edition account without master edition `supply` of master edition: `master_edition.max_supply - master_edition.supply` ### ClaimNFT #### Instruction Accounts Enables the buyer to 'claim NFT resource': * Updates `primary_sale_happened` flag in metadata * Transfers token ownership from vault token account to destination token account (buyer's token account) ```rs= /// Transfers token ownership from vault token account to destination token account /// (buyer's token account). /// 0. `[]` Vault token account to transfer Master Edition ownership from /// 1. `[]` Owner of the vault token account /// 2. `[]` Treasury holder/escrow account /// 3. `[writable]` The metadata account, storing information about NFT resource put on market /// 4. `[]` Destination (buyer's) NFT token account ClaimResource(ClaimResourceArgs), ``` :::info `UpdatePrimarySaleHappenedViaToken` - allows updating the primary sale boolean on Metadata solely through owning an account with a token from the metadata's mint and signing the transaction. ::: #### Instruction Data ```rs= pub struct ClaimResourceArgs { /// NFT resource for sales pub resource: Pubkey, /// Mint account of the NFT resource pub mint: Pubkey, } ``` #### Errors and Limitations * Treasury holder **balance should be zero** prior to claiming NFT resource for sale * **Vault owner account address** should be derived with the following seeds: ```rs= &[ VAULT_OWNER_PREFIX.as_bytes(), program_id.as_ref(), args.resource.as_ref(), vault.as_ref(), ], ``` `VAULT_OWNER_PREFIX` - constant prefix string `resource` - account address holding NFT resource for sales `vault` - vault account to transfer ownership from ### BuyNFT Transfers lamports from the buyer's wallet to escrow account: * Transfers `market.price` lamports from the source token account (buyer's token account) to that of the treasury holder/escrow * Updates `market.funds_collected` * Mints new edition from master edition for NFT resource for sale * Updates `primary_sale_happened` flag in the new metadata account * Updates `resource.supply` accordingly #### Instruction Accounts ```=rs /// Transfers `market.price` lamports from the source token account (buyer's token account) /// to that of treasury holder/escrow and mints new edition from NFT master edition. /// 0. `[writable]` Market account bound to NFT resource for sale /// 1. `[writable]` NFT resource for sale /// 2. `[]` Source (i.e. buyer's) token account to transfer `market.price` amount of lamports from /// 3. `[signer]` Authority (i.e. buyer) for spl token transfer from source to escrow account /// and a new mint authority for new minted NFT print /// 4. `[]` Treasury holder/escrow account to transfer `market.price` amount of lamports to /// 5. `[writable]` New metadata account for minted NFT print /// 6. `[]` New edition account for minted NFT resource copy /// 7. `[writable]` Master edition account of the NFT resource for sale /// 8. `[writable]` New mint account to mint new edition from master edition /// 9. `[writable]` Edition marker account to mint new edition /// 10. `[writable]` Token account to mint new edition from /// 11. `[]` Token owner account to mint new edition from /// 12. `[]` New token account /// 13. `[]` Master edition metadata account /// 14. `[]` SPL Token Program /// 15. `[]` Rent sysvar /// 16. `[]` System account BuyNFT(), ``` #### Errors and Limitations * `resource.supply` should not exceed `resource.max_supply` * Same errors and limitations apply as on * `spl_token::instruction::transfer` * `MintNewEditionFromMasterEditionViaToken` : `mpl_token_metadata::instruction::mint_new_edition_from_master_edition_via_token` ### BuyNFTWithAuthUpdate Transfers lamports from the buyer's wallet to escrow account and sets the corresponding new update authority in the NFT metadata (results in full rights master edition ownership transfer): * Transfers `market.price` lamports from the source token account (buyer's token account) to that of the treasury holder/escrow * Updates `market.funds_collected` * Sets update authority for NFT metadata to `user_wallet` * Updates `primary_sale_happened` flag in the new metadata account * Updates `resource.supply` accordingly #### Instruction Accounts ```rs= /// Transfers `market.price` lamports from the source token account (buyer's token account) /// to that of treasury holder/escrow and sets new update authority in NFT metadata. /// 0. `[writable]` Market account bound to NFT resource for sale /// 1. `[writable]` NFT resource for sale /// 2. `[signer]` An update authority with permission to modify the NFT metadata /// 3. `[]` Source (i.e. buyer's) token account to transfer `market.price` amount of lamports from /// 4. `[]` Authority (i.e. buyer) for spl token transfer from source to escrow account /// 5. `[writable]` Metadata account for NFT resource /// 6. `[]` Treasury holder/escrow account to transfer `market.price` amount of lamports to /// 7. `[]` SPL Token Program BuyNFTWithAuthUpdate(), ``` #### Errors and Limitations * `resource.supply` should not exceed `resource.max_supply` * Same errors and limitations apply as on * `spl_token::instruction::transfer` * `MintNewEditionFromMasterEditionViaToken` : `mpl_token_metadata::instruction::mint_new_edition_from_master_edition_via_token` ### DistributeRoyalties Transfers royalties from escrow/treasury holder account to the destination account: * Calculates shares and royalties according to the beneficiary party type (creator, market owner or both) * Creates the destination associated token account if needed * Transfers royalties to the destination associated token account * Creates a marker account on successful royalties transfer so as to prevent multiple withdrawals :::warning Note: * Should be called for each beneficiary party (could be a creator of the NFT resource, a `market.owner`, or both) * If the stated beneficiary is both a creator and a market owner he should receive both shares ::: #### Instruction Accounts ```rs= /// Transfers royalties from escrow/treasury holder account to destination account. /// Note: Destination associated token account should match the provided beneficiary address /// (receiving wallet address) and spl token mint address (otherwise, error is thrown). /// 0. `[]` Market account bound to NFT resource for sale /// 1. `[]` Metadata account for NFT resource /// 2. `[]` Escrow/treasury holder account to transfer royalties from /// 3. `[]` Authority account for royalties transfer transaction /// 4. `[]` Credit associated token account of the shareholder to receive royalties: will be created if empty /// 5. `[]` Debit account for associated token account creation and payout_ticket account creation /// 6. `[writable]` Uninitialized marker account to prevent multiple withdrawal: created once on each withdrawal /// 7. `[]` SPL Token Program /// 8. `[]` Rent sysvar /// 9. `[]` System account DistributeRoyalties(DistributeRoyaltiesArgs), } ``` #### Instruction Data ```rs= pub struct DistributeRoyaltiesArgs { // NFT resource for sale pub resource: Pubkey, // Receiving address (wallet address to be associated with the destination account) pub beneficiary: Pubkey, // Mint account for the new associated token account treasury_mint: Pubkey, } ``` #### Errors and Limitations * `beneficiary` pubkey (i.e. the receiving wallet) provided for the royalties withdrawal **should belong to either market owner or any of the creators** in the creators list * A given beneficiary is **apt for a single (one-time) royalties withdrawal only** * If `primary_sale_happened` of the metadata account **has not been set** (which serves as an indicator of the post-primary sales) - **at least one creator should be listed** among `metadata.creators` * **Credit associated token account** of the shareholder should be a **valid associated token account address for the given receiving wallet** (`beneficary`) and **mint** (`treasury_mint`) accounts -- see `spl_associated_token_account::get_associated_token_address` for details ### Shares and Royalties Calculation #### Primary Sale Revenue for each **creator** (corresponding creator's address listed as `beneficiary` wallet): $$ Rev[cr_{i}] = Total\frac{s_{i}}{100}, \sum_{i=0}^n s_{i} = 100 $$ where `n` - creators number, `s` - creator's share, `Total` - total funds collected #### Secondary Sale Revenue for each **creator** (corresponding creator's address listed as `beneficiary` wallet): $$ Rev[cr_{i}] = Total\frac{s_{i}}{100} \frac{P}{10000}, \sum_{i=0}^n s_{i} = 100, P \in \{1, 2, \dots,10000\} $$ where `n` - creators number, `s` - creator's share, `Total` - total funds collected Revenue for the **market owner** (market address listed as `beneficiary` wallet): $$ Rev[cr_{i}] = Total \frac{P}{10000}, P \in \{1, 2, \dots,10000\} $$ :::success #### Example Given: Total funds = 1000, share for the first creator = 60, second creator = 40 and `seller_fee_basis_points` = 250: first creator withdraws 15, second creator - 20, market - 25, i.e. 50 total fees added on top of the stated price. ::: :::info Market owner can withdraw royalties both as a creator and a market owner ::: ###### tags: `Documentation`