Merge pull request #32 from metaplex-foundation/various-patches

History doesn't repeat itself, but it does rhyme.
This commit is contained in:
B 2021-06-12 12:28:46 -05:00 committed by GitHub
commit b57a335552
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 26753 additions and 3 deletions

View File

@ -44,7 +44,7 @@
"test": "craco test",
"eject": "react-scripts eject",
"deploy:ar": "arweave deploy-dir ../../build/web --key-file ",
"deploy": "gh-pages -d ../../build/web --repo https://github.com/solana-labs/oyster-meta",
"deploy": "gh-pages -d ../../build/web --repo https://github.com/metaplex-foundation/metaplex",
"format:fix": "prettier --write \"**/*.+(js|jsx|ts|tsx|json|css|md)\""
},
"eslintConfig": {
@ -64,7 +64,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/solana-labs/oyster"
"url": "https://github.com/metaplex-foundation/metaplex"
},
"homepage": ".",
"devDependencies": {

View File

@ -10,7 +10,7 @@ export const Footer = () => {
<Button
shape={'circle'}
target={'_blank'}
href={'https://github.com/solana-labs/oyster'}
href={'https://github.com/metaplex-foundation/metaplex'}
icon={<GithubOutlined />}
style={{ marginRight: '20px' }}
></Button>

3603
rust/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
rust/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[workspace]
members = [
"auction/program",
"metaplex/program",
"token-vault/program",
"token-metadata/program",
]
exclude = [
]

View File

@ -0,0 +1,22 @@
[package]
name = "spl-auction-test-client"
version = "0.1.0"
description = "Metaplex Library Auction Test Client"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
publish = false
[dependencies]
bincode = "1.3.2"
borsh = "0.8.2"
clap = "2.33.3"
rand = "*"
solana-clap-utils = "1.6"
solana-cli-config = "1.6"
solana-client = "1.6.10"
solana-program = "1.6.10"
solana-sdk = "1.6.10"
spl-auction = { path = "../program", features = [ "no-entrypoint" ] }
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }

1170
rust/auction/cli/src/main.rs Normal file

File diff suppressed because it is too large Load Diff

3497
rust/auction/program/Cargo.lock generated Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
[package]
name = "spl-auction"
version = "0.0.1"
description = "Solana Auction Program"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
exclude = ["tests/**"]
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
borsh = "0.8.2"
num-derive = "0.3"
num-traits = "0.2"
solana-program = "1.6.10"
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }
thiserror = "1.0"
[dev-dependencies]
solana-program-test = "1.6.10"
solana-sdk = "1.6.10"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,24 @@
---
title: Auction Program
---
## Background
Solana's programming model and the definitions of the Solana terms used in this
document are available at:
- https://docs.solana.com/apps
- https://docs.solana.com/terminology
## Source
The Auction Program's source is available on
[github](https://github.com/metaplex-foundation/metaplex)
## Interface
TODO
## Operational Overview
TODO

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,23 @@
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
use {
crate::{errors::AuctionError, processor},
solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,
program_error::PrintProgramError, pubkey::Pubkey,
},
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) {
error.print::<AuctionError>();
msg!("{}", error);
return Err(error);
}
Ok(())
}

View File

@ -0,0 +1,143 @@
use {
num_derive::FromPrimitive,
solana_program::{
decode_error::DecodeError,
msg,
program_error::{PrintProgramError, ProgramError},
},
thiserror::Error,
};
/// Errors that may be returned by the Auction program.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum AuctionError {
/// Account does not have correct owner
#[error("Account does not have correct owner")]
IncorrectOwner,
/// Lamport balance below rent-exempt threshold.
#[error("Lamport balance below rent-exempt threshold")]
NotRentExempt,
/// Bid account provided does not match the derived address.
#[error("Bid account provided does not match the derived address.")]
InvalidBidAccount,
/// Auction account specified is invalid.
#[error("Auction account specified is invalid.")]
InvalidAuctionAccount,
/// Balance too low to make bid.
#[error("Balance too low to make bid.")]
BalanceTooLow,
/// Auction is not currently running.
#[error("Auction is not currently running.")]
InvalidState,
/// Bid is too small.
#[error("Bid is too small.")]
BidTooSmall,
/// Invalid transition, auction state may only transition: Created -> Started -> Stopped
#[error("Invalid auction state transition.")]
AuctionTransitionInvalid,
/// Failed to derive an account from seeds.
#[error("Failed to derive an account from seeds.")]
DerivedKeyInvalid,
/// Token transfer failed
#[error("Token transfer failed")]
TokenTransferFailed,
/// Token mint to failed
#[error("Token mint to failed")]
TokenMintToFailed,
/// Token burn failed
#[error("Token burn failed")]
TokenBurnFailed,
/// Invalid authority
#[error("Invalid authority")]
InvalidAuthority,
/// Authority not signer
#[error("Authority not signer")]
AuthorityNotSigner,
/// Numerical overflow
#[error("Numerical overflow")]
NumericalOverflowError,
/// Bidder pot token account does not match
#[error("Bidder pot token account does not match")]
BidderPotTokenAccountOwnerMismatch,
/// Uninitialized
#[error("Uninitialized")]
Uninitialized,
/// Metadata account is missing or invalid.
#[error("Metadata account is missing or invalid.")]
MetadataInvalid,
/// Bidder pot is missing, and required for SPL trades.
#[error("Bidder pot is missing, and required for SPL trades.")]
BidderPotDoesNotExist,
/// Existing Bid is already active.
#[error("Existing Bid is already active.")]
BidAlreadyActive,
/// Incorrect mint specified, must match auction.
#[error("Incorrect mint specified, must match auction.")]
IncorrectMint,
/// Must reveal price when ending a blinded auction.
#[error("Must reveal price when ending a blinded auction.")]
MustReveal,
/// The revealing hash is invalid.
#[error("The revealing hash is invalid.")]
InvalidReveal,
/// The pot for this bid is already empty.
#[error("The pot for this bid is already empty.")]
BidderPotEmpty,
/// This is not a valid token program
#[error(" This is not a valid token program")]
InvalidTokenProgram,
/// Accept payment delegate should be none
#[error("Accept payment delegate should be none")]
DelegateShouldBeNone,
/// Accept payment close authority should be none
#[error("Accept payment close authority should be none")]
CloseAuthorityShouldBeNone,
/// Data type mismatch
#[error("Data type mismatch")]
DataTypeMismatch,
}
impl PrintProgramError for AuctionError {
fn print<E>(&self) {
msg!(&self.to_string());
}
}
impl From<AuctionError> for ProgramError {
fn from(e: AuctionError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for AuctionError {
fn type_of() -> &'static str {
"Vault Error"
}
}

View File

@ -0,0 +1,356 @@
use crate::{EXTENDED, PREFIX};
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
sysvar,
};
pub use crate::processor::{
cancel_bid::CancelBidArgs, claim_bid::ClaimBidArgs, create_auction::CreateAuctionArgs,
end_auction::EndAuctionArgs, place_bid::PlaceBidArgs, start_auction::StartAuctionArgs,
};
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub enum AuctionInstruction {
/// Cancel a bid on a running auction.
/// 0. `[signer]` The bidders primary account, for PDA calculation/transit auth.
/// 1. `[writable]` The bidders token account they'll receive refund with
/// 2. `[writable]` The pot, containing a reference to the stored SPL token account.
/// 3. `[writable]` The pot SPL account, where the tokens will be deposited.
/// 4. `[writable]` The metadata account, storing information about the bidders actions.
/// 5. `[writable]` Auction account, containing data about the auction and item being bid on.
/// 6. `[writable]` Token mint, for transfer instructions and verification.
/// 7. `[]` Clock sysvar
/// 8. `[]` Rent sysvar
/// 9. `[]` System program
/// 10. `[]` SPL Token Program
CancelBid(CancelBidArgs),
/// Create a new auction account bound to a resource, initially in a pending state.
/// 0. `[signer]` The account creating the auction, which is authorised to make changes.
/// 1. `[writable]` Uninitialized auction account.
/// 2. `[]` Rent sysvar
/// 3. `[]` System account
CreateAuction(CreateAuctionArgs),
/// Move SPL tokens from winning bid to the destination account.
/// 0. `[writable]` The destination account
/// 1. `[writable]` The bidder pot token account
/// 2. `[]` The bidder pot pda account [seed of ['auction', program_id, auction key, bidder key]]
/// 3. `[signer]` The authority on the auction
/// 4. `[]` The auction
/// 5. `[]` The bidder wallet
/// 6. `[]` Token mint of the auction
/// 7. `[]` Clock sysvar
/// 8. `[]` Token program
ClaimBid(ClaimBidArgs),
/// Ends an auction, regardless of end timing conditions
EndAuction(EndAuctionArgs),
/// Start an inactive auction.
/// 0. `[signer]` The creator/authorised account.
/// 1. `[writable]` Initialized auction account.
/// 2. `[]` Clock sysvar
StartAuction(StartAuctionArgs),
/// Update the authority for an auction account.
SetAuthority,
/// Place a bid on a running auction.
/// 0. `[signer]` The bidders primary account, for PDA calculation/transit auth.
/// 1. `[writable]` The bidders token account they'll pay with
/// 2. `[writable]` The pot, containing a reference to the stored SPL token account.
/// 3. `[writable]` The pot SPL account, where the tokens will be deposited.
/// 4. `[writable]` The metadata account, storing information about the bidders actions.
/// 5. `[writable]` Auction account, containing data about the auction and item being bid on.
/// 6. `[writable]` Token mint, for transfer instructions and verification.
/// 7. `[signer]` Transfer authority, for moving tokens into the bid pot.
/// 8. `[signer]` Payer
/// 9. `[]` Clock sysvar
/// 10. `[]` Rent sysvar
/// 11. `[]` System program
/// 12. `[]` SPL Token Program
PlaceBid(PlaceBidArgs),
}
/// Creates an CreateAuction instruction.
pub fn create_auction_instruction(
program_id: Pubkey,
creator_pubkey: Pubkey,
args: CreateAuctionArgs,
) -> Instruction {
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
args.resource.as_ref(),
];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
];
let (auction_extended_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(creator_pubkey, true),
AccountMeta::new(auction_pubkey, false),
AccountMeta::new(auction_extended_pubkey, false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
],
data: AuctionInstruction::CreateAuction(args)
.try_to_vec()
.unwrap(),
}
}
/// Creates an SetAuthority instruction.
pub fn set_authority_instruction(
program_id: Pubkey,
resource: Pubkey,
authority: Pubkey,
new_authority: Pubkey,
) -> Instruction {
let seeds = &[PREFIX.as_bytes(), &program_id.as_ref(), resource.as_ref()];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_pubkey, false),
AccountMeta::new_readonly(authority, true),
AccountMeta::new_readonly(new_authority, false),
],
data: AuctionInstruction::SetAuthority.try_to_vec().unwrap(),
}
}
/// Creates an StartAuction instruction.
pub fn start_auction_instruction(
program_id: Pubkey,
authority_pubkey: Pubkey,
args: StartAuctionArgs,
) -> Instruction {
// Derive Auction Key
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
args.resource.as_ref(),
];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(authority_pubkey, true),
AccountMeta::new(auction_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
],
data: AuctionInstruction::StartAuction(args).try_to_vec().unwrap(),
}
}
/// Creates an PlaceBid instruction.
pub fn place_bid_instruction(
program_id: Pubkey,
bidder_pubkey: Pubkey,
bidder_token_pubkey: Pubkey,
bidder_pot_token_pubkey: Pubkey,
token_mint_pubkey: Pubkey,
transfer_authority: Pubkey,
payer: Pubkey,
args: PlaceBidArgs,
) -> Instruction {
// Derive Auction Key
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
];
let (auction_extended_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
// Derive Bidder Pot
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
auction_pubkey.as_ref(),
bidder_pubkey.as_ref(),
];
let (bidder_pot_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
// Derive Bidder Meta
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
auction_pubkey.as_ref(),
bidder_pubkey.as_ref(),
"metadata".as_bytes(),
];
let (bidder_meta_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(bidder_pubkey, true),
AccountMeta::new(bidder_token_pubkey, false),
AccountMeta::new(bidder_pot_pubkey, false),
AccountMeta::new(bidder_pot_token_pubkey, false),
AccountMeta::new(bidder_meta_pubkey, false),
AccountMeta::new(auction_pubkey, false),
AccountMeta::new(auction_extended_pubkey, false),
AccountMeta::new(token_mint_pubkey, false),
AccountMeta::new_readonly(transfer_authority, true),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: AuctionInstruction::PlaceBid(args).try_to_vec().unwrap(),
}
}
/// Creates an CancelBidinstruction.
pub fn cancel_bid_instruction(
program_id: Pubkey,
bidder_pubkey: Pubkey,
bidder_token_pubkey: Pubkey,
bidder_pot_token_pubkey: Pubkey,
token_mint_pubkey: Pubkey,
args: CancelBidArgs,
) -> Instruction {
// Derive Auction Key
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
];
let (auction_extended_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
// Derive Bidder Pot
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
auction_pubkey.as_ref(),
bidder_pubkey.as_ref(),
];
let (bidder_pot_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
// Derive Bidder Meta
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
auction_pubkey.as_ref(),
bidder_pubkey.as_ref(),
"metadata".as_bytes(),
];
let (bidder_meta_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(bidder_pubkey, true),
AccountMeta::new(bidder_token_pubkey, false),
AccountMeta::new(bidder_pot_pubkey, false),
AccountMeta::new(bidder_pot_token_pubkey, false),
AccountMeta::new(bidder_meta_pubkey, false),
AccountMeta::new(auction_pubkey, false),
AccountMeta::new(auction_extended_pubkey, false),
AccountMeta::new(token_mint_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: AuctionInstruction::CancelBid(args).try_to_vec().unwrap(),
}
}
pub fn end_auction_instruction(
program_id: Pubkey,
authority_pubkey: Pubkey,
args: EndAuctionArgs,
) -> Instruction {
// Derive Auction Key
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
args.resource.as_ref(),
];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(authority_pubkey, true),
AccountMeta::new(auction_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
],
data: AuctionInstruction::EndAuction(args).try_to_vec().unwrap(),
}
}
pub fn claim_bid_instruction(
program_id: Pubkey,
destination_pubkey: Pubkey,
authority_pubkey: Pubkey,
bidder_pubkey: Pubkey,
bidder_pot_token_pubkey: Pubkey,
token_mint_pubkey: Pubkey,
args: ClaimBidArgs,
) -> Instruction {
// Derive Auction Key
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
args.resource.as_ref(),
];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
// Derive Bidder Pot
let seeds = &[
PREFIX.as_bytes(),
&program_id.as_ref(),
auction_pubkey.as_ref(),
bidder_pubkey.as_ref(),
];
let (bidder_pot_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
Instruction {
program_id,
accounts: vec![
AccountMeta::new(destination_pubkey, false),
AccountMeta::new(bidder_pot_token_pubkey, false),
AccountMeta::new(bidder_pot_pubkey, false),
AccountMeta::new_readonly(authority_pubkey, true),
AccountMeta::new_readonly(auction_pubkey, false),
AccountMeta::new_readonly(bidder_pubkey, false),
AccountMeta::new_readonly(token_mint_pubkey, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: AuctionInstruction::ClaimBid(args).try_to_vec().unwrap(),
}
}

View File

@ -0,0 +1,14 @@
#![allow(warnings)]
mod errors;
mod utils;
pub mod entrypoint;
pub mod instruction;
pub mod processor;
/// Prefix used in PDA derivations to avoid collisions with other programs.
pub const PREFIX: &str = "auction";
pub const EXTENDED: &str = "extended";
solana_program::declare_id!("auctxRXPeJoc4817jDhf4HbjnhEcr1cCXenosMhK5R8");

View File

@ -0,0 +1,463 @@
use crate::errors::AuctionError;
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::AccountInfo, borsh::try_from_slice_unchecked, clock::UnixTimestamp,
entrypoint::ProgramResult, hash::Hash, msg, program_error::ProgramError, pubkey::Pubkey,
};
use std::{cmp, mem};
// Declare submodules, each contains a single handler for each instruction variant in the program.
pub mod cancel_bid;
pub mod claim_bid;
pub mod create_auction;
pub mod end_auction;
pub mod place_bid;
pub mod set_authority;
pub mod start_auction;
// Re-export submodules handlers + associated types for other programs to consume.
pub use cancel_bid::*;
pub use claim_bid::*;
pub use create_auction::*;
pub use end_auction::*;
pub use place_bid::*;
pub use set_authority::*;
pub use start_auction::*;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
use crate::instruction::AuctionInstruction;
match AuctionInstruction::try_from_slice(input)? {
AuctionInstruction::CancelBid(args) => cancel_bid(program_id, accounts, args),
AuctionInstruction::ClaimBid(args) => claim_bid(program_id, accounts, args),
AuctionInstruction::CreateAuction(args) => create_auction(program_id, accounts, args),
AuctionInstruction::EndAuction(args) => end_auction(program_id, accounts, args),
AuctionInstruction::PlaceBid(args) => place_bid(program_id, accounts, args),
AuctionInstruction::SetAuthority => set_authority(program_id, accounts),
AuctionInstruction::StartAuction(args) => start_auction(program_id, accounts, args),
}
}
/// Structure with pricing floor data.
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub enum PriceFloor {
/// Due to borsh on the front end disallowing different arguments in enums, we have to make sure data is
/// same size across all three
/// No price floor, any bid is valid.
None([u8; 32]),
/// Explicit minimum price, any bid below this is rejected.
MinimumPrice([u64; 4]),
/// Hidden minimum price, revealed at the end of the auction.
BlindedPrice(Hash),
}
// The two extra 8's are present, one 8 is for the Vec's amount of elements and one is for the max
// usize in bid state.
pub const BASE_AUCTION_DATA_SIZE: usize = 32 + 32 + 9 + 9 + 9 + 9 + 1 + 32 + 1 + 8 + 8 + 8;
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub struct AuctionData {
/// Pubkey of the authority with permission to modify this auction.
pub authority: Pubkey,
/// Pubkey of the resource being bid on.
/// TODO try to bring this back some day. Had to remove this due to a stack access violation bug
/// interactin that happens in metaplex during redemptions due to some low level rust error
/// that happens when AuctionData has too many fields. This field was the least used.
///pub resource: Pubkey,
/// Token mint for the SPL token being used to bid
pub token_mint: Pubkey,
/// The time the last bid was placed, used to keep track of auction timing.
pub last_bid: Option<UnixTimestamp>,
/// Slot time the auction was officially ended by.
pub ended_at: Option<UnixTimestamp>,
/// End time is the cut-off point that the auction is forced to end by.
pub end_auction_at: Option<UnixTimestamp>,
/// Gap time is the amount of time in slots after the previous bid at which the auction ends.
pub end_auction_gap: Option<UnixTimestamp>,
/// Minimum price for any bid to meet.
pub price_floor: PriceFloor,
/// The state the auction is in, whether it has started or ended.
pub state: AuctionState,
/// Auction Bids, each user may have one bid open at a time.
pub bid_state: BidState,
}
pub const MAX_AUCTION_DATA_EXTENDED_SIZE: usize = 8 + 9 + 2 + 200;
// Further storage for more fields. Would like to store more on the main data but due
// to a borsh issue that causes more added fields to inflict "Access violation" errors
// during redemption in main Metaplex app for no reason, we had to add this nasty PDA.
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub struct AuctionDataExtended {
/// Total uncancelled bids
pub total_uncancelled_bids: u64,
// Unimplemented fields
/// Tick size
pub tick_size: Option<u64>,
/// gap_tick_size_percentage - two decimal points
pub gap_tick_size_percentage: Option<u8>,
}
impl AuctionDataExtended {
pub fn from_account_info(a: &AccountInfo) -> Result<AuctionDataExtended, ProgramError> {
if a.data_len() != MAX_AUCTION_DATA_EXTENDED_SIZE {
return Err(AuctionError::DataTypeMismatch.into());
}
let auction_extended: AuctionDataExtended = try_from_slice_unchecked(&a.data.borrow_mut())?;
Ok(auction_extended)
}
}
impl AuctionData {
pub fn from_account_info(a: &AccountInfo) -> Result<AuctionData, ProgramError> {
if (a.data_len() - BASE_AUCTION_DATA_SIZE) % mem::size_of::<Bid>() != 0 {
return Err(AuctionError::DataTypeMismatch.into());
}
let auction: AuctionData = try_from_slice_unchecked(&a.data.borrow_mut())?;
Ok(auction)
}
pub fn ended(&self, now: UnixTimestamp) -> Result<bool, ProgramError> {
// If there is an end time specified, handle conditions.
return match (self.ended_at, self.end_auction_gap) {
// NOTE if changing this, change in auction.ts on front end as well where logic duplicates.
// Both end and gap present, means a bid can still be placed post-auction if it is
// within the gap time.
(Some(end), Some(gap)) => {
// Check if the bid is within the gap between the last bidder.
if let Some(last) = self.last_bid {
let next_bid_time = match last.checked_add(gap) {
Some(val) => val,
None => return Err(AuctionError::NumericalOverflowError.into()),
};
Ok(now > end && now > next_bid_time)
} else {
Ok(now > end)
}
}
// Simply whether now has passed the end.
(Some(end), None) => Ok(now > end),
// No other end conditions.
_ => Ok(false),
};
}
pub fn is_winner(&self, key: &Pubkey) -> Option<usize> {
let minimum = match self.price_floor {
PriceFloor::MinimumPrice(min) => min[0],
_ => 0,
};
self.bid_state.is_winner(key, minimum)
}
pub fn num_winners(&self) -> u64 {
let minimum = match self.price_floor {
PriceFloor::MinimumPrice(min) => min[0],
_ => 0,
};
self.bid_state.num_winners(minimum)
}
pub fn winner_at(&self, idx: usize) -> Option<Pubkey> {
let minimum = match self.price_floor {
PriceFloor::MinimumPrice(min) => min[0],
_ => 0,
};
self.bid_state.winner_at(idx, minimum)
}
}
/// Define valid auction state transitions.
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub enum AuctionState {
Created,
Started,
Ended,
}
impl AuctionState {
pub fn create() -> Self {
AuctionState::Created
}
#[inline(always)]
pub fn start(self) -> Result<Self, ProgramError> {
match self {
AuctionState::Created => Ok(AuctionState::Started),
_ => Err(AuctionError::AuctionTransitionInvalid.into()),
}
}
#[inline(always)]
pub fn end(self) -> Result<Self, ProgramError> {
match self {
AuctionState::Started => Ok(AuctionState::Ended),
_ => Err(AuctionError::AuctionTransitionInvalid.into()),
}
}
}
/// Bids associate a bidding key with an amount bid.
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub struct Bid(pub Pubkey, pub u64);
/// BidState tracks the running state of an auction, each variant represents a different kind of
/// auction being run.
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub enum BidState {
EnglishAuction { bids: Vec<Bid>, max: usize },
OpenEdition { bids: Vec<Bid>, max: usize },
}
/// Bidding Implementations.
///
/// English Auction: this stores only the current winning bids in the auction, pruning cancelled
/// and lost bids over time.
///
/// Open Edition: All bids are accepted, cancellations return money to the bidder and always
/// succeed.
impl BidState {
pub fn new_english(n: usize) -> Self {
BidState::EnglishAuction {
bids: vec![],
max: n,
}
}
pub fn new_open_edition() -> Self {
BidState::OpenEdition {
bids: vec![],
max: 0,
}
}
pub fn max_array_size_for(n: usize) -> usize {
let mut real_max = n;
if real_max < 8 {
real_max = 8;
} else {
real_max = 2 * real_max
}
real_max
}
/// Push a new bid into the state, this succeeds only if the bid is larger than the current top
/// winner stored. Crappy list information to start with.
pub fn place_bid(&mut self, bid: Bid) -> Result<(), ProgramError> {
match self {
// In a capped auction, track the limited number of winners.
BidState::EnglishAuction { ref mut bids, max } => match bids.last() {
Some(top) => {
msg!("Looking to go over the loop");
for i in (0..bids.len()).rev() {
msg!("Comparison of {:?} and {:?} for {:?}", bids[i].1, bid.1, i);
if bids[i].1 < bid.1 {
msg!("Ok we can do an insert");
if i + 1 < bids.len() {
msg!("Doing a normal insert");
bids.insert(i + 1, bid);
} else {
msg!("Doing an on the end insert");
bids.push(bid)
}
break;
} else if bids[i].1 == bid.1 {
msg!("Ok we can do an equivalent insert");
if i == 0 {
msg!("Doing a normal insert");
bids.insert(0, bid);
break;
} else {
if bids[i - 1].1 != bids[i].1 {
msg!("Doing an insert just before");
bids.insert(i, bid);
break;
}
msg!("More duplicates ahead...")
}
} else if i == 0 {
msg!("Inserting at 0");
bids.insert(0, bid);
break;
}
}
let max_size = BidState::max_array_size_for(*max);
if bids.len() > max_size {
bids.remove(0);
}
Ok(())
}
_ => {
msg!("Pushing bid onto stack");
bids.push(bid);
Ok(())
}
},
// In an open auction, bidding simply succeeds.
BidState::OpenEdition { bids, max } => Ok(()),
}
}
/// Cancels a bid, if the bid was a winning bid it is removed, if the bid is invalid the
/// function simple no-ops.
pub fn cancel_bid(&mut self, key: Pubkey) -> Result<(), ProgramError> {
match self {
BidState::EnglishAuction { ref mut bids, max } => {
bids.retain(|b| b.0 != key);
Ok(())
}
// In an open auction, cancelling simply succeeds. It's up to the manager of an auction
// to decide what to do with open edition bids.
BidState::OpenEdition { bids, max } => Ok(()),
}
}
pub fn amount(&self, index: usize) -> u64 {
match self {
BidState::EnglishAuction { bids, max } => {
if index >= 0 as usize && index < bids.len() {
return bids[bids.len() - index - 1].1;
} else {
return 0;
}
}
BidState::OpenEdition { bids, max } => 0,
}
}
/// Check if a pubkey is currently a winner and return winner #1 as index 0 to outside world.
pub fn is_winner(&self, key: &Pubkey, min: u64) -> Option<usize> {
// NOTE if changing this, change in auction.ts on front end as well where logic duplicates.
match self {
// Presense in the winner list is enough to check win state.
BidState::EnglishAuction { bids, max } => {
match bids.iter().position(|bid| &bid.0 == key && bid.1 >= min) {
Some(val) => {
let zero_based_index = bids.len() - val - 1;
if zero_based_index < *max {
Some(zero_based_index)
} else {
None
}
}
None => None,
}
}
// There are no winners in an open edition, it is up to the auction manager to decide
// what to do with open edition bids.
BidState::OpenEdition { bids, max } => None,
}
}
pub fn num_winners(&self, min: u64) -> u64 {
match self {
BidState::EnglishAuction { bids, max } => cmp::min(
bids.iter()
.filter(|b| b.1 >= min)
.collect::<Vec<&Bid>>()
.len(),
*max,
) as u64,
BidState::OpenEdition { bids, max } => 0,
}
}
// Idea is to present winner as index 0 to outside world
pub fn winner_at(&self, index: usize, min: u64) -> Option<Pubkey> {
match self {
BidState::EnglishAuction { bids, max } => {
if index < *max && index < bids.len() {
let bid = &bids[bids.len() - index - 1];
if bid.1 >= min {
Some(bids[bids.len() - index - 1].0)
} else {
None
}
} else {
None
}
}
BidState::OpenEdition { bids, max } => None,
}
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub enum WinnerLimit {
Unlimited(usize),
Capped(usize),
}
pub const BIDDER_METADATA_LEN: usize = 32 + 32 + 8 + 8 + 1;
/// Models a set of metadata for a bidder, meant to be stored in a PDA. This allows looking up
/// information about a bidder regardless of if they have won, lost or cancelled.
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub struct BidderMetadata {
// Relationship with the bidder who's metadata this covers.
pub bidder_pubkey: Pubkey,
// Relationship with the auction this bid was placed on.
pub auction_pubkey: Pubkey,
// Amount that the user bid.
pub last_bid: u64,
// Tracks the last time this user bid.
pub last_bid_timestamp: UnixTimestamp,
// Whether the last bid the user made was cancelled. This should also be enough to know if the
// user is a winner, as if cancelled it implies previous bids were also cancelled.
pub cancelled: bool,
}
impl BidderMetadata {
pub fn from_account_info(a: &AccountInfo) -> Result<BidderMetadata, ProgramError> {
if a.data_len() != BIDDER_METADATA_LEN {
return Err(AuctionError::DataTypeMismatch.into());
}
let bidder_meta: BidderMetadata = try_from_slice_unchecked(&a.data.borrow_mut())?;
Ok(bidder_meta)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct BidderPot {
/// Points at actual pot that is a token account
pub bidder_pot: Pubkey,
/// Originating bidder account
pub bidder_act: Pubkey,
/// Auction account
pub auction_act: Pubkey,
/// emptied or not
pub emptied: bool,
}
impl BidderPot {
pub fn from_account_info(a: &AccountInfo) -> Result<BidderPot, ProgramError> {
if a.data_len() != mem::size_of::<BidderPot>() {
return Err(AuctionError::DataTypeMismatch.into());
}
let bidder_pot: BidderPot = try_from_slice_unchecked(&a.data.borrow_mut())?;
Ok(bidder_pot)
}
}

View File

@ -0,0 +1,240 @@
//! Cancels an existing bid. This only works in two cases:
//!
//! 1) The auction is still going on, in which case it is possible to cancel a bid at any time.
//! 2) The auction has finished, but the bid did not win. This allows users to claim back their
//! funds from bid accounts.
use crate::{
errors::AuctionError,
processor::{AuctionData, AuctionDataExtended, BidderMetadata, BidderPot},
utils::{
assert_derivation, assert_initialized, assert_owned_by, assert_signer,
assert_token_program_matches_package, create_or_allocate_account_raw, spl_token_transfer,
TokenTransferParams,
},
EXTENDED, PREFIX,
};
use super::AuctionState;
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::invoke_signed,
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
system_instruction,
sysvar::{clock::Clock, Sysvar},
},
spl_token::state::Account,
};
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct CancelBidArgs {
pub resource: Pubkey,
}
struct Accounts<'a, 'b: 'a> {
auction: &'a AccountInfo<'b>,
auction_extended: &'a AccountInfo<'b>,
bidder_meta: &'a AccountInfo<'b>,
bidder_pot: &'a AccountInfo<'b>,
bidder_pot_token: &'a AccountInfo<'b>,
bidder: &'a AccountInfo<'b>,
bidder_token: &'a AccountInfo<'b>,
clock_sysvar: &'a AccountInfo<'b>,
mint: &'a AccountInfo<'b>,
rent: &'a AccountInfo<'b>,
system: &'a AccountInfo<'b>,
token_program: &'a AccountInfo<'b>,
}
fn parse_accounts<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
) -> Result<Accounts<'a, 'b>, ProgramError> {
let account_iter = &mut accounts.iter();
let accounts = Accounts {
bidder: next_account_info(account_iter)?,
bidder_token: next_account_info(account_iter)?,
bidder_pot: next_account_info(account_iter)?,
bidder_pot_token: next_account_info(account_iter)?,
bidder_meta: next_account_info(account_iter)?,
auction: next_account_info(account_iter)?,
auction_extended: next_account_info(account_iter)?,
mint: next_account_info(account_iter)?,
clock_sysvar: next_account_info(account_iter)?,
rent: next_account_info(account_iter)?,
system: next_account_info(account_iter)?,
token_program: next_account_info(account_iter)?,
};
assert_owned_by(accounts.auction, program_id)?;
assert_owned_by(accounts.auction_extended, program_id)?;
assert_owned_by(accounts.bidder_meta, program_id)?;
assert_owned_by(accounts.mint, &spl_token::id())?;
assert_owned_by(accounts.bidder_pot, program_id)?;
assert_owned_by(accounts.bidder_pot_token, &spl_token::id())?;
assert_signer(accounts.bidder)?;
assert_token_program_matches_package(accounts.token_program)?;
if *accounts.token_program.key != spl_token::id() {
return Err(AuctionError::InvalidTokenProgram.into());
}
Ok(accounts)
}
pub fn cancel_bid(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: CancelBidArgs,
) -> ProgramResult {
msg!("+ Processing Cancelbid");
let accounts = parse_accounts(program_id, accounts)?;
// The account within the pot must be owned by us.
let actual_account: Account = assert_initialized(accounts.bidder_pot_token)?;
if actual_account.owner != *accounts.auction.key {
return Err(AuctionError::BidderPotTokenAccountOwnerMismatch.into());
}
// Derive and load Auction.
let auction_bump = assert_derivation(
program_id,
accounts.auction,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
],
)?;
let auction_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
&[auction_bump],
];
// Load the auction and verify this bid is valid.
let mut auction = AuctionData::from_account_info(accounts.auction)?;
// The mint provided in this bid must match the one the auction was initialized with.
if auction.token_mint != *accounts.mint.key {
return Err(AuctionError::IncorrectMint.into());
}
// Load the clock, used for various auction timing.
let clock = Clock::from_account_info(accounts.clock_sysvar)?;
// Derive Metadata key and load it.
let metadata_bump = assert_derivation(
program_id,
accounts.bidder_meta,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
"metadata".as_bytes(),
],
)?;
// If metadata doesn't exist, error, can't cancel a bid that doesn't exist and metadata must
// exist if a bid was placed.
if accounts.bidder_meta.owner != program_id {
return Err(AuctionError::MetadataInvalid.into());
}
// Derive Pot address, this account wraps/holds an SPL account to transfer tokens out of.
let pot_seeds = [
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
];
let pot_bump = assert_derivation(program_id, accounts.bidder_pot, &pot_seeds)?;
let bump_authority_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
&[pot_bump],
];
// If the bidder pot account is empty, this bid is invalid.
if accounts.bidder_pot.data_is_empty() {
return Err(AuctionError::BidderPotDoesNotExist.into());
}
// Refuse to cancel if the auction ended and this person is a winning account.
if auction.ended(clock.unix_timestamp)? && auction.is_winner(accounts.bidder.key).is_some() {
return Err(AuctionError::InvalidState.into());
}
// Confirm we're looking at the real SPL account for this bidder.
let bidder_pot = BidderPot::from_account_info(accounts.bidder_pot)?;
if bidder_pot.bidder_pot != *accounts.bidder_pot_token.key {
return Err(AuctionError::BidderPotTokenAccountOwnerMismatch.into());
}
// Transfer SPL bid balance back to the user.
let account: Account = Account::unpack_from_slice(&accounts.bidder_pot_token.data.borrow())?;
spl_token_transfer(TokenTransferParams {
source: accounts.bidder_pot_token.clone(),
destination: accounts.bidder_token.clone(),
authority: accounts.auction.clone(),
authority_signer_seeds: auction_seeds,
token_program: accounts.token_program.clone(),
amount: account.amount,
})?;
// Update Metadata
let metadata = BidderMetadata::from_account_info(accounts.bidder_meta)?;
let already_cancelled = metadata.cancelled;
BidderMetadata {
cancelled: true,
..metadata
}
.serialize(&mut *accounts.bidder_meta.data.borrow_mut())?;
// Update Auction
if auction.state != AuctionState::Ended {
// Once ended we want uncancelled bids to retain it's pre-ending count
assert_derivation(
program_id,
accounts.auction_extended,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
],
)?;
let mut auction_extended =
AuctionDataExtended::from_account_info(accounts.auction_extended)?;
msg!("Already cancelled is {:?}", already_cancelled);
if !already_cancelled && auction_extended.total_uncancelled_bids > 0 {
auction_extended.total_uncancelled_bids = auction_extended
.total_uncancelled_bids
.checked_sub(1)
.ok_or(AuctionError::NumericalOverflowError)?;
}
auction_extended.serialize(&mut *accounts.auction_extended.data.borrow_mut())?;
}
auction.bid_state.cancel_bid(*accounts.bidder.key);
auction.serialize(&mut *accounts.auction.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,181 @@
//! Claim bid winnings into a target SPL account, only the authorised key can do this, though the
//! target can be any SPL account.
use crate::{
errors::AuctionError,
processor::{AuctionData, BidderMetadata, BidderPot},
utils::{
assert_derivation, assert_initialized, assert_owned_by, assert_signer,
assert_token_program_matches_package, create_or_allocate_account_raw, spl_token_transfer,
TokenTransferParams,
},
PREFIX,
};
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::invoke_signed,
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
system_instruction,
sysvar::{clock::Clock, Sysvar},
},
spl_token::state::Account,
};
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct ClaimBidArgs {
pub resource: Pubkey,
}
struct Accounts<'a, 'b: 'a> {
destination: &'a AccountInfo<'b>,
bidder_pot_token: &'a AccountInfo<'b>,
bidder_pot: &'a AccountInfo<'b>,
authority: &'a AccountInfo<'b>,
auction: &'a AccountInfo<'b>,
bidder: &'a AccountInfo<'b>,
mint: &'a AccountInfo<'b>,
clock_sysvar: &'a AccountInfo<'b>,
token_program: &'a AccountInfo<'b>,
}
fn parse_accounts<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
) -> Result<Accounts<'a, 'b>, ProgramError> {
let account_iter = &mut accounts.iter();
let accounts = Accounts {
destination: next_account_info(account_iter)?,
bidder_pot_token: next_account_info(account_iter)?,
bidder_pot: next_account_info(account_iter)?,
authority: next_account_info(account_iter)?,
auction: next_account_info(account_iter)?,
bidder: next_account_info(account_iter)?,
mint: next_account_info(account_iter)?,
clock_sysvar: next_account_info(account_iter)?,
token_program: next_account_info(account_iter)?,
};
assert_owned_by(accounts.auction, program_id)?;
assert_owned_by(accounts.mint, &spl_token::id())?;
assert_owned_by(accounts.destination, &spl_token::id())?;
assert_owned_by(accounts.bidder_pot_token, &spl_token::id())?;
assert_owned_by(accounts.bidder_pot, program_id)?;
assert_signer(accounts.authority)?;
assert_token_program_matches_package(accounts.token_program)?;
if *accounts.token_program.key != spl_token::id() {
return Err(AuctionError::InvalidTokenProgram.into());
}
Ok(accounts)
}
pub fn claim_bid(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: ClaimBidArgs,
) -> ProgramResult {
msg!("+ Processing ClaimBid");
let accounts = parse_accounts(program_id, accounts)?;
let clock = Clock::from_account_info(accounts.clock_sysvar)?;
// The account within the pot must be owned by us.
let actual_account: Account = assert_initialized(accounts.bidder_pot_token)?;
if actual_account.owner != *accounts.auction.key {
return Err(AuctionError::BidderPotTokenAccountOwnerMismatch.into());
}
// Derive and load Auction.
let auction_bump = assert_derivation(
program_id,
accounts.auction,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
],
)?;
let auction_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
&[auction_bump],
];
// Load the auction and verify this bid is valid.
let auction = AuctionData::from_account_info(accounts.auction)?;
if auction.authority != *accounts.authority.key {
return Err(AuctionError::InvalidAuthority.into());
}
// User must have won the auction in order to claim their funds. Check early as the rest of the
// checks will be for nothing otherwise.
if auction.is_winner(accounts.bidder.key).is_none() {
msg!("User {:?} is not winner", accounts.bidder.key);
return Err(AuctionError::InvalidState.into());
}
// Auction must have ended.
if !auction.ended(clock.unix_timestamp)? {
return Err(AuctionError::InvalidState.into());
}
// The mint provided in this claim must match the one the auction was initialized with.
if auction.token_mint != *accounts.mint.key {
return Err(AuctionError::IncorrectMint.into());
}
// Derive Pot address, this account wraps/holds an SPL account to transfer tokens into.
let pot_seeds = [
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
];
let pot_bump = assert_derivation(program_id, accounts.bidder_pot, &pot_seeds)?;
let bump_authority_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
&[pot_bump],
];
// If the bidder pot account is empty, this bid is invalid.
if accounts.bidder_pot.data_is_empty() {
return Err(AuctionError::BidderPotDoesNotExist.into());
}
// Confirm we're looking at the real SPL account for this bidder.
let mut bidder_pot = BidderPot::from_account_info(accounts.bidder_pot)?;
if bidder_pot.bidder_pot != *accounts.bidder_pot_token.key {
return Err(AuctionError::BidderPotTokenAccountOwnerMismatch.into());
}
// Transfer SPL bid balance back to the user.
spl_token_transfer(TokenTransferParams {
source: accounts.bidder_pot_token.clone(),
destination: accounts.destination.clone(),
authority: accounts.auction.clone(),
authority_signer_seeds: auction_seeds,
token_program: accounts.token_program.clone(),
amount: actual_account.amount,
})?;
bidder_pot.emptied = true;
bidder_pot.serialize(&mut *accounts.bidder_pot.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,159 @@
use mem::size_of;
use crate::{
errors::AuctionError,
processor::{
AuctionData, AuctionDataExtended, AuctionState, Bid, BidState, PriceFloor, WinnerLimit,
BASE_AUCTION_DATA_SIZE, MAX_AUCTION_DATA_EXTENDED_SIZE,
},
utils::{assert_derivation, assert_owned_by, create_or_allocate_account_raw},
EXTENDED, PREFIX,
};
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
clock::UnixTimestamp,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
},
std::mem,
};
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct CreateAuctionArgs {
/// How many winners are allowed for this auction. See AuctionData.
pub winners: WinnerLimit,
/// End time is the cut-off point that the auction is forced to end by. See AuctionData.
pub end_auction_at: Option<UnixTimestamp>,
/// Gap time is how much time after the previous bid where the auction ends. See AuctionData.
pub end_auction_gap: Option<UnixTimestamp>,
/// Token mint for the SPL token used for bidding.
pub token_mint: Pubkey,
/// Authority
pub authority: Pubkey,
/// The resource being auctioned. See AuctionData.
pub resource: Pubkey,
/// Set a price floor.
pub price_floor: PriceFloor,
}
struct Accounts<'a, 'b: 'a> {
auction: &'a AccountInfo<'b>,
auction_extended: &'a AccountInfo<'b>,
payer: &'a AccountInfo<'b>,
rent: &'a AccountInfo<'b>,
system: &'a AccountInfo<'b>,
}
fn parse_accounts<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
) -> Result<Accounts<'a, 'b>, ProgramError> {
let account_iter = &mut accounts.iter();
let accounts = Accounts {
payer: next_account_info(account_iter)?,
auction: next_account_info(account_iter)?,
auction_extended: next_account_info(account_iter)?,
rent: next_account_info(account_iter)?,
system: next_account_info(account_iter)?,
};
Ok(accounts)
}
pub fn create_auction(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: CreateAuctionArgs,
) -> ProgramResult {
msg!("+ Processing CreateAuction");
let accounts = parse_accounts(program_id, accounts)?;
let auction_path = [
PREFIX.as_bytes(),
program_id.as_ref(),
&args.resource.to_bytes(),
];
// Derive the address we'll store the auction in, and confirm it matches what we expected the
// user to provide.
let (auction_key, bump) = Pubkey::find_program_address(&auction_path, program_id);
if auction_key != *accounts.auction.key {
return Err(AuctionError::InvalidAuctionAccount.into());
}
// The data must be large enough to hold at least the number of winners.
let auction_size = match args.winners {
WinnerLimit::Capped(n) => {
mem::size_of::<Bid>() * BidState::max_array_size_for(n) + BASE_AUCTION_DATA_SIZE
}
WinnerLimit::Unlimited(_) => BASE_AUCTION_DATA_SIZE,
};
let bid_state = match args.winners {
WinnerLimit::Capped(n) => BidState::new_english(n),
WinnerLimit::Unlimited(_) => BidState::new_open_edition(),
};
// Create auction account with enough space for a winner tracking.
create_or_allocate_account_raw(
*program_id,
accounts.auction,
accounts.rent,
accounts.system,
accounts.payer,
auction_size,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
&args.resource.to_bytes(),
&[bump],
],
)?;
let auction_ext_bump = assert_derivation(
program_id,
accounts.auction_extended,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
&args.resource.to_bytes(),
EXTENDED.as_bytes(),
],
)?;
create_or_allocate_account_raw(
*program_id,
accounts.auction_extended,
accounts.rent,
accounts.system,
accounts.payer,
MAX_AUCTION_DATA_EXTENDED_SIZE,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
&args.resource.to_bytes(),
EXTENDED.as_bytes(),
&[auction_ext_bump],
],
)?;
// Configure Auction.
AuctionData {
authority: args.authority,
bid_state: bid_state,
end_auction_at: args.end_auction_at,
end_auction_gap: args.end_auction_gap,
ended_at: None,
last_bid: None,
price_floor: args.price_floor,
state: AuctionState::create(),
token_mint: args.token_mint,
}
.serialize(&mut *accounts.auction.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,117 @@
use crate::{
errors::AuctionError,
processor::{AuctionData, AuctionState, Bid, BidState, PriceFloor, WinnerLimit},
utils::{assert_derivation, assert_owned_by, assert_signer, create_or_allocate_account_raw},
PREFIX,
};
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
hash, msg,
program_error::ProgramError,
pubkey::Pubkey,
sysvar::Sysvar,
},
std::mem,
};
type Price = u64;
type Salt = u64;
type Revealer = (Price, Salt);
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct EndAuctionArgs {
/// The resource being auctioned. See AuctionData.
pub resource: Pubkey,
/// If the auction was blinded, a revealing price must be specified to release the auction
/// winnings.
pub reveal: Option<Revealer>,
}
struct Accounts<'a, 'b: 'a> {
authority: &'a AccountInfo<'b>,
auction: &'a AccountInfo<'b>,
clock_sysvar: &'a AccountInfo<'b>,
}
fn parse_accounts<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
) -> Result<Accounts<'a, 'b>, ProgramError> {
let account_iter = &mut accounts.iter();
let accounts = Accounts {
authority: next_account_info(account_iter)?,
auction: next_account_info(account_iter)?,
clock_sysvar: next_account_info(account_iter)?,
};
assert_owned_by(accounts.auction, program_id)?;
assert_signer(accounts.authority)?;
Ok(accounts)
}
fn reveal(price_floor: PriceFloor, revealer: Option<Revealer>) -> Result<PriceFloor, ProgramError> {
// If the price floor was blinded, we update it.
if let PriceFloor::BlindedPrice(blinded) = price_floor {
// If the hash matches, update the price to the actual minimum.
if let Some(reveal) = revealer {
let reveal_hash = hash::hashv(&[&reveal.0.to_be_bytes(), &reveal.1.to_be_bytes()]);
if reveal_hash != blinded {
return Err(AuctionError::InvalidReveal.into());
}
Ok(PriceFloor::MinimumPrice([reveal.0, 0, 0, 0]))
} else {
return Err(AuctionError::MustReveal.into());
}
} else {
// No change needed in the else case.
Ok(price_floor)
}
}
pub fn end_auction<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
args: EndAuctionArgs,
) -> ProgramResult {
msg!("+ Processing EndAuction");
let accounts = parse_accounts(program_id, accounts)?;
let clock = Clock::from_account_info(accounts.clock_sysvar)?;
assert_derivation(
program_id,
accounts.auction,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
&args.resource.as_ref(),
],
)?;
// End auction.
let mut auction = AuctionData::from_account_info(accounts.auction)?;
// Check authority is correct.
if auction.authority != *accounts.authority.key {
return Err(AuctionError::InvalidAuthority.into());
}
// As long as it hasn't already ended.
if auction.ended_at.is_some() {
return Err(AuctionError::AuctionTransitionInvalid.into());
}
AuctionData {
ended_at: Some(clock.unix_timestamp),
state: auction.state.end()?,
price_floor: reveal(auction.price_floor, args.reveal)?,
..auction
}
.serialize(&mut *accounts.auction.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,335 @@
//! Places a bid on a running auction, the logic here implements a standard English auction
//! mechanism, once the auction starts, new bids can be made until 10 minutes has passed with no
//! new bid. At this point the auction ends.
//!
//! Possible Attacks to Consider:
//!
//! 1) A user bids many many small bids to fill up the buffer, so that his max bid wins.
//! 2) A user bids a large amount repeatedly to indefinitely delay the auction finishing.
//!
//! A few solutions come to mind: don't allow cancelling bids, and simply prune all bids that
//! are not winning bids from the state.
use borsh::try_to_vec_with_schema;
use crate::{
errors::AuctionError,
processor::{
AuctionData, AuctionDataExtended, AuctionState, Bid, BidderMetadata, BidderPot, PriceFloor,
},
utils::{
assert_derivation, assert_initialized, assert_owned_by, assert_signer,
assert_token_program_matches_package, create_or_allocate_account_raw, spl_token_transfer,
TokenTransferParams,
},
EXTENDED, PREFIX,
};
use super::BIDDER_METADATA_LEN;
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_option::COption,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
system_instruction,
system_instruction::create_account,
sysvar::{clock::Clock, Sysvar},
},
spl_token::state::Account,
std::mem,
};
/// Arguments for the PlaceBid instruction discriminant .
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct PlaceBidArgs {
/// Size of the bid being placed. The user must have enough SOL to satisfy this amount.
pub amount: u64,
/// Resource being bid on.
pub resource: Pubkey,
}
struct Accounts<'a, 'b: 'a> {
auction: &'a AccountInfo<'b>,
auction_extended: &'a AccountInfo<'b>,
bidder_meta: &'a AccountInfo<'b>,
bidder_pot: &'a AccountInfo<'b>,
bidder_pot_token: &'a AccountInfo<'b>,
bidder: &'a AccountInfo<'b>,
bidder_token: &'a AccountInfo<'b>,
clock_sysvar: &'a AccountInfo<'b>,
mint: &'a AccountInfo<'b>,
payer: &'a AccountInfo<'b>,
rent: &'a AccountInfo<'b>,
system: &'a AccountInfo<'b>,
token_program: &'a AccountInfo<'b>,
transfer_authority: &'a AccountInfo<'b>,
}
fn parse_accounts<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
) -> Result<Accounts<'a, 'b>, ProgramError> {
let account_iter = &mut accounts.iter();
let accounts = Accounts {
bidder: next_account_info(account_iter)?,
bidder_token: next_account_info(account_iter)?,
bidder_pot: next_account_info(account_iter)?,
bidder_pot_token: next_account_info(account_iter)?,
bidder_meta: next_account_info(account_iter)?,
auction: next_account_info(account_iter)?,
auction_extended: next_account_info(account_iter)?,
mint: next_account_info(account_iter)?,
transfer_authority: next_account_info(account_iter)?,
payer: next_account_info(account_iter)?,
clock_sysvar: next_account_info(account_iter)?,
rent: next_account_info(account_iter)?,
system: next_account_info(account_iter)?,
token_program: next_account_info(account_iter)?,
};
assert_owned_by(accounts.auction, program_id)?;
assert_owned_by(accounts.auction_extended, program_id)?;
assert_owned_by(accounts.bidder_token, &spl_token::id())?;
if !accounts.bidder_pot.data_is_empty() {
assert_owned_by(accounts.bidder_pot, program_id)?;
}
if !accounts.bidder_meta.data_is_empty() {
assert_owned_by(accounts.bidder_meta, program_id)?;
}
assert_owned_by(accounts.mint, &spl_token::id())?;
assert_owned_by(accounts.bidder_pot_token, &spl_token::id())?;
assert_signer(accounts.bidder)?;
assert_signer(accounts.payer)?;
assert_signer(accounts.transfer_authority)?;
assert_token_program_matches_package(accounts.token_program)?;
if *accounts.token_program.key != spl_token::id() {
return Err(AuctionError::InvalidTokenProgram.into());
}
Ok(accounts)
}
#[allow(clippy::absurd_extreme_comparisons)]
pub fn place_bid<'r, 'b: 'r>(
program_id: &Pubkey,
accounts: &'r [AccountInfo<'b>],
args: PlaceBidArgs,
) -> ProgramResult {
msg!("+ Processing PlaceBid");
let accounts = parse_accounts(program_id, accounts)?;
// Load the auction and verify this bid is valid.
let mut auction = AuctionData::from_account_info(accounts.auction)?;
// Load the clock, used for various auction timing.
let clock = Clock::from_account_info(accounts.clock_sysvar)?;
// Verify auction has not ended.
if auction.ended(clock.unix_timestamp)? {
auction.state = auction.state.end()?;
auction.serialize(&mut *accounts.auction.data.borrow_mut())?;
msg!("Auction ended!");
return Ok(());
}
// Derive Metadata key and load it.
let metadata_bump = assert_derivation(
program_id,
accounts.bidder_meta,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
"metadata".as_bytes(),
],
)?;
// If metadata doesn't exist, create it.
if accounts.bidder_meta.owner != program_id {
create_or_allocate_account_raw(
*program_id,
accounts.bidder_meta,
accounts.rent,
accounts.system,
accounts.payer,
// For whatever reason, using Mem function here returns 7, which is wholly wrong for this struct
// seems to be issues with UnixTimestamp
BIDDER_METADATA_LEN,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
"metadata".as_bytes(),
&[metadata_bump],
],
)?;
} else {
// Verify the last bid was cancelled before continuing.
let bidder_metadata: BidderMetadata =
BidderMetadata::from_account_info(accounts.bidder_meta)?;
if bidder_metadata.cancelled == false {
return Err(AuctionError::BidAlreadyActive.into());
}
};
// Derive Pot address, this account wraps/holds an SPL account to transfer tokens into and is
// also used as the authoriser of the SPL pot.
let pot_bump = assert_derivation(
program_id,
accounts.bidder_pot,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
],
)?;
// The account within the pot must be owned by us.
let actual_account: Account = assert_initialized(accounts.bidder_pot_token)?;
if actual_account.owner != *accounts.auction.key {
return Err(AuctionError::BidderPotTokenAccountOwnerMismatch.into());
}
if actual_account.delegate != COption::None {
return Err(AuctionError::DelegateShouldBeNone.into());
}
if actual_account.close_authority != COption::None {
return Err(AuctionError::CloseAuthorityShouldBeNone.into());
}
// Derive and load Auction.
let auction_bump = assert_derivation(
program_id,
accounts.auction,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
],
)?;
// Can't bid on an auction that isn't running.
if auction.state != AuctionState::Started {
return Err(AuctionError::InvalidState.into());
}
// Can't bid smaller than the minimum price.
if let PriceFloor::MinimumPrice(min) = auction.price_floor {
msg!(
"Amount is too small: {:?}, compared to price floor of {:?}",
args.amount,
min[0]
);
if args.amount <= min[0] {
return Err(AuctionError::BidTooSmall.into());
}
}
let bump_authority_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
accounts.auction.key.as_ref(),
accounts.bidder.key.as_ref(),
&[pot_bump],
];
// If the bidder pot account is empty, we need to generate one.
if accounts.bidder_pot.data_is_empty() {
create_or_allocate_account_raw(
*program_id,
accounts.bidder_pot,
accounts.rent,
accounts.system,
accounts.payer,
mem::size_of::<BidderPot>(),
bump_authority_seeds,
)?;
// Attach SPL token address to pot account.
let mut pot = BidderPot::from_account_info(accounts.bidder_pot)?;
pot.bidder_pot = *accounts.bidder_pot_token.key;
pot.bidder_act = *accounts.bidder.key;
pot.auction_act = *accounts.auction.key;
pot.serialize(&mut *accounts.bidder_pot.data.borrow_mut())?;
} else {
// Already exists, verify that the pot contains the specified SPL address.
let bidder_pot = BidderPot::from_account_info(accounts.bidder_pot)?;
if bidder_pot.bidder_pot != *accounts.bidder_pot_token.key {
return Err(AuctionError::BidderPotTokenAccountOwnerMismatch.into());
}
}
// Update now we have new bid.
assert_derivation(
program_id,
accounts.auction_extended,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
args.resource.as_ref(),
EXTENDED.as_bytes(),
],
)?;
let mut auction_extended: AuctionDataExtended =
AuctionDataExtended::from_account_info(accounts.auction_extended)?;
auction_extended.total_uncancelled_bids = auction_extended
.total_uncancelled_bids
.checked_add(1)
.ok_or(AuctionError::NumericalOverflowError)?;
auction_extended.serialize(&mut *accounts.auction_extended.data.borrow_mut())?;
// Confirm payers SPL token balance is enough to pay the bid.
let account: Account = Account::unpack_from_slice(&accounts.bidder_token.data.borrow())?;
if account.amount.saturating_sub(args.amount) < 0 {
msg!(
"Amount is too small: {:?}, compared to account amount of {:?}",
args.amount,
account.amount
);
return Err(AuctionError::BalanceTooLow.into());
}
// Transfer amount of SPL token to bid account.
spl_token_transfer(TokenTransferParams {
source: accounts.bidder_token.clone(),
destination: accounts.bidder_pot_token.clone(),
authority: accounts.transfer_authority.clone(),
authority_signer_seeds: bump_authority_seeds,
token_program: accounts.token_program.clone(),
amount: args.amount,
})?;
// Serialize new Auction State
auction.last_bid = Some(clock.unix_timestamp);
auction
.bid_state
.place_bid(Bid(*accounts.bidder.key, args.amount))?;
auction.serialize(&mut *accounts.auction.data.borrow_mut())?;
// Update latest metadata with results from the bid.
BidderMetadata {
bidder_pubkey: *accounts.bidder.key,
auction_pubkey: *accounts.auction.key,
last_bid: args.amount,
last_bid_timestamp: clock.unix_timestamp,
cancelled: false,
}
.serialize(&mut *accounts.bidder_meta.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,41 @@
//! Resets authority on an auction account.
use crate::{
errors::AuctionError,
processor::{AuctionData, BASE_AUCTION_DATA_SIZE},
utils::assert_owned_by,
PREFIX,
};
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
},
};
pub fn set_authority(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
msg!("+ Processing SetAuthority");
let account_iter = &mut accounts.iter();
let auction_act = next_account_info(account_iter)?;
let current_authority = next_account_info(account_iter)?;
let new_authority = next_account_info(account_iter)?;
let mut auction = AuctionData::from_account_info(auction_act)?;
assert_owned_by(auction_act, program_id)?;
if auction.authority != *current_authority.key {
return Err(AuctionError::InvalidAuthority.into());
}
if !current_authority.is_signer {
return Err(AuctionError::InvalidAuthority.into());
}
auction.authority = *new_authority.key;
auction.serialize(&mut *auction_act.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,96 @@
use crate::{
errors::AuctionError,
processor::{AuctionData, AuctionState, Bid, BidState, WinnerLimit},
utils::{assert_derivation, assert_owned_by, assert_signer, create_or_allocate_account_raw},
PREFIX,
};
use {
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
sysvar::Sysvar,
},
std::mem,
};
struct Accounts<'a, 'b: 'a> {
authority: &'a AccountInfo<'b>,
auction: &'a AccountInfo<'b>,
clock_sysvar: &'a AccountInfo<'b>,
}
fn parse_accounts<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
) -> Result<Accounts<'a, 'b>, ProgramError> {
let account_iter = &mut accounts.iter();
let accounts = Accounts {
authority: next_account_info(account_iter)?,
auction: next_account_info(account_iter)?,
clock_sysvar: next_account_info(account_iter)?,
};
assert_owned_by(accounts.auction, program_id)?;
assert_signer(accounts.authority)?;
Ok(accounts)
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub struct StartAuctionArgs {
/// The resource being auctioned. See AuctionData.
pub resource: Pubkey,
}
pub fn start_auction<'a, 'b: 'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'b>],
args: StartAuctionArgs,
) -> ProgramResult {
msg!("+ Processing StartAuction");
let accounts = parse_accounts(program_id, accounts)?;
let clock = Clock::from_account_info(accounts.clock_sysvar)?;
// Derive auction address so we can make the modifications necessary to start it.
assert_derivation(
program_id,
accounts.auction,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
&args.resource.as_ref(),
],
)?;
// Initialise a new auction. The end time is calculated relative to now.
let mut auction = AuctionData::from_account_info(accounts.auction)?;
// Check authority is correct.
if auction.authority != *accounts.authority.key {
return Err(AuctionError::InvalidAuthority.into());
}
// Calculate the relative end time.
let ended_at = if let Some(end_auction_at) = auction.end_auction_at {
match clock.unix_timestamp.checked_add(end_auction_at) {
Some(val) => Some(val),
None => return Err(AuctionError::NumericalOverflowError.into()),
}
} else {
None
};
AuctionData {
ended_at,
state: auction.state.start()?,
..auction
}
.serialize(&mut *accounts.auction.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,229 @@
use solana_program::program_pack::IsInitialized;
use {
crate::errors::AuctionError,
solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
},
std::convert::TryInto,
};
pub fn assert_initialized<T: Pack + IsInitialized>(
account_info: &AccountInfo,
) -> Result<T, ProgramError> {
let account: T = T::unpack_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(AuctionError::Uninitialized.into())
} else {
Ok(account)
}
}
pub fn assert_token_program_matches_package(token_program_info: &AccountInfo) -> ProgramResult {
if *token_program_info.key != spl_token::id() {
return Err(AuctionError::InvalidTokenProgram.into());
}
Ok(())
}
pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> ProgramResult {
if account.owner != owner {
msg!(
"{} Owner Invalid, Expected {}, Got {}",
account.key,
owner,
account.owner
);
Err(AuctionError::IncorrectOwner.into())
} else {
Ok(())
}
}
pub fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult {
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
Err(AuctionError::NotRentExempt.into())
} else {
Ok(())
}
}
pub fn assert_signer(account_info: &AccountInfo) -> ProgramResult {
if !account_info.is_signer {
Err(ProgramError::MissingRequiredSignature)
} else {
Ok(())
}
}
pub fn assert_derivation(
program_id: &Pubkey,
account: &AccountInfo,
path: &[&[u8]],
) -> Result<u8, ProgramError> {
let (key, bump) = Pubkey::find_program_address(&path, program_id);
if key != *account.key {
return Err(AuctionError::DerivedKeyInvalid.into());
}
Ok(bump)
}
#[inline(always)]
pub fn create_or_allocate_account_raw<'a>(
program_id: Pubkey,
new_account_info: &AccountInfo<'a>,
rent_sysvar_info: &AccountInfo<'a>,
system_program_info: &AccountInfo<'a>,
payer_info: &AccountInfo<'a>,
size: usize,
signer_seeds: &[&[u8]],
) -> Result<(), ProgramError> {
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let required_lamports = rent
.minimum_balance(size)
.max(1)
.saturating_sub(new_account_info.lamports());
if required_lamports > 0 {
msg!("Transfer {} lamports to the new account", required_lamports);
invoke(
&system_instruction::transfer(&payer_info.key, new_account_info.key, required_lamports),
&[
payer_info.clone(),
new_account_info.clone(),
system_program_info.clone(),
],
)?;
}
msg!("Allocate space for the account");
invoke_signed(
&system_instruction::allocate(new_account_info.key, size.try_into().unwrap()),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
msg!("Assign the account to the owning program");
invoke_signed(
&system_instruction::assign(new_account_info.key, &program_id),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
msg!("Completed assignation!");
Ok(())
}
///TokenTransferParams
pub struct TokenTransferParams<'a: 'b, 'b> {
/// source
pub source: AccountInfo<'a>,
/// destination
pub destination: AccountInfo<'a>,
/// amount
pub amount: u64,
/// authority
pub authority: AccountInfo<'a>,
/// authority_signer_seeds
pub authority_signer_seeds: &'b [&'b [u8]],
/// token_program
pub token_program: AccountInfo<'a>,
}
#[inline(always)]
pub fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult {
let TokenTransferParams {
source,
destination,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let result = invoke_signed(
&spl_token::instruction::transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[source, destination, authority, token_program],
&[authority_signer_seeds],
);
result.map_err(|_| AuctionError::TokenTransferFailed.into())
}
/// TokenMintToParams
pub struct TokenCreateAccount<'a> {
/// payer
pub payer: AccountInfo<'a>,
/// mint
pub mint: AccountInfo<'a>,
/// account
pub account: AccountInfo<'a>,
/// authority
pub authority: AccountInfo<'a>,
/// authority seeds
pub authority_seeds: &'a [&'a [u8]],
/// token_program
pub token_program: AccountInfo<'a>,
/// rent information
pub rent: AccountInfo<'a>,
}
/// Create a new SPL token account.
#[inline(always)]
pub fn spl_token_create_account(params: TokenCreateAccount<'_>) -> ProgramResult {
let TokenCreateAccount {
payer,
mint,
account,
authority,
authority_seeds,
token_program,
rent,
} = params;
let size = spl_token::state::Account::LEN;
let rent = &Rent::from_account_info(&rent)?;
let required_lamports = rent
.minimum_balance(size)
.max(1)
.saturating_sub(payer.lamports());
invoke(
&system_instruction::create_account(
payer.key,
account.key,
required_lamports,
size as u64,
&spl_token::id(),
),
&[payer, account.clone(), token_program],
)?;
invoke_signed(
&spl_token::instruction::initialize_account(
&spl_token::id(),
account.key,
mint.key,
authority.key,
)?,
&[],
&[authority_seeds],
)?;
Ok(())
}

View File

@ -0,0 +1,335 @@
use solana_program::{hash::Hash, program_pack::Pack, pubkey::Pubkey, system_instruction};
use solana_program_test::*;
use solana_sdk::{
account::Account,
signature::{Keypair, Signer},
transaction::Transaction,
transport::TransportError,
};
use spl_auction::{
instruction,
processor::{
CancelBidArgs, ClaimBidArgs, CreateAuctionArgs, EndAuctionArgs, PlaceBidArgs, PriceFloor,
StartAuctionArgs, WinnerLimit,
},
};
pub async fn get_account(banks_client: &mut BanksClient, pubkey: &Pubkey) -> Account {
banks_client
.get_account(*pubkey)
.await
.expect("account not found")
.expect("account empty")
}
pub async fn create_mint(
banks_client: &mut BanksClient,
payer: &Keypair,
recent_blockhash: &Hash,
) -> Result<(Keypair, Keypair), TransportError> {
let rent = banks_client.get_rent().await.unwrap();
let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN);
let pool_mint = Keypair::new();
let manager = Keypair::new();
let mut transaction = Transaction::new_with_payer(
&[
system_instruction::create_account(
&payer.pubkey(),
&pool_mint.pubkey(),
mint_rent,
spl_token::state::Mint::LEN as u64,
&spl_token::id(),
),
spl_token::instruction::initialize_mint(
&spl_token::id(),
&pool_mint.pubkey(),
&manager.pubkey(),
None,
0,
)
.unwrap(),
],
Some(&payer.pubkey()),
);
transaction.sign(&[payer, &pool_mint], *recent_blockhash);
banks_client.process_transaction(transaction).await?;
Ok((pool_mint, manager))
}
pub async fn create_token_account(
banks_client: &mut BanksClient,
payer: &Keypair,
recent_blockhash: &Hash,
account: &Keypair,
pool_mint: &Pubkey,
manager: &Pubkey,
) -> Result<(), TransportError> {
let rent = banks_client.get_rent().await.unwrap();
let account_rent = rent.minimum_balance(spl_token::state::Account::LEN);
let mut transaction = Transaction::new_with_payer(
&[
system_instruction::create_account(
&payer.pubkey(),
&account.pubkey(),
account_rent,
spl_token::state::Account::LEN as u64,
&spl_token::id(),
),
spl_token::instruction::initialize_account(
&spl_token::id(),
&account.pubkey(),
pool_mint,
manager,
)
.unwrap(),
],
Some(&payer.pubkey()),
);
transaction.sign(&[payer, account], *recent_blockhash);
banks_client.process_transaction(transaction).await?;
Ok(())
}
pub async fn mint_tokens(
banks_client: &mut BanksClient,
payer: &Keypair,
recent_blockhash: &Hash,
mint: &Pubkey,
account: &Pubkey,
mint_authority: &Keypair,
amount: u64,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[spl_token::instruction::mint_to(
&spl_token::id(),
mint,
account,
&mint_authority.pubkey(),
&[],
amount,
)
.unwrap()],
Some(&payer.pubkey()),
&[payer, mint_authority],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}
pub async fn get_token_balance(banks_client: &mut BanksClient, token: &Pubkey) -> u64 {
let token_account = banks_client.get_account(*token).await.unwrap().unwrap();
let account_info: spl_token::state::Account =
spl_token::state::Account::unpack_from_slice(token_account.data.as_slice()).unwrap();
account_info.amount
}
pub async fn get_token_supply(banks_client: &mut BanksClient, mint: &Pubkey) -> u64 {
let mint_account = banks_client.get_account(*mint).await.unwrap().unwrap();
let account_info =
spl_token::state::Mint::unpack_from_slice(mint_account.data.as_slice()).unwrap();
account_info.supply
}
pub async fn create_auction(
banks_client: &mut BanksClient,
program_id: &Pubkey,
payer: &Keypair,
recent_blockhash: &Hash,
resource: &Pubkey,
mint_keypair: &Pubkey,
max_winners: usize,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[instruction::create_auction_instruction(
*program_id,
payer.pubkey(),
CreateAuctionArgs {
authority: payer.pubkey(),
end_auction_at: None,
end_auction_gap: None,
resource: *resource,
token_mint: *mint_keypair,
winners: WinnerLimit::Capped(max_winners),
price_floor: PriceFloor::None([0u8; 32]),
},
)],
Some(&payer.pubkey()),
&[payer],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}
pub async fn end_auction(
banks_client: &mut BanksClient,
program_id: &Pubkey,
recent_blockhash: &Hash,
payer: &Keypair,
resource: &Pubkey,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[instruction::end_auction_instruction(
*program_id,
payer.pubkey(),
EndAuctionArgs {
resource: *resource,
reveal: None,
},
)],
Some(&payer.pubkey()),
&[payer],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}
pub async fn start_auction(
banks_client: &mut BanksClient,
program_id: &Pubkey,
recent_blockhash: &Hash,
payer: &Keypair,
resource: &Pubkey,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[instruction::start_auction_instruction(
*program_id,
payer.pubkey(),
StartAuctionArgs {
resource: *resource,
},
)],
Some(&payer.pubkey()),
&[payer],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn place_bid(
banks_client: &mut BanksClient,
recent_blockhash: &Hash,
program_id: &Pubkey,
payer: &Keypair,
bidder: &Keypair,
bidder_spl_account: &Keypair,
transfer_authority: &Keypair,
resource: &Pubkey,
mint: &Pubkey,
amount: u64,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[instruction::place_bid_instruction(
*program_id,
bidder.pubkey(), // Wallet used to identify bidder
bidder.pubkey(), // SPL token account (source) using same account here for ease of testing
bidder_spl_account.pubkey(), // SPL Token Account (Destination)
*mint, // Token Mint
transfer_authority.pubkey(), // Approved to Move Tokens
payer.pubkey(), // Pays for Transactions
PlaceBidArgs {
amount,
resource: *resource,
},
)],
Some(&payer.pubkey()),
&[bidder, transfer_authority, payer],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn cancel_bid(
banks_client: &mut BanksClient,
recent_blockhash: &Hash,
program_id: &Pubkey,
payer: &Keypair,
bidder: &Keypair,
bidder_spl_account: &Keypair,
resource: &Pubkey,
mint: &Pubkey,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[instruction::cancel_bid_instruction(
*program_id,
bidder.pubkey(),
bidder.pubkey(),
bidder_spl_account.pubkey(),
*mint,
CancelBidArgs {
resource: *resource,
},
)],
Some(&payer.pubkey()),
&[bidder, payer],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}
pub async fn approve(
banks_client: &mut BanksClient,
recent_blockhash: &Hash,
payer: &Keypair,
transfer_authority: &Pubkey,
spl_wallet: &Keypair,
amount: u64,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[spl_token::instruction::approve(
&spl_token::id(),
&spl_wallet.pubkey(),
transfer_authority,
&payer.pubkey(),
&[&payer.pubkey()],
amount,
)
.unwrap()],
Some(&payer.pubkey()),
&[payer],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn claim_bid(
banks_client: &mut BanksClient,
recent_blockhash: &Hash,
program_id: &Pubkey,
payer: &Keypair,
authority: &Keypair,
bidder: &Keypair,
bidder_spl_account: &Keypair,
seller: &Pubkey,
resource: &Pubkey,
mint: &Pubkey,
) -> Result<(), TransportError> {
let transaction = Transaction::new_signed_with_payer(
&[instruction::claim_bid_instruction(
*program_id,
authority.pubkey(),
*seller,
bidder.pubkey(),
bidder_spl_account.pubkey(),
*mint,
ClaimBidArgs {
resource: *resource,
},
)],
Some(&payer.pubkey()),
&[payer, authority],
*recent_blockhash,
);
banks_client.process_transaction(transaction).await?;
Ok(())
}

View File

@ -0,0 +1,696 @@
#![allow(warnings)]
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::borsh::try_from_slice_unchecked;
use solana_program_test::*;
use solana_sdk::program_pack::Pack;
use solana_sdk::{
account::Account,
hash::Hash,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{Keypair, Signer},
system_instruction, system_program,
transaction::Transaction,
transport::TransportError,
};
use spl_auction::{
instruction,
processor::{
process_instruction, AuctionData, AuctionState, Bid, BidState, BidderPot, CancelBidArgs,
CreateAuctionArgs, PlaceBidArgs, PriceFloor, StartAuctionArgs, WinnerLimit,
},
PREFIX,
};
use std::mem;
mod helpers;
/// Initialize an auction with a random resource, and generate bidders with tokens that can be used
/// for testing.
async fn setup_auction(
start: bool,
max_winners: usize,
) -> (
Pubkey,
BanksClient,
Vec<(Keypair, Keypair, Pubkey)>,
Keypair,
Pubkey,
Pubkey,
Pubkey,
Pubkey,
Hash,
) {
// Create a program to attach accounts to.
let program_id = Pubkey::new_unique();
let mut program_test =
ProgramTest::new("spl_auction", program_id, processor!(process_instruction));
// Start executing test.
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
// Create a Token mint to mint some test tokens with.
let (mint_keypair, mint_manager) =
helpers::create_mint(&mut banks_client, &payer, &recent_blockhash)
.await
.unwrap();
// Derive Auction PDA account for lookup.
let resource = Pubkey::new_unique();
let seeds = &[PREFIX.as_bytes(), &program_id.as_ref(), resource.as_ref()];
let (auction_pubkey, _) = Pubkey::find_program_address(seeds, &program_id);
// Run Create Auction instruction.
let err = helpers::create_auction(
&mut banks_client,
&program_id,
&payer,
&recent_blockhash,
&resource,
&mint_keypair.pubkey(),
max_winners,
)
.await
.unwrap();
// Attach useful Accounts for testing.
let mut bidders = vec![];
for n in 0..5 {
// Bidder SPL Account, with Minted Tokens
let bidder = Keypair::new();
// PDA in the auction for the Bidder to deposit their funds to.
let auction_spl_pot = Keypair::new();
// Generate User SPL Wallet Account
helpers::create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&bidder,
&mint_keypair.pubkey(),
&payer.pubkey(),
)
.await
.unwrap();
// Owner via pot PDA.
let (bid_pot_pubkey, pot_bump) = Pubkey::find_program_address(
&[
PREFIX.as_bytes(),
program_id.as_ref(),
auction_pubkey.as_ref(),
bidder.pubkey().as_ref(),
],
&program_id,
);
// Generate Auction SPL Pot to Transfer to.
helpers::create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&auction_spl_pot,
&mint_keypair.pubkey(),
&auction_pubkey,
)
.await
.unwrap();
// Mint Tokens
helpers::mint_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&mint_keypair.pubkey(),
&bidder.pubkey(),
&mint_manager,
10_000_000,
)
.await
.unwrap();
bidders.push((bidder, auction_spl_pot, bid_pot_pubkey));
}
// Verify Auction was created as expected.
let auction: AuctionData = try_from_slice_unchecked(
&banks_client
.get_account(auction_pubkey)
.await
.expect("get_account")
.expect("account not found")
.data,
)
.unwrap();
assert_eq!(auction.authority, payer.pubkey());
assert_eq!(auction.last_bid, None);
assert_eq!(auction.state as i32, AuctionState::create() as i32);
assert_eq!(auction.end_auction_at, None);
// Start Auction.
if start {
helpers::start_auction(
&mut banks_client,
&program_id,
&recent_blockhash,
&payer,
&resource,
)
.await
.unwrap();
}
return (
program_id,
banks_client,
bidders,
payer,
resource,
mint_keypair.pubkey(),
mint_manager.pubkey(),
auction_pubkey,
recent_blockhash,
);
}
/// Used to drive tests in the functions below.
#[derive(Debug)]
enum Action {
Bid(usize, u64),
Cancel(usize),
End,
}
/* Commenting out for now
#[cfg(feature = "test-bpf")]
#[tokio::test]
async fn test_correct_runs() {
// Local wrapper around a small test description described by actions.
struct Test {
actions: Vec<Action>,
expect: Vec<(usize, u64)>,
max_winners: usize,
price_floor: PriceFloor,
seller_collects: u64,
}
// A list of auction runs that should succeed. At the end of the run the winning bid state
// should match the expected result.
let strategies = [
// Simple successive bids should work.
Test {
actions: vec![
Action::Bid(0, 1000),
Action::Bid(1, 2000),
Action::Bid(2, 3000),
Action::Bid(3, 4000),
Action::End,
],
max_winners: 3,
price_floor: PriceFloor::None,
seller_collects: 9000,
expect: vec![(1, 2000), (2, 3000), (3, 4000)],
},
// A single bidder should be able to cancel and rebid lower.
Test {
actions: vec![
Action::Bid(0, 5000),
Action::Cancel(0),
Action::Bid(0, 4000),
Action::End,
],
expect: vec![(0, 4000)],
max_winners: 3,
price_floor: PriceFloor::None,
seller_collects: 4000,
},
// The top bidder when cancelling should allow room for lower bidders.
Test {
actions: vec![
Action::Bid(0, 5000),
Action::Bid(1, 6000),
Action::Cancel(1),
Action::Bid(2, 5500),
Action::Bid(1, 6000),
Action::Bid(3, 7000),
Action::Cancel(0),
Action::End,
],
expect: vec![(2, 5500), (1, 6000), (3, 7000)],
max_winners: 3,
price_floor: PriceFloor::None,
seller_collects: 18500,
},
// An auction where everyone cancels should still succeed, with no winners.
Test {
actions: vec![
Action::Bid(0, 5000),
Action::Bid(1, 6000),
Action::Bid(2, 7000),
Action::Cancel(0),
Action::Cancel(1),
Action::Cancel(2),
Action::End,
],
expect: vec![],
max_winners: 3,
price_floor: PriceFloor::None,
seller_collects: 0,
},
// An auction where no one bids should still succeed.
Test {
actions: vec![Action::End],
expect: vec![],
max_winners: 3,
price_floor: PriceFloor::None,
seller_collects: 0,
},
];
// Run each strategy with a new auction.
for strategy in strategies.iter() {
let (
program_id,
mut banks_client,
bidders,
payer,
resource,
mint,
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(true, strategy.max_winners).await;
// Interpret test actions one by one.
for action in strategy.actions.iter() {
println!("Strategy: {} Step {:?}", strategy.actions.len(), action);
match *action {
Action::Bid(bidder, amount) => {
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
let transfer_authority = Keypair::new();
helpers::approve(
&mut banks_client,
&recent_blockhash,
&payer,
&transfer_authority.pubkey(),
&bidders[bidder].0,
amount,
)
.await
.expect("approve");
helpers::place_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[bidder].0,
&bidders[bidder].1,
&transfer_authority,
&resource,
&mint,
amount,
)
.await
.expect("place_bid");
let post_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
assert_eq!(post_balance.0, pre_balance.0 - amount);
assert_eq!(post_balance.1, pre_balance.1 + amount);
}
Action::Cancel(bidder) => {
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
helpers::cancel_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[bidder].0,
&bidders[bidder].1,
&resource,
&mint,
)
.await
.expect("cancel_bid");
let bidder_account = banks_client
.get_account(bidders[bidder].0.pubkey())
.await
.expect("get_account")
.expect("account not found");
let post_balance = (
helpers::get_token_balance(&mut banks_client, &bidders[bidder].0.pubkey())
.await,
helpers::get_token_balance(&mut banks_client, &bidders[bidder].1.pubkey())
.await,
);
// Assert the balance successfully moves.
assert_eq!(post_balance.0, pre_balance.0 + pre_balance.1);
assert_eq!(post_balance.1, 0);
}
Action::End => {
helpers::end_auction(
&mut banks_client,
&program_id,
&recent_blockhash,
&payer,
&resource,
)
.await
.expect("end_auction");
// Assert Auction is actually in ended state.
let auction: AuctionData = try_from_slice_unchecked(
&banks_client
.get_account(auction_pubkey)
.await
.expect("get_account")
.expect("account not found")
.data,
)
.unwrap();
assert!(auction.ended_at.is_some());
}
}
}
// Verify a bid was created, and Metadata for this bidder correctly reflects
// the last bid as expected.
let auction: AuctionData = try_from_slice_unchecked(
&banks_client
.get_account(auction_pubkey)
.await
.expect("get_account")
.expect("account not found")
.data,
)
.unwrap();
// Verify BidState, all winners should be as expected
match auction.bid_state {
BidState::EnglishAuction { ref bids, .. } => {
// Zip internal bid state with the expected indices this strategy expects winners
// to result in.
let results: Vec<(_, _)> = strategy.expect.iter().zip(bids).collect();
for (index, bid) in results.iter() {
let bidder = &bidders[index.0];
let amount = index.1;
// Winners should match the keypair indices we expected.
// bid.0 is the pubkey.
// bidder.2 is the derived potkey we expect Bid.0 to be.
assert_eq!(bid.0, bidder.2);
// Must have bid the amount we expected.
// bid.1 is the amount.
assert_eq!(bid.1, amount);
}
// If the auction has ended, attempt to claim back SPL tokens into a new account.
if auction.ended(0) {
let collection = Keypair::new();
// Generate Collection Pot.
helpers::create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&collection,
&mint,
&payer.pubkey(),
)
.await
.unwrap();
// For each winning bid, claim into auction.
for (index, bid) in results {
let err = helpers::claim_bid(
&mut banks_client,
&recent_blockhash,
&program_id,
&payer,
&payer,
&bidders[index.0].0,
&bidders[index.0].1,
&collection.pubkey(),
&resource,
&mint,
)
.await;
println!("{:?}", err);
err.expect("claim_bid");
// Bid pot should be empty
let balance = helpers::get_token_balance(
&mut banks_client,
&bidders[index.0].1.pubkey(),
)
.await;
assert_eq!(balance, 0);
}
// Total claimed balance should match what we expect
let balance =
helpers::get_token_balance(&mut banks_client, &collection.pubkey()).await;
assert_eq!(balance, strategy.seller_collects);
}
}
_ => {}
}
}
}
// Function wrapper expected to fail for testing failures.
async fn handle_failing_action(
banks_client: &mut BanksClient,
recent_blockhash: &Hash,
program_id: &Pubkey,
bidders: &Vec<(Keypair, Keypair, Pubkey)>,
mint: &Pubkey,
payer: &Keypair,
resource: &Pubkey,
auction_pubkey: &Pubkey,
action: &Action,
) -> Result<(), TransportError> {
match *action {
Action::Bid(bidder, amount) => {
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(banks_client, &bidders[bidder].0.pubkey()).await,
helpers::get_token_balance(banks_client, &bidders[bidder].1.pubkey()).await,
);
let transfer_authority = Keypair::new();
helpers::approve(
banks_client,
&recent_blockhash,
&payer,
&transfer_authority.pubkey(),
&bidders[bidder].0,
amount,
)
.await?;
let value = helpers::place_bid(
banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[bidder].0,
&bidders[bidder].1,
&transfer_authority,
&resource,
&mint,
amount,
)
.await?;
let post_balance = (
helpers::get_token_balance(banks_client, &bidders[bidder].0.pubkey()).await,
helpers::get_token_balance(banks_client, &bidders[bidder].1.pubkey()).await,
);
assert_eq!(post_balance.0, pre_balance.0 - amount);
assert_eq!(post_balance.1, pre_balance.1 + amount);
}
Action::Cancel(bidder) => {
// Get balances pre bidding.
let pre_balance = (
helpers::get_token_balance(banks_client, &bidders[bidder].0.pubkey()).await,
helpers::get_token_balance(banks_client, &bidders[bidder].1.pubkey()).await,
);
helpers::cancel_bid(
banks_client,
&recent_blockhash,
&program_id,
&payer,
&bidders[bidder].0,
&bidders[bidder].1,
&resource,
&mint,
)
.await?;
let bidder_account = banks_client
.get_account(bidders[bidder].0.pubkey())
.await
.expect("get_account")
.expect("account not found");
let post_balance = (
helpers::get_token_balance(banks_client, &bidders[bidder].0.pubkey()).await,
helpers::get_token_balance(banks_client, &bidders[bidder].1.pubkey()).await,
);
// Assert the balance successfully moves.
assert_eq!(post_balance.0, pre_balance.0 + pre_balance.1);
assert_eq!(post_balance.1, 0);
}
Action::End => {
helpers::end_auction(
banks_client,
&program_id,
&recent_blockhash,
&payer,
&resource,
)
.await?;
// Assert Auction is actually in ended state.
let auction: AuctionData = try_from_slice_unchecked(
&banks_client
.get_account(*auction_pubkey)
.await
.expect("get_account")
.expect("account not found")
.data,
)?;
assert!(auction.ended_at.is_some());
}
}
Ok(())
}
#[cfg(feature = "test-bpf")]
#[tokio::test]
async fn test_incorrect_runs() {
// Local wrapper around a small test description described by actions.
#[derive(Debug)]
struct Test {
actions: Vec<Action>,
max_winners: usize,
price_floor: PriceFloor,
}
// A list of auction runs that should succeed. At the end of the run the winning bid state
// should match the expected result.
let strategies = [
Test {
actions: vec![Action::Cancel(0), Action::End],
max_winners: 3,
price_floor: PriceFloor::None,
},
// Cancel a non-existing bid.
// Bidding less than the top bidder should fail.
Test {
actions: vec![
Action::Bid(0, 5000),
Action::Bid(1, 6000),
Action::Bid(2, 5500),
Action::Bid(0, 1000),
Action::Bid(1, 2000),
Action::Bid(2, 3000),
Action::Bid(3, 4000),
Action::Bid(3, 4000),
Action::End,
],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
},
// Bidding less than any bidder should fail.
Test {
actions: vec![
Action::Bid(0, 5000),
Action::Bid(1, 6000),
Action::Bid(2, 1000),
Action::End,
],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
},
// Bidding after an auction has been explicitly ended should fail.
Test {
actions: vec![Action::Bid(0, 5000), Action::End, Action::Bid(1, 6000)],
max_winners: 3,
price_floor: PriceFloor::None([0; 32]),
},
];
// Run each strategy with a new auction.
for strategy in strategies.iter() {
let (
program_id,
mut banks_client,
bidders,
payer,
resource,
mint,
mint_authority,
auction_pubkey,
recent_blockhash,
) = setup_auction(true, strategy.max_winners).await;
let mut failed = false;
for action in strategy.actions.iter() {
failed = failed
|| handle_failing_action(
&mut banks_client,
&recent_blockhash,
&program_id,
&bidders,
&mint,
&payer,
&resource,
&auction_pubkey,
action,
)
.await
.is_err();
}
// Expect to fail.
assert!(failed);
}
}
*/

15
rust/cbindgen.sh Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
set -x
# Cargo.lock can cause older spl-token bindings to be generated? Move it out of
# the way...
mv -f Cargo.lock Cargo.lock.org
cargo run --manifest-path=utils/cgen/Cargo.toml
exitcode=$?
mv -f Cargo.lock.org Cargo.lock
exit $exitcode

22
rust/ci/cargo-build-test.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
source ./ci/rust-version.sh stable
source ./ci/solana-version.sh
export RUSTFLAGS="-D warnings"
export RUSTBACKTRACE=1
set -x
# Build/test all BPF programs
cargo +"$rust_stable" test-bpf -- --nocapture
rm -rf target/debug # Prevents running out of space on github action runners
# Build/test all host crates
cargo +"$rust_stable" build
cargo +"$rust_stable" test -- --nocapture
exit 0

92
rust/ci/env.sh Normal file
View File

@ -0,0 +1,92 @@
#
# Normalized CI environment variables
#
# |source| me
#
if [[ -n $CI ]]; then
export CI=1
if [[ -n $TRAVIS ]]; then
export CI_BRANCH=$TRAVIS_BRANCH
export CI_BASE_BRANCH=$TRAVIS_BRANCH
export CI_BUILD_ID=$TRAVIS_BUILD_ID
export CI_COMMIT=$TRAVIS_COMMIT
export CI_JOB_ID=$TRAVIS_JOB_ID
if [[ $TRAVIS_PULL_REQUEST != false ]]; then
export CI_PULL_REQUEST=true
else
export CI_PULL_REQUEST=
fi
export CI_OS_NAME=$TRAVIS_OS_NAME
export CI_REPO_SLUG=$TRAVIS_REPO_SLUG
export CI_TAG=$TRAVIS_TAG
elif [[ -n $BUILDKITE ]]; then
export CI_BRANCH=$BUILDKITE_BRANCH
export CI_BUILD_ID=$BUILDKITE_BUILD_ID
export CI_COMMIT=$BUILDKITE_COMMIT
export CI_JOB_ID=$BUILDKITE_JOB_ID
# The standard BUILDKITE_PULL_REQUEST environment variable is always "false" due
# to how solana-ci-gate is used to trigger PR builds rather than using the
# standard Buildkite PR trigger.
if [[ $CI_BRANCH =~ pull/* ]]; then
export CI_BASE_BRANCH=$BUILDKITE_PULL_REQUEST_BASE_BRANCH
export CI_PULL_REQUEST=true
else
export CI_BASE_BRANCH=$BUILDKITE_BRANCH
export CI_PULL_REQUEST=
fi
export CI_OS_NAME=linux
if [[ -n $BUILDKITE_TRIGGERED_FROM_BUILD_PIPELINE_SLUG ]]; then
# The solana-secondary pipeline should use the slug of the pipeline that
# triggered it
export CI_REPO_SLUG=$BUILDKITE_ORGANIZATION_SLUG/$BUILDKITE_TRIGGERED_FROM_BUILD_PIPELINE_SLUG
else
export CI_REPO_SLUG=$BUILDKITE_ORGANIZATION_SLUG/$BUILDKITE_PIPELINE_SLUG
fi
# TRIGGERED_BUILDKITE_TAG is a workaround to propagate BUILDKITE_TAG into
# the solana-secondary pipeline
if [[ -n $TRIGGERED_BUILDKITE_TAG ]]; then
export CI_TAG=$TRIGGERED_BUILDKITE_TAG
else
export CI_TAG=$BUILDKITE_TAG
fi
elif [[ -n $APPVEYOR ]]; then
export CI_BRANCH=$APPVEYOR_REPO_BRANCH
export CI_BUILD_ID=$APPVEYOR_BUILD_ID
export CI_COMMIT=$APPVEYOR_REPO_COMMIT
export CI_JOB_ID=$APPVEYOR_JOB_ID
if [[ -n $APPVEYOR_PULL_REQUEST_NUMBER ]]; then
export CI_PULL_REQUEST=true
else
export CI_PULL_REQUEST=
fi
if [[ $CI_LINUX = True ]]; then
export CI_OS_NAME=linux
else
export CI_OS_NAME=windows
fi
export CI_REPO_SLUG=$APPVEYOR_REPO_NAME
export CI_TAG=$APPVEYOR_REPO_TAG_NAME
fi
else
export CI=
export CI_BRANCH=
export CI_BUILD_ID=
export CI_COMMIT=
export CI_JOB_ID=
export CI_OS_NAME=
export CI_PULL_REQUEST=
export CI_REPO_SLUG=
export CI_TAG=
fi
cat <<EOF
CI=$CI
CI_BRANCH=$CI_BRANCH
CI_BUILD_ID=$CI_BUILD_ID
CI_COMMIT=$CI_COMMIT
CI_JOB_ID=$CI_JOB_ID
CI_OS_NAME=$CI_OS_NAME
CI_PULL_REQUEST=$CI_PULL_REQUEST
CI_TAG=$CI_TAG
EOF

15
rust/ci/install-build-deps.sh Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -ex
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo apt-add-repository "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-10 main"
sudo apt-get update
sudo apt-get install -y clang-7 --allow-unauthenticated
sudo apt-get install -y openssl --allow-unauthenticated
sudo apt-get install -y libssl-dev --allow-unauthenticated
sudo apt-get install -y libssl1.1 --allow-unauthenticated
sudo apt-get install -y libudev-dev
sudo apt-get install -y binutils-dev
sudo apt-get install -y libunwind-dev
clang-7 --version

14
rust/ci/install-program-deps.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
source ci/rust-version.sh stable
source ci/solana-version.sh install
set -x
cargo --version
cargo install rustfilt || true
cargo install honggfuzz --version=0.5.52 --force || true
cargo +"$rust_stable" build-bpf --version

65
rust/ci/rust-version.sh Normal file
View File

@ -0,0 +1,65 @@
#
# This file maintains the rust versions for use by CI.
#
# Obtain the environment variables without any automatic toolchain updating:
# $ source ci/rust-version.sh
#
# Obtain the environment variables updating both stable and nightly, only stable, or
# only nightly:
# $ source ci/rust-version.sh all
# $ source ci/rust-version.sh stable
# $ source ci/rust-version.sh nightly
# Then to build with either stable or nightly:
# $ cargo +"$rust_stable" build
# $ cargo +"$rust_nightly" build
#
if [[ -n $RUST_STABLE_VERSION ]]; then
stable_version="$RUST_STABLE_VERSION"
else
stable_version=1.50.0
fi
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
nightly_version="$RUST_NIGHTLY_VERSION"
else
nightly_version=2021-02-18
fi
export rust_stable="$stable_version"
export rust_stable_docker_image=solanalabs/rust:"$stable_version"
export rust_nightly=nightly-"$nightly_version"
export rust_nightly_docker_image=solanalabs/rust-nightly:"$nightly_version"
[[ -z $1 ]] || (
rustup_install() {
declare toolchain=$1
if ! cargo +"$toolchain" -V > /dev/null; then
echo "$0: Missing toolchain? Installing...: $toolchain" >&2
rustup install "$toolchain"
cargo +"$toolchain" -V
fi
}
set -e
cd "$(dirname "${BASH_SOURCE[0]}")"
case $1 in
stable)
rustup_install "$rust_stable"
;;
# nightly)
# rustup_install "$rust_nightly"
# ;;
all)
rustup_install "$rust_stable"
rustup_install "$rust_nightly"
;;
*)
echo "$0: Note: ignoring unknown argument: $1" >&2
;;
esac
)

34
rust/ci/solana-version.sh Executable file
View File

@ -0,0 +1,34 @@
#
# This file maintains the solana versions for use by CI.
#
# Obtain the environment variables without any automatic updating:
# $ source ci/solana-version.sh
#
# Obtain the environment variables and install update:
# $ source ci/solana-version.sh install
# Then to access the solana version:
# $ echo "$solana_version"
#
if [[ -n $SOLANA_VERSION ]]; then
solana_version="$SOLANA_VERSION"
else
solana_version=v1.6.2
fi
export solana_version="$solana_version"
export solana_docker_image=solanalabs/solana:"$solana_version"
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
if [[ -n $1 ]]; then
case $1 in
install)
sh -c "$(curl -sSfL https://release.solana.com/$solana_version/install)"
solana --version
;;
*)
echo "$0: Note: ignoring unknown argument: $1" >&2
;;
esac
fi

94
rust/coverage.sh Executable file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env bash
#
# Runs all program tests and builds a code coverage report
#
set -e
cd "$(dirname "$0")"
if ! which grcov; then
echo "Error: grcov not found. Try |cargo install grcov|"
exit 1
fi
if [[ ! "$(grcov --version)" =~ "0.6.1" ]]; then
echo Error: Required grcov version not installed
exit 1
fi
: "${CI_COMMIT:=local}"
reportName="lcov-${CI_COMMIT:0:9}"
if [[ -z $1 ]]; then
programs=(
memo/program
token/program
token-lending/program
token-swap/program
)
else
programs=("$@")
fi
coverageFlags=(-Zprofile) # Enable coverage
coverageFlags+=("-Clink-dead-code") # Dead code should appear red in the report
coverageFlags+=("-Ccodegen-units=1") # Disable code generation parallelism which is unsupported under -Zprofile (see [rustc issue #51705]).
coverageFlags+=("-Cinline-threshold=0") # Disable inlining, which complicates control flow.
coverageFlags+=("-Copt-level=0") #
coverageFlags+=("-Coverflow-checks=off") # Disable overflow checks, which create unnecessary branches.
export RUSTFLAGS="${coverageFlags[*]} $RUSTFLAGS"
export CARGO_INCREMENTAL=0
export RUST_BACKTRACE=1
export RUST_MIN_STACK=8388608
echo "--- remove old coverage results"
if [[ -d target/cov ]]; then
find target/cov -type f -name '*.gcda' -delete
fi
rm -rf target/cov/$reportName
mkdir -p target/cov
# Mark the base time for a clean room dir
touch target/cov/before-test
for program in ${programs[@]}; do
here=$PWD
(
set -ex
cd $program
cargo +nightly test --target-dir $here/target/cov
)
done
touch target/cov/after-test
echo "--- grcov"
# Create a clean room dir only with updated gcda/gcno files for this run,
# because our cached target dir is full of other builds' coverage files
rm -rf target/cov/tmp
mkdir -p target/cov/tmp
# Can't use a simpler construct under the condition of SC2044 and bash 3
# (macOS's default). See: https://github.com/koalaman/shellcheck/wiki/SC2044
find target/cov -type f -name '*.gcda' -newer target/cov/before-test ! -newer target/cov/after-test -print0 |
(while IFS= read -r -d '' gcda_file; do
gcno_file="${gcda_file%.gcda}.gcno"
ln -sf "../../../$gcda_file" "target/cov/tmp/$(basename "$gcda_file")"
ln -sf "../../../$gcno_file" "target/cov/tmp/$(basename "$gcno_file")"
done)
(
set -x
grcov target/cov/tmp --llvm -t html -o target/cov/$reportName
grcov target/cov/tmp --llvm -t lcov -o target/cov/lcov.info
cd target/cov
tar zcf report.tar.gz $reportName
)
ls -l target/cov/$reportName/index.html
ln -sfT $reportName target/cov/LATEST
exit $test_status

View File

@ -0,0 +1,28 @@
[package]
name = "spl-metaplex"
version = "0.0.1"
description = "Metaplex"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
exclude = ["js/**"]
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
spl-auction = { path = "../../auction/program", features = [ "no-entrypoint" ] }
num-derive = "0.3"
num-traits = "0.2"
arrayref = "0.3.6"
solana-program = "1.6.10"
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }
spl-token-vault = { path = "../../token-vault/program", features = [ "no-entrypoint" ] }
spl-token-metadata = { path = "../../token-metadata/program", features = [ "no-entrypoint" ] }
thiserror = "1.0"
borsh = "0.8.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,30 @@
---
title: Metaplex
---
## Background
Solana's programming model and the definitions of the Solana terms used in this
document are available at:
- https://docs.solana.com/apps
- https://docs.solana.com/terminology
## Source
The Metaplex Program's source is available on
[github](https://github.com/metaplex-foundation/metaplex)
There is also an example Rust client located at
[github](https://github.com/metaplex-foundation/metaplex/tree/master/token_vault/test/src/main.rs)
that can be perused for learning and built if desired with `cargo build`. It allows testing out a variety of scenarios.
## Interface
The on-chain Token Fraction program is written in Rust and available on crates.io as
[spl-vault](https://crates.io/crates/spl-token-vault) and
[docs.rs](https://docs.rs/spl-token-vault).
## Operational overview
TODO

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,25 @@
//! Program entrypoint definitions
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
use {
crate::{error::MetaplexError, processor},
solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
program_error::PrintProgramError, pubkey::Pubkey,
},
};
entrypoint!(process_instruction);
fn process_instruction<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
instruction_data: &[u8],
) -> ProgramResult {
if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) {
// catch the error so we can print it
error.print::<MetaplexError>();
return Err(error);
}
Ok(())
}

View File

@ -0,0 +1,395 @@
//! Error types
use {
num_derive::FromPrimitive,
solana_program::{
decode_error::DecodeError,
msg,
program_error::{PrintProgramError, ProgramError},
},
thiserror::Error,
};
/// Errors that may be returned by the Metaplex program.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum MetaplexError {
/// Invalid instruction data passed in.
#[error("Failed to unpack instruction data")]
InstructionUnpackError,
/// Lamport balance below rent-exempt threshold.
#[error("Lamport balance below rent-exempt threshold")]
NotRentExempt,
/// Already initialized
#[error("Already initialized")]
AlreadyInitialized,
/// Uninitialized
#[error("Uninitialized")]
Uninitialized,
/// Account does not have correct owner
#[error("Account does not have correct owner")]
IncorrectOwner,
/// NumericalOverflowError
#[error("NumericalOverflowError")]
NumericalOverflowError,
/// Token transfer failed
#[error("Token transfer failed")]
TokenTransferFailed,
/// Invalid transfer authority provided
#[error("Invalid transfer authority provided")]
InvalidTransferAuthority,
/// Vault's authority does not match the expected pda with seed ['metaplex', auction_key]
#[error("Vault's authority does not match the expected ['metaplex', auction_key]")]
VaultAuthorityMismatch,
/// Auction's authority does not match the expected pda with seed ['metaplex', auction_key]
#[error(
"Auction's authority does not match the expected pda with seed ['metaplex', auction_key]"
)]
AuctionAuthorityMismatch,
/// The authority passed to the call does not match the authority on the auction manager!
#[error(
"The authority passed to the call does not match the authority on the auction manager!"
)]
AuctionManagerAuthorityMismatch,
/// Vault given does not match that on given auction manager!
#[error("Vault given does not match that on given auction manager!")]
AuctionManagerVaultMismatch,
/// The safety deposit box given does not belong to the given vault!
#[error("The safety deposit box given does not belong to the given vault!")]
SafetyDepositBoxVaultMismatch,
/// The store given does not belong to the safety deposit box given!
#[error("The store given does not belong to the safety deposit box given!")]
SafetyDepositBoxStoreMismatch,
/// The metadata given does not match the mint on the safety deposit box given!
#[error("The metadata given does not match the mint on the safety deposit box given!")]
SafetyDepositBoxMetadataMismatch,
/// The Safety Deposit Box mint does not match the one time auth mint on the master edition
#[error(
"The Safety Deposit Box mint does not match the one time auth mint on the master edition!"
)]
SafetyDepositBoxMasterEditionOneTimeAuthMintMismatch,
/// The mint given does not match the mint on the given safety deposit box!
#[error("The mint given does not match the mint on the given safety deposit box!")]
SafetyDepositBoxMintMismatch,
/// The token metadata program given does not match the token metadata program on this auction manager!
#[error("The token metadata program given does not match the token metadata program on this auction manager!")]
AuctionManagerTokenMetadataProgramMismatch,
/// The mint is owned by a different token program than the one used by this auction manager!
#[error(
"The mint is owned by a different token program than the one used by this auction manager!"
)]
TokenProgramMismatch,
/// The auction given does not match the auction on the auction manager!
#[error("The auction given does not match the auction on the auction manager!")]
AuctionManagerAuctionMismatch,
/// The auction program given does not match the auction program on the auction manager!
#[error(
"The auction program given does not match the auction program on the auction manager!"
)]
AuctionManagerAuctionProgramMismatch,
/// The token program given does not match the token program on the auction manager!
#[error("The token program given does not match the token program on the auction manager!")]
AuctionManagerTokenProgramMismatch,
/// The token vault program given does not match the token vault program on the auction manager!
#[error("The token vault program given does not match the token vault program on the auction manager!")]
AuctionManagerTokenVaultProgramMismatch,
/// Only combined vaults may be used in auction managers!
#[error("Only combined vaults may be used in auction managers!")]
VaultNotCombined,
/// Cannot auction off an empty vault!
#[error("Cannot auction off an empty vault!")]
VaultCannotEmpty,
/// Listed a safety deposit box index that does not exist in this vault
#[error("Listed a safety deposit box index that does not exist in this vault")]
InvalidSafetyDepositBox,
/// Cant use a limited supply edition for an open edition as you may run out of editions to print
#[error("Cant use a limited supply edition for an open edition as you may run out of editions to print")]
CantUseLimitedSupplyEditionsWithOpenEditionAuction,
/// This safety deposit box is not listed as a prize in this auction manager!
#[error("This safety deposit box is not listed as a prize in this auction manager!")]
SafetyDepositBoxNotUsedInAuction,
/// Either you have given a non-existent edition address or you have given the address to a different token-metadata program than was used to make this edition!
#[error("Either you have given a non-existent edition address or you have given the address to a different token-metadata program than was used to make this edition!")]
InvalidEditionAddress,
/// There are not enough editions available for this auction!
#[error("There are not enough editions available for this auction!")]
NotEnoughEditionsAvailableForAuction,
/// The store in the safety deposit is empty, so you have nothing to auction!
#[error("The store in the safety deposit is empty, so you have nothing to auction!")]
StoreIsEmpty,
/// Not enough tokens to supply winners!
#[error("Not enough tokens to supply winners!")]
NotEnoughTokensToSupplyWinners,
/// The auction manager must own the payoff account!
#[error("The auction manager must own the payoff account!")]
AuctionManagerMustOwnPayoffAccount,
/// The auction manager must own the oustanding shares account!
#[error("The auction manager must own the oustanding shares account!")]
AuctionManagerMustOwnOutstandingSharesAccount,
/// The safety deposit box for your winning bid or participation placement does not match the safety deposit box you provided!
#[error("The safety deposit box for your winning bid or participation placement does not match the safety deposit box you provided!")]
SafetyDepositIndexMismatch,
/// This prize has already been claimed!
#[error("This prize has already been claimed!")]
PrizeAlreadyClaimed,
/// The bid redemption key does not match the expected PDA with seed ['metaplex', auction key, bidder metadata key]
#[error("The bid redemption key does not match the expected PDA with seed ['metaplex', auction key, bidder metadata key]")]
BidRedemptionMismatch,
/// This bid has already been redeemed!
#[error("This bid has already been redeemed!")]
BidAlreadyRedeemed,
/// Auction has not ended yet!
#[error("Auction has not ended yet!")]
AuctionHasNotEnded,
/// The original authority lookup does not match the expected PDA of ['metaplex', auction key, metadata key]
#[error("The original authority lookup does not match the expected PDA of ['metaplex', auction key, metadata key]")]
OriginalAuthorityLookupKeyMismatch,
/// The original authority given does not match that on the original authority lookup account!
#[error("The original authority given does not match that on the original authority lookup account!")]
OriginalAuthorityMismatch,
/// The prize you are attempting to claim needs to be claimed from a different endpoint than this one.
#[error("The prize you are attempting to claim needs to be claimed from a different endpoint than this one.")]
WrongBidEndpointForPrize,
/// The bidder given is not the bidder on the bidder metadata!
#[error("The bidder given is not the bidder on the bidder metadata!")]
BidderMetadataBidderMismatch,
/// Printing mint given does not match the mint on master edition!
#[error("Printing mint given does not match the mint on master edition!")]
MasterEditionMintMismatch,
/// One Time Auth mint given does not match the mint on master edition!
#[error("One Time Auth mint given does not match the mint on master edition!")]
MasterEditionOneTimeAuthMintMismatch,
/// The printing token account must be of the printing mint type to hold authorization tokens after auction end
#[error("The printing token account must be of the printing mint type to hold authorization tokens after auction end")]
PrintingTokenAccountMintMismatch,
/// Destination does not have the proper mint!
#[error("Destination does not have the proper mint!")]
DestinationMintMismatch,
/// Invalid edition key
#[error("Invalid edition key")]
InvalidEditionKey,
/// Token mint to failed
#[error("Token mint to failed")]
TokenMintToFailed,
/// The Printing mint authority provided does not match that on the mint
#[error("The Printing mint authority provided does not match that on the mint")]
MasterMintAuthorityMismatch,
/// The safety deposit box is not using the one time authorization mint of the master edition
#[error(
"The safety deposit box is not using the one time authorization mint of the master edition"
)]
MasterEditionOneTimeAuthorizationMintMismatch,
/// The accept payment account for this auction manager must match the auction's token mint!
#[error(
"The accept payment account for this auction manager must match the auction's token mint!"
)]
AuctionAcceptPaymentMintMismatch,
/// The accept payment owner must be the auction manager!
#[error("The accept payment owner must be the auction manager!")]
AcceptPaymentOwnerMismatch,
/// The accept payment given does not match the accept payment account on the auction manager!
#[error("The accept payment given does not match the accept payment account on the auction manager!")]
AcceptPaymentMismatch,
/// You are not eligible for an participation NFT!
#[error("You are not eligible for a participation NFT!")]
NotEligibleForParticipation,
#[error("Auction manager must be validated to start auction!")]
/// Auction manager must be validated to start auction!
AuctionManagerMustBeValidated,
/// The safety deposit mint type must be the Printing mint of the limited edition!
#[error("The safety deposit mint type must be the Printing mint of the limited edition!")]
SafetyDepositBoxMasterMintMismatch,
/// The mints between the accept payment and account provided do not match
#[error("The mints between the accept payment and account provided do not match")]
AcceptPaymentMintMismatch,
/// You do not have enough to buy this participation NFT!
#[error("You do not have enough to buy this participation NFT!")]
NotEnoughBalanceForParticipation,
/// Derived key invalid
#[error("Derived key invalid")]
DerivedKeyInvalid,
/// Creator is not active on this store!
#[error("Creator is not active on this store!")]
WhitelistedCreatorInactive,
/// This creator is not whitelisted
#[error("This creator is not whitelisted")]
InvalidWhitelistedCreator,
/// Store given does not match store on auction manager!
#[error("Store given does not match store on auction manager!")]
AuctionManagerStoreMismatch,
/// Supplied an invalid creator index to empty payment account
#[error("Supplied an invalid creator index to empty payment account")]
InvalidCreatorIndex,
/// Supplied an invalid winning config index
#[error("Supplied an invalid winning config index")]
InvalidWinningConfigIndex,
/// Metadata has creators and no creator index was supplied!
#[error("Metadata has creators and no creator index was supplied!")]
CreatorIndexExpected,
/// This winning config does not contain this safety deposit box as one of it's prizes
#[error("This winning config does not contain this safety deposit box as one of it's prizes")]
WinningConfigSafetyDepositMismatch,
/// The participation prize does not match the safety deposit given
#[error("The participation prize does not match the safety deposit given")]
ParticipationSafetyDepositMismatch,
/// Participation NFT not present on this auction, so cannot collect money for it
#[error("Participation NFT not present on this auction, so cannot collect money for it")]
ParticipationNotPresent,
/// Not possible to settle until all bids have been claimed
#[error("Not possible to settle until all bids have been claimed")]
NotAllBidsClaimed,
/// Invalid winning config item index provided
#[error("Invalid winning config item index provided")]
InvalidWinningConfigItemIndex,
/// When using a one time authorization token in a winning config item, you can never have amount > 1
#[error("When using a one time authorization token in a winning config item, you can never have amount > 1")]
OneTimeAuthorizationTokenMustBeOne,
/// Adding a reservation list failed
#[error("Adding a reservation list failed")]
AddReservationListFailed,
/// Close account command failed
#[error("Close account command failed")]
CloseAccountFailed,
/// A creator on this metadata has not verified it
#[error("A creator on this metadata has not verified it")]
CreatorHasNotVerifiedMetadata,
/// Duplicate winning config item detected
#[error("Duplicate winning config item detected")]
DuplicateWinningConfigItemDetected,
/// The authorization account provided does not match that on the participation state
#[error("The authorization account provided does not match that on the participation state")]
PrintingAuthorizationTokenAccountMismatch,
/// The transient account provided does not have the correct mint
#[error("The transient account provided does not have the correct mint")]
TransientAuthAccountMintMismatch,
/// The participation printing authorization token account is empty. One person needs to call populate on it!
#[error("The participation printing authorization token account is empty. One person needs to call populate on it!")]
ParticipationPrintingEmpty,
/// The printing authorization token command failed
#[error("The printing authorization token command failed")]
PrintingAuthorizationTokensFailed,
/// Invalid token program
#[error("Invalid token program")]
InvalidTokenProgram,
/// Token metadata program does not match
#[error("Token metadata program does not match")]
AuctionManagerTokenMetadataMismatch,
/// This safety deposit box has already been validated
#[error("This safety deposit box has already been validated")]
AlreadyValidated,
/// Auction must be created
#[error("Auction must be created")]
AuctionMustBeCreated,
/// Accept payment delegate should be none
#[error("Accept payment delegate should be none")]
DelegateShouldBeNone,
/// Accept payment close authority should be none
#[error("Accept payment close authority should be none")]
CloseAuthorityShouldBeNone,
/// Data type mismatch
#[error("Data type mismatch")]
DataTypeMismatch,
}
impl PrintProgramError for MetaplexError {
fn print<E>(&self) {
msg!(&self.to_string());
}
}
impl From<MetaplexError> for ProgramError {
fn from(e: MetaplexError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for MetaplexError {
fn type_of() -> &'static str {
"Metaplex Error"
}
}

View File

@ -0,0 +1,652 @@
use {
crate::state::{AuctionManagerSettings, PREFIX},
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
sysvar,
},
};
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct SetStoreArgs {
pub public: bool,
}
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct SetWhitelistedCreatorArgs {
pub activated: bool,
}
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct EmptyPaymentAccountArgs {
// If not redeeming a participation NFT's contributions, need to provide
// the winning config index your redeeming for. For participation, just pass None.
pub winning_config_index: Option<u8>,
/// If not redeeming a participation NFT, you also need to index into the winning config item's list.
pub winning_config_item_index: Option<u8>,
/// index in the metadata creator list, can be None if metadata has no creator list.
pub creator_index: Option<u8>,
}
/// Instructions supported by the Fraction program.
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub enum MetaplexInstruction {
/// Initializes an Auction Manager
//
/// 0. `[writable]` Uninitialized, unallocated auction manager account with pda of ['metaplex', auction_key from auction referenced below]
/// 1. `[]` Combined vault account with authority set to auction manager account (this will be checked)
/// Note in addition that this vault account should have authority set to this program's pda of ['metaplex', auction_key]
/// 2. `[]` Auction with auctioned item being set to the vault given and authority set to this program's pda of ['metaplex', auction_key]
/// 3. `[]` Authority for the Auction Manager
/// 4. `[signer]` Payer
/// 5. `[]` Accept payment account of same token mint as the auction for taking payment for open editions, owner should be auction manager key
/// 6. `[]` Store that this auction manager will belong to
/// 7. `[]` System sysvar
/// 8. `[]` Rent sysvar
InitAuctionManager(AuctionManagerSettings),
/// Validates that a given safety deposit box has in it contents that match the expected WinningConfig in the auction manager.
/// A stateful call, this will error out if you call it a second time after validation has occurred.
/// 0. `[writable]` Uninitialized Safety deposit validation ticket, pda of seed ['metaplex', program id, auction manager key, safety deposit key]
/// 1. `[writable]` Auction manager
/// 2. `[writable]` Metadata account
/// 3. `[writable]` Original authority lookup - unallocated uninitialized pda account with seed ['metaplex', auction key, metadata key]
/// We will store original authority here to return it later.
/// 4. `[]` A whitelisted creator entry for the store of this auction manager pda of ['metaplex', store key, creator key]
/// where creator key comes from creator list of metadata, any will do
/// 5. `[]` The auction manager's store key
/// 6. `[]` Safety deposit box account
/// 7. `[]` Safety deposit box storage account where the actual nft token is stored
/// 8. `[]` Mint account of the token in the safety deposit box
/// 9. `[]` Edition OR MasterEdition record key
/// Remember this does not need to be an existing account (may not be depending on token), just is a pda with seed
/// of ['metadata', program id, Printing mint id, 'edition']. - remember PDA is relative to token metadata program.
/// 10. `[]` Vault account
/// 11. `[signer]` Authority
/// 12. `[signer optional]` Metadata Authority - Signer only required if doing a full ownership txfer
/// 13. `[signer]` Payer
/// 14. `[]` Token metadata program
/// 15. `[]` System
/// 16. `[]` Rent sysvar
/// 17. `[writable]` Limited edition Printing mint account (optional - only if using sending Limited Edition)
/// 18. `[signer]` Limited edition Printing mint Authority account, this will TEMPORARILY TRANSFER MINTING AUTHORITY to the auction manager
/// until all limited editions have been redeemed for authority tokens.
ValidateSafetyDepositBox,
/// Note: This requires that auction manager be in a Running state.
///
/// If an auction is complete, you can redeem your bid for a specific item here. If you are the first to do this,
/// The auction manager will switch from Running state to Disbursing state. If you are the last, this may change
/// the auction manager state to Finished provided that no authorities remain to be delegated for Master Edition tokens.
///
/// NOTE: Please note that it is totally possible to redeem a bid 2x - once for a prize you won and once at the RedeemParticipationBid point for an open edition
/// that comes as a 'token of appreciation' for bidding. They are not mutually exclusive unless explicitly set to be that way.
///
/// 0. `[writable]` Auction manager
/// 1. `[writable]` Safety deposit token storage account
/// 2. `[writable]` Destination account.
/// 3. `[writable]` Bid redemption key -
/// Just a PDA with seed ['metaplex', auction_key, bidder_metadata_key] that we will allocate to mark that you redeemed your bid
/// 4. `[writable]` Safety deposit box account
/// 5. `[writable]` Vault account
/// 6. `[writable]` Fraction mint of the vault
/// 7. `[]` Auction
/// 8. `[]` Your BidderMetadata account
/// 9. `[signer optional]` Your Bidder account - Only needs to be signer if payer does not own
/// 10. `[signer]` Payer
/// 11. `[]` Token program
/// 12. `[]` Token Vault program
/// 13. `[]` Token metadata program
/// 14. `[]` Store
/// 15. `[]` System
/// 16. `[]` Rent sysvar
/// 17. `[]` PDA-based Transfer authority to move the tokens from the store to the destination seed ['vault', program_id]
/// but please note that this is a PDA relative to the Token Vault program, with the 'vault' prefix
/// 18. `[optional/writable]` Master edition (if Printing type of WinningConfig)
/// 19. `[optional/writable]` Reservation list PDA ['metadata', program id, master edition key, 'reservation', auction manager key]
/// relative to token metadata program (if Printing type of WinningConfig)
RedeemBid,
/// Note: This requires that auction manager be in a Running state.
///
/// If an auction is complete, you can redeem your bid for the actual Master Edition itself if it's for that prize here.
/// If you are the first to do this, the auction manager will switch from Running state to Disbursing state.
/// If you are the last, this may change the auction manager state to Finished provided that no authorities remain to be delegated for Master Edition tokens.
///
/// NOTE: Please note that it is totally possible to redeem a bid 2x - once for a prize you won and once at the RedeemParticipationBid point for an open edition
/// that comes as a 'token of appreciation' for bidding. They are not mutually exclusive unless explicitly set to be that way.
///
/// 0. `[writable]` Auction manager
/// 1. `[writable]` Safety deposit token storage account
/// 2. `[writable]` Destination account.
/// 3. `[writable]` Bid redemption key -
/// Just a PDA with seed ['metaplex', auction_key, bidder_metadata_key] that we will allocate to mark that you redeemed your bid
/// 4. `[writable]` Safety deposit box account
/// 5. `[writable]` Vault account
/// 6. `[writable]` Fraction mint of the vault
/// 7. `[]` Auction
/// 8. `[]` Your BidderMetadata account
/// 9. `[signer optional]` Your Bidder account - Only needs to be signer if payer does not own
/// 10. `[signer]` Payer
/// 11. `[]` Token program
/// 12. `[]` Token Vault program
/// 13. `[]` Token metadata program
/// 14. `[]` Store
/// 15. `[]` System
/// 16. `[]` Rent sysvar
/// 17. `[writable]` Master Metadata account (pda of ['metadata', program id, Printing mint id]) - remember PDA is relative to token metadata program
/// (This account is optional, and will only be used if metadata is unique, otherwise this account key will be ignored no matter it's value)
/// 18. `[]` New authority for Master Metadata - If you are taking ownership of a Master Edition in and of itself, or a Limited Edition that isn't newly minted for you during this auction
/// ie someone else had it minted for themselves in a prior auction or through some other means, this is the account the metadata for these tokens will be delegated to
/// after this transaction. Otherwise this account will be ignored.
/// 19. `[]` PDA-based Transfer authority to move the tokens from the store to the destination seed ['vault', program_id]
/// but please note that this is a PDA relative to the Token Vault program, with the 'vault' prefix
RedeemFullRightsTransferBid,
/// Note: This requires that auction manager be in a Running state.
///
/// If an auction is complete, you can redeem your bid for an Open Edition token if it is eligible. If you are the first to do this,
/// The auction manager will switch from Running state to Disbursing state. If you are the last, this may change
/// the auction manager state to Finished provided that no authorities remain to be delegated for Master Edition tokens.
///
/// NOTE: Please note that it is totally possible to redeem a bid 2x - once for a prize you won and once at this end point for a open edition
/// that comes as a 'token of appreciation' for bidding. They are not mutually exclusive unless explicitly set to be that way.
///
/// NOTE: If you are redeeming a newly minted Open Edition, you must actually supply a destination account containing a token from a brand new
/// mint. We do not provide the token to you. Our job with this action is to christen this mint + token combo as an official Open Edition.
///
/// 0. `[writable]` Auction manager
/// 1. `[writable]` Safety deposit token storage account
/// 2. `[writable]` Destination account for limited edition authority token. Must be same mint as master edition Printing mint.
/// 3. `[writable]` Bid redemption key -
/// Just a PDA with seed ['metaplex', auction_key, bidder_metadata_key] that we will allocate to mark that you redeemed your bid
/// 4. `[]` Safety deposit box account
/// 5. `[]` Vault account
/// 6. `[]` Fraction mint of the vault
/// 7. `[]` Auction
/// 8. `[]` Your BidderMetadata account
/// 9. `[signer optional/writable]` Your Bidder account - Only needs to be signer if payer does not own
/// 10. `[signer]` Payer
/// 11. `[]` Token program
/// 12. `[]` Token Vault program
/// 13. `[]` Token metadata program
/// 14. `[]` Store
/// 15. `[]` System
/// 16. `[]` Rent sysvar
/// 18. `[signer]` Transfer authority to move the payment in the auction's token_mint coin from the bidder account for the participation_fixed_price
/// on the auction manager to the auction manager account itself.
/// 19. `[writable]` The accept payment account for the auction manager
/// 20. `[writable]` The token account you will potentially pay for the open edition bid with if necessary
/// 21. `[writable]` Participation NFT printing holding account (present on participation_state)
RedeemParticipationBid,
/// If the auction manager is in Validated state, it can invoke the start command via calling this command here.
///
/// 0. `[writable]` Auction manager
/// 1. `[writable]` Auction
/// 3. `[signer]` Auction manager authority
/// 4. `[]` Store key
/// 5. `[]` Auction program
/// 6. `[]` Clock sysvar
StartAuction,
/// If the auction manager is in a Disbursing or Finished state, then this means Auction must be in Ended state.
/// Then this end point can be used as a signed proxy to use auction manager's authority over the auction to claim bid funds
/// into the accept payment account on the auction manager for a given bid. Auction has no opinions on how bids are redeemed,
/// only that they exist, have been paid, and have a winning place. It is up to the implementer of the auction to determine redemption,
/// and auction manager does this via bid redemption tickets and the vault contract which ensure the user always
/// can get their NFT once they have paid. Therefore, once they have paid, and the auction is over, the artist can claim
/// funds at any time without any danger to the user of losing out on their NFT, because the AM will honor their bid with an NFT
/// at ANY time.
///
/// 0. `[writable]` The accept payment account on the auction manager
/// 1. `[writable]` The bidder pot token account
/// 2. `[writable]` The bidder pot pda account [seed of ['auction', program_id, auction key, bidder key] -
/// relative to the auction program, not auction manager
/// 3. `[writable]` Auction manager
/// 4. `[]` The auction
/// 5. `[]` The bidder wallet
/// 6. `[]` Token mint of the auction
/// 7. `[]` Vault
/// 8. `[]` Store
/// 9. `[]` Auction program
/// 10. `[]` Clock sysvar
/// 11. `[]` Token program
ClaimBid,
/// At any time, the auction manager authority may empty whatever funds are in the accept payment account
/// on the auction manager. Funds come here from fixed price payments for partipation nfts, and from draining bid payments
/// from the auction.
///
/// This action specifically takes a given safety deposit box, winning config, and creator on a metadata for the token inside that safety deposit box
/// and pumps the requisite monies out to that creator as required by the royalties formula.
///
/// It's up to the UI to iterate through all winning configs, all safety deposit boxes in a given winning config tier, and all creators for
/// each metadata attached to each safety deposit box, to get all the money. Note that one safety deposit box can be used in multiple different winning configs,
/// but this shouldn't make any difference to this function.
///
/// We designed this function to be called in this loop-like manner because there is a limit to the number of accounts that can
/// be passed up at once (32) and there may be many more than that easily in a given auction, so it's easier for the implementer to just
/// loop through and call it, and there is an incentive for them to do so (to get paid.) It's permissionless as well as it
/// will empty into any destination account owned by the creator that has the proper mint, so anybody can call it.
///
/// For the participation NFT, there is no winning config, but the total is figured by summing the winning bids and subtracting
/// from the total escrow amount present.
///
/// 0. `[writable]` The accept payment account on the auction manager
/// 1. `[writable]` The destination account of same mint type as the accept payment account. Must be an Associated Token Account.
/// 2. `[writable]` Auction manager
/// 3. `[writable]` Payout ticket info to keep track of this artist or auctioneer's payment, pda of [metaplex, auction manager, winning config index OR 'participation', safety deposit key]
/// 4. `[signer]` payer
/// 5. `[]` The metadata
/// 6. `[]` The master edition of the metadata (optional if exists)
/// (pda of ['metadata', program id, metadata mint id, 'edition']) - remember PDA is relative to token metadata program
/// 7. `[]` Safety deposit box account
/// 8. `[]` The store of the auction manager
/// 9. `[]` The vault
/// 10. `[]` Auction
/// 11. `[]` Token program
/// 12. `[]` System program
/// 13. `[]` Rent sysvar
EmptyPaymentAccount(EmptyPaymentAccountArgs),
/// Given a signer wallet, create a store with pda ['metaplex', wallet] (if it does not exist) and/or update it
/// (if it already exists). Stores can be set to open (anybody can publish) or closed (publish only via whitelist).
///
/// 0. `[writable]` The store key, seed of ['metaplex', admin wallet]
/// 1. `[signer]` The admin wallet
/// 2. `[signer]` Payer
/// 3. `[]` Token program
/// 4. `[]` Token vault program
/// 5. `[]` Token metadata program
/// 6. `[]` Auction program
/// 7. `[]` System
/// 8. `[]` Rent sysvar
SetStore(SetStoreArgs),
/// Given an existing store, add or update an existing whitelisted creator for the store. This creates
/// a PDA with seed ['metaplex', store key, creator key] if it does not already exist to store attributes there.
///
/// 0. `[writable]` The whitelisted creator pda key, seed of ['metaplex', store key, creator key]
/// 1. `[signer]` The admin wallet
/// 2. `[signer]` Payer
/// 3. `[]` The creator key
/// 4. `[]` The store key, seed of ['metaplex', admin wallet]
/// 5. `[]` System
/// 6. `[]` Rent sysvar
SetWhitelistedCreator(SetWhitelistedCreatorArgs),
/// Validates an participation nft (if present) on the Auction Manager. Because of the differing mechanics of an open
/// edition (required for participation nft), it needs to be validated at a different endpoint than a normal safety deposit box.
/// 0. `[writable]` Auction manager
/// 1. `[]` Open edition metadata
/// 2. `[]` Open edition MasterEdition account
/// 3. `[]` Printing authorization token holding account - must be of the printing_mint type on the master_edition, used by
/// the auction manager to hold printing authorization tokens for all eligible winners of the participation nft when auction ends. Must
/// be owned by auction manager account.
/// 4. `[signer]` Authority for the Auction Manager
/// 5. `[]` A whitelisted creator entry for this store for the open edition
/// pda of ['metaplex', store key, creator key] where creator key comes from creator list of metadata
/// 6. `[]` The auction manager's store
/// 7. `[]` Safety deposit box
/// 8. `[]` Safety deposit token store
/// 9. `[]` Vault
/// 10. `[]` Rent sysvar
ValidateParticipation,
/// Needs to be called by someone at the end of the auction - will use the one time authorization token
/// to fire up a bunch of printing tokens for use in participation redemptions.
///
/// 0. `[writable]` Safety deposit token store
/// 1. `[writable]` Transient account with mint of one time authorization account on master edition - you can delete after this txn
/// 2. `[writable]` The printing token account on the participation state of the auction manager
/// 3. `[writable]` One time printing authorization mint
/// 4. `[writable]` Printing mint
/// 5. `[writable]` Safety deposit of the participation prize
/// 6. `[writable]` Vault info
/// 7. `[]` Fraction mint
/// 8. `[]` Auction info
/// 9. `[]` Auction manager info
/// 10. `[]` Token program
/// 11. `[]` Token vault program
/// 12. `[]` Token metadata program
/// 13. `[]` Auction manager store
/// 14. `[]` Master edition
/// 15. `[]` PDA-based Transfer authority to move the tokens from the store to the destination seed ['vault', program_id]
/// but please note that this is a PDA relative to the Token Vault program, with the 'vault' prefix
/// 16. `[]` Payer who wishes to receive refund for closing of one time transient account once we're done here
/// 17. `[]` Rent
PopulateParticipationPrintingAccount,
}
/// Creates an InitAuctionManager instruction
#[allow(clippy::too_many_arguments)]
pub fn create_init_auction_manager_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
vault: Pubkey,
auction: Pubkey,
auction_manager_authority: Pubkey,
payer: Pubkey,
accept_payment_account_key: Pubkey,
store: Pubkey,
settings: AuctionManagerSettings,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_manager, false),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(auction, false),
AccountMeta::new_readonly(auction_manager_authority, false),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(accept_payment_account_key, false),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: MetaplexInstruction::InitAuctionManager(settings)
.try_to_vec()
.unwrap(),
}
}
/// Creates an ValidateParticipation instruction
#[allow(clippy::too_many_arguments)]
pub fn create_validate_participation_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
open_edition_metadata: Pubkey,
open_edition_master_edition: Pubkey,
printing_authorization_token_account: Pubkey,
auction_manager_authority: Pubkey,
whitelisted_creator: Pubkey,
store: Pubkey,
safety_deposit_box: Pubkey,
safety_deposit_box_token_store: Pubkey,
vault: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_manager, false),
AccountMeta::new_readonly(open_edition_metadata, false),
AccountMeta::new_readonly(open_edition_master_edition, false),
AccountMeta::new_readonly(printing_authorization_token_account, false),
AccountMeta::new_readonly(auction_manager_authority, true),
AccountMeta::new_readonly(whitelisted_creator, false),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(safety_deposit_box, false),
AccountMeta::new_readonly(safety_deposit_box_token_store, false),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: MetaplexInstruction::ValidateParticipation
.try_to_vec()
.unwrap(),
}
}
/// Creates an ValidateSafetyDepositBox instruction
#[allow(clippy::too_many_arguments)]
pub fn create_validate_safety_deposit_box_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
metadata: Pubkey,
original_authority_lookup: Pubkey,
whitelisted_creator: Pubkey,
store: Pubkey,
safety_deposit_box: Pubkey,
safety_deposit_token_store: Pubkey,
safety_deposit_mint: Pubkey,
edition: Pubkey,
vault: Pubkey,
auction_manager_authority: Pubkey,
metadata_authority: Pubkey,
payer: Pubkey,
printing_mint: Option<Pubkey>,
printing_mint_authority: Option<Pubkey>,
) -> Instruction {
let (validation, _) = Pubkey::find_program_address(
&[
PREFIX.as_bytes(),
program_id.as_ref(),
auction_manager.as_ref(),
safety_deposit_box.as_ref(),
],
&program_id,
);
let mut accounts = vec![
AccountMeta::new(validation, false),
AccountMeta::new(auction_manager, false),
AccountMeta::new(metadata, false),
AccountMeta::new(original_authority_lookup, false),
AccountMeta::new_readonly(whitelisted_creator, false),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(safety_deposit_box, false),
AccountMeta::new_readonly(safety_deposit_token_store, false),
AccountMeta::new_readonly(safety_deposit_mint, false),
AccountMeta::new_readonly(edition, false),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(auction_manager_authority, true),
AccountMeta::new_readonly(metadata_authority, true),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(spl_token_metadata::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
if let Some(key) = printing_mint {
accounts.push(AccountMeta::new(key, false))
}
if let Some(key) = printing_mint_authority {
accounts.push(AccountMeta::new_readonly(key, true))
}
Instruction {
program_id,
accounts,
data: MetaplexInstruction::ValidateSafetyDepositBox
.try_to_vec()
.unwrap(),
}
}
/// Creates an RedeemBid instruction
#[allow(clippy::too_many_arguments)]
pub fn create_redeem_bid_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
safety_deposit_token_store: Pubkey,
destination: Pubkey,
bid_redemption: Pubkey,
safety_deposit_box: Pubkey,
vault: Pubkey,
fraction_mint: Pubkey,
auction: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
store: Pubkey,
transfer_authority: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_manager, false),
AccountMeta::new(safety_deposit_token_store, false),
AccountMeta::new(destination, false),
AccountMeta::new(bid_redemption, false),
AccountMeta::new(safety_deposit_box, false),
AccountMeta::new(vault, false),
AccountMeta::new(fraction_mint, false),
AccountMeta::new_readonly(auction, false),
AccountMeta::new_readonly(bidder_metadata, false),
AccountMeta::new_readonly(bidder, true),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(spl_token_vault::id(), false),
AccountMeta::new_readonly(spl_token_metadata::id(), false),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(transfer_authority, false),
],
data: MetaplexInstruction::RedeemBid.try_to_vec().unwrap(),
}
}
/// Creates an RedeemFullRightsTransferBid instruction
#[allow(clippy::too_many_arguments)]
pub fn create_redeem_full_rights_transfer_bid_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
safety_deposit_token_store: Pubkey,
destination: Pubkey,
bid_redemption: Pubkey,
safety_deposit_box: Pubkey,
vault: Pubkey,
fraction_mint: Pubkey,
auction: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
store: Pubkey,
master_metadata: Pubkey,
new_metadata_authority: Pubkey,
transfer_authority: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_manager, false),
AccountMeta::new(safety_deposit_token_store, false),
AccountMeta::new(destination, false),
AccountMeta::new(bid_redemption, false),
AccountMeta::new(safety_deposit_box, false),
AccountMeta::new(vault, false),
AccountMeta::new(fraction_mint, false),
AccountMeta::new_readonly(auction, false),
AccountMeta::new_readonly(bidder_metadata, false),
AccountMeta::new_readonly(bidder, true),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(spl_token_vault::id(), false),
AccountMeta::new_readonly(spl_token_metadata::id(), false),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new(master_metadata, false),
AccountMeta::new_readonly(new_metadata_authority, false),
AccountMeta::new_readonly(transfer_authority, false),
],
data: MetaplexInstruction::RedeemFullRightsTransferBid
.try_to_vec()
.unwrap(),
}
}
/// Creates an RedeemOpenEditionBid instruction
#[allow(clippy::too_many_arguments)]
pub fn create_redeem_participation_bid_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
safety_deposit_token_store: Pubkey,
destination: Pubkey,
bid_redemption: Pubkey,
safety_deposit_box: Pubkey,
vault: Pubkey,
fraction_mint: Pubkey,
auction: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
store: Pubkey,
transfer_authority: Pubkey,
accept_payment: Pubkey,
paying_token_account: Pubkey,
printing_authorization_token_account: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_manager, false),
AccountMeta::new(safety_deposit_token_store, false),
AccountMeta::new(destination, false),
AccountMeta::new(bid_redemption, false),
AccountMeta::new_readonly(safety_deposit_box, false),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(fraction_mint, false),
AccountMeta::new_readonly(auction, false),
AccountMeta::new_readonly(bidder_metadata, false),
AccountMeta::new_readonly(bidder, true),
AccountMeta::new(payer, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(spl_token_vault::id(), false),
AccountMeta::new_readonly(spl_token_metadata::id(), false),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(transfer_authority, true),
AccountMeta::new(accept_payment, false),
AccountMeta::new(paying_token_account, false),
AccountMeta::new(printing_authorization_token_account, false),
],
data: MetaplexInstruction::RedeemParticipationBid
.try_to_vec()
.unwrap(),
}
}
/// Creates an StartAuction instruction
#[allow(clippy::too_many_arguments)]
pub fn create_start_auction_instruction(
program_id: Pubkey,
auction_manager: Pubkey,
auction: Pubkey,
auction_manager_authority: Pubkey,
store: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(auction_manager, false),
AccountMeta::new(auction, false),
AccountMeta::new_readonly(auction_manager_authority, true),
AccountMeta::new_readonly(store, false),
AccountMeta::new_readonly(spl_auction::id(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
],
data: MetaplexInstruction::StartAuction.try_to_vec().unwrap(),
}
}
/// Creates an SetStore instruction
pub fn create_set_store_instruction(
program_id: Pubkey,
store: Pubkey,
admin: Pubkey,
payer: Pubkey,
public: bool,
) -> Instruction {
let accounts = vec![
AccountMeta::new(store, false),
AccountMeta::new_readonly(admin, true),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(spl_token_vault::id(), false),
AccountMeta::new_readonly(spl_token_metadata::id(), false),
AccountMeta::new_readonly(spl_auction::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
Instruction {
program_id,
accounts,
data: MetaplexInstruction::SetStore(SetStoreArgs { public })
.try_to_vec()
.unwrap(),
}
}

View File

@ -0,0 +1,12 @@
//! A Token Fraction program for the Solana blockchain.
pub mod entrypoint;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
pub mod utils;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
solana_program::declare_id!("p1exdMJcjVao65QdewkaZRUnU6VPSXhus9n2GzWfh98");

View File

@ -0,0 +1,88 @@
use {
crate::instruction::MetaplexInstruction,
borsh::BorshDeserialize,
claim_bid::process_claim_bid,
empty_payment_account::process_empty_payment_account,
init_auction_manager::process_init_auction_manager,
populate_participation_printing_account::process_populate_participation_printing_account,
redeem_bid::process_redeem_bid,
redeem_full_rights_transfer_bid::process_full_rights_transfer_bid,
redeem_participation_bid::process_redeem_participation_bid,
set_store::process_set_store,
set_whitelisted_creator::process_set_whitelisted_creator,
solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey},
start_auction::process_start_auction,
validate_participation::process_validate_participation,
validate_safety_deposit_box::process_validate_safety_deposit_box,
};
pub mod claim_bid;
pub mod empty_payment_account;
pub mod init_auction_manager;
pub mod populate_participation_printing_account;
pub mod redeem_bid;
pub mod redeem_full_rights_transfer_bid;
pub mod redeem_participation_bid;
pub mod set_store;
pub mod set_whitelisted_creator;
pub mod start_auction;
pub mod validate_participation;
pub mod validate_safety_deposit_box;
pub fn process_instruction<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
input: &[u8],
) -> ProgramResult {
let instruction = MetaplexInstruction::try_from_slice(input)?;
match instruction {
MetaplexInstruction::InitAuctionManager(auction_manager_settings) => {
msg!("Instruction: Init Auction Manager");
process_init_auction_manager(program_id, accounts, auction_manager_settings)
}
MetaplexInstruction::ValidateSafetyDepositBox => {
msg!("Instruction: Validate Safety Deposit Box");
process_validate_safety_deposit_box(program_id, accounts)
}
MetaplexInstruction::RedeemBid => {
msg!("Instruction: Redeem Normal Token Bid");
process_redeem_bid(program_id, accounts)
}
MetaplexInstruction::RedeemFullRightsTransferBid => {
msg!("Instruction: Redeem Full Rights Transfer Bid");
process_full_rights_transfer_bid(program_id, accounts)
}
MetaplexInstruction::RedeemParticipationBid => {
msg!("Instruction: Redeem Participation Bid");
process_redeem_participation_bid(program_id, accounts)
}
MetaplexInstruction::StartAuction => {
msg!("Instruction: Start Auction");
process_start_auction(program_id, accounts)
}
MetaplexInstruction::ClaimBid => {
msg!("Instruction: Claim Bid");
process_claim_bid(program_id, accounts)
}
MetaplexInstruction::EmptyPaymentAccount(args) => {
msg!("Instruction: Empty Payment Account");
process_empty_payment_account(program_id, accounts, args)
}
MetaplexInstruction::SetStore(args) => {
msg!("Instruction: Set Store");
process_set_store(program_id, accounts, args.public)
}
MetaplexInstruction::SetWhitelistedCreator(args) => {
msg!("Instruction: Set Whitelisted Creator");
process_set_whitelisted_creator(program_id, accounts, args.activated)
}
MetaplexInstruction::ValidateParticipation => {
msg!("Instruction: Validate Open Edition");
process_validate_participation(program_id, accounts)
}
MetaplexInstruction::PopulateParticipationPrintingAccount => {
msg!("Instruction: Populate Participation Printing Account");
process_populate_participation_printing_account(program_id, accounts)
}
}
}

View File

@ -0,0 +1,159 @@
use {
crate::{
error::MetaplexError,
state::{AuctionManager, AuctionManagerStatus, Store, PREFIX},
utils::{assert_derivation, assert_owned_by},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
},
spl_auction::{
instruction::claim_bid_instruction,
processor::{claim_bid::ClaimBidArgs, AuctionData, AuctionState},
},
};
#[allow(clippy::too_many_arguments)]
pub fn issue_claim_bid<'a>(
auction_program: AccountInfo<'a>,
auction: AccountInfo<'a>,
accept_payment: AccountInfo<'a>,
authority: AccountInfo<'a>,
bidder: AccountInfo<'a>,
bidder_pot: AccountInfo<'a>,
bidder_pot_token_acct: AccountInfo<'a>,
token_mint: AccountInfo<'a>,
clock: AccountInfo<'a>,
token_program: AccountInfo<'a>,
vault: Pubkey,
signer_seeds: &[&[u8]],
) -> ProgramResult {
invoke_signed(
&claim_bid_instruction(
*auction_program.key,
*accept_payment.key,
*authority.key,
*bidder.key,
*bidder_pot_token_acct.key,
*token_mint.key,
ClaimBidArgs { resource: vault },
),
&[
auction_program,
authority,
auction,
clock,
token_mint,
bidder,
bidder_pot_token_acct,
bidder_pot,
accept_payment,
token_program,
],
&[&signer_seeds],
)?;
Ok(())
}
pub fn process_claim_bid(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let accept_payment_info = next_account_info(account_info_iter)?;
let bidder_pot_token_info = next_account_info(account_info_iter)?;
let bidder_pot_info = next_account_info(account_info_iter)?;
let auction_manager_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let bidder_info = next_account_info(account_info_iter)?;
let token_mint_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let auction_program_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
let store = Store::from_account_info(store_info)?;
let auction = AuctionData::from_account_info(auction_info)?;
assert_owned_by(auction_info, &store.auction_program)?;
assert_owned_by(auction_manager_info, program_id)?;
assert_owned_by(accept_payment_info, &spl_token::id())?;
assert_owned_by(bidder_pot_token_info, &spl_token::id())?;
assert_owned_by(bidder_pot_info, &store.auction_program)?;
assert_owned_by(token_mint_info, &spl_token::id())?;
assert_owned_by(vault_info, &store.token_vault_program)?;
assert_owned_by(store_info, program_id)?;
if auction_manager.store != *store_info.key {
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
}
if auction_manager.auction != *auction_info.key {
return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
}
if store.auction_program != *auction_program_info.key {
return Err(MetaplexError::AuctionManagerAuctionProgramMismatch.into());
}
if store.token_program != *token_program_info.key {
return Err(MetaplexError::AuctionManagerTokenProgramMismatch.into());
}
if auction_manager.accept_payment != *accept_payment_info.key {
return Err(MetaplexError::AcceptPaymentMismatch.into());
}
if auction_manager.vault != *vault_info.key {
return Err(MetaplexError::AuctionManagerVaultMismatch.into());
}
if auction.state != AuctionState::Ended {
return Err(MetaplexError::AuctionHasNotEnded.into());
}
if auction_manager.state.status != AuctionManagerStatus::Disbursing
&& auction_manager.state.status != AuctionManagerStatus::Finished
{
auction_manager.state.status = AuctionManagerStatus::Disbursing;
}
if let Some(winner_index) = auction.is_winner(bidder_info.key) {
auction_manager.state.winning_config_states[winner_index].money_pushed_to_accept_payment =
true;
}
let bump_seed = assert_derivation(
program_id,
auction_manager_info,
&[PREFIX.as_bytes(), &auction_manager.auction.as_ref()],
)?;
let authority_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[bump_seed],
];
issue_claim_bid(
auction_program_info.clone(),
auction_info.clone(),
accept_payment_info.clone(),
auction_manager_info.clone(),
bidder_info.clone(),
bidder_pot_info.clone(),
bidder_pot_token_info.clone(),
token_mint_info.clone(),
clock_info.clone(),
token_program_info.clone(),
*vault_info.key,
authority_seeds,
)?;
// Note do not move this above the assert_derivation ... it does something to auction manager
// that causes assert_derivation to get caught in infinite loop...borsh sucks.
auction_manager.serialize(&mut *auction_manager_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,483 @@
use solana_program::msg;
use {
crate::{
error::MetaplexError,
instruction::EmptyPaymentAccountArgs,
state::{AuctionManager, Key, PayoutTicket, Store, MAX_PAYOUT_TICKET_SIZE, PREFIX},
utils::{
assert_derivation, assert_initialized, assert_owned_by, assert_rent_exempt,
create_or_allocate_account_raw, spl_token_transfer,
},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program_error::ProgramError,
program_option::COption,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
},
spl_auction::processor::AuctionData,
spl_token::state::Account,
spl_token_metadata::state::{MasterEdition, Metadata},
spl_token_vault::state::SafetyDepositBox,
std::str::FromStr,
};
fn assert_winning_config_safety_deposit_validity(
auction_manager: &AuctionManager,
safety_deposit: &SafetyDepositBox,
winning_config_index: Option<u8>,
winning_config_item_index: Option<u8>,
) -> ProgramResult {
if let Some(winning_index) = winning_config_index {
let winning_configs = &auction_manager.settings.winning_configs;
if (winning_index as usize) < winning_configs.len() {
let winning_config = &winning_configs[winning_index as usize];
if let Some(item_index) = winning_config_item_index {
if winning_config.items[item_index as usize].safety_deposit_box_index
!= safety_deposit.order
{
return Err(MetaplexError::WinningConfigSafetyDepositMismatch.into());
}
} else {
return Err(MetaplexError::InvalidWinningConfigItemIndex.into());
}
} else {
return Err(MetaplexError::InvalidWinningConfigIndex.into());
}
} else if let Some(participation) = &auction_manager.settings.participation_config {
if participation.safety_deposit_box_index != safety_deposit.order {
return Err(MetaplexError::ParticipationSafetyDepositMismatch.into());
}
} else {
return Err(MetaplexError::ParticipationNotPresent.into());
}
Ok(())
}
fn assert_destination_ownership_validity(
auction_manager: &AuctionManager,
metadata: &Metadata,
destination_info: &AccountInfo,
destination: &Account,
store: &Store,
creator_index: Option<u8>,
) -> ProgramResult {
if let Some(creators) = &metadata.data.creators {
if let Some(index) = creator_index {
if (index as usize) < creators.len() {
let creator = &creators[index as usize];
if destination.owner != creator.address {
return Err(MetaplexError::IncorrectOwner.into());
}
// Let's avoid importing the entire ATA library here just to get a helper and an ID.
// Assert destination is, in fact, an ATA.
assert_derivation(
&Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap(),
destination_info,
&[
creator.address.as_ref(),
&store.token_program.as_ref(),
&destination.mint.as_ref(),
],
)?;
} else {
return Err(MetaplexError::InvalidCreatorIndex.into());
}
} else if destination.owner != auction_manager.authority {
return Err(MetaplexError::IncorrectOwner.into());
}
} else if destination.owner != auction_manager.authority {
return Err(MetaplexError::IncorrectOwner.into());
}
if destination.delegate != COption::None {
return Err(MetaplexError::DelegateShouldBeNone.into());
}
if destination.close_authority != COption::None {
return Err(MetaplexError::CloseAuthorityShouldBeNone.into());
}
Ok(())
}
fn calculate_owed_amount(
auction_manager: &AuctionManager,
auction: &AuctionData,
metadata: &Metadata,
winning_config_index: &Option<u8>,
winning_config_item_index: &Option<u8>,
creator_index: &Option<u8>,
) -> Result<u64, ProgramError> {
let primary_sale_happened = match winning_config_index {
Some(val) => {
if let Some(item_index) = winning_config_item_index {
auction_manager.state.winning_config_states[*val as usize].items
[*item_index as usize]
.primary_sale_happened
} else {
return Err(MetaplexError::InvalidWinningConfigItemIndex.into());
}
}
None => {
if let Some(config) = &auction_manager.state.participation_state {
config.primary_sale_happened
} else {
false
}
}
};
let mut amount_available_to_split: u128 = match winning_config_index {
Some(index) => auction.bid_state.amount(*index as usize) as u128,
None => {
// this means the amount owed is the amount collected from participation nft bids.
if let Some(state) = &auction_manager.state.participation_state {
state.collected_to_accept_payment as u128
} else {
0
}
}
};
if winning_config_index.is_some() {
msg!("Winning config index {:?}", winning_config_index.unwrap());
}
if winning_config_item_index.is_some() {
msg!(
"Winning config item index {:?}",
winning_config_item_index.unwrap()
);
}
if creator_index.is_some() {
msg!("Creator index {:?}", creator_index.unwrap());
}
msg!("Amount available to split {:?}", amount_available_to_split);
let numerator: u128 = match creator_index {
Some(_) => {
if primary_sale_happened {
// during secondary sale, artists get a percentage of the proceeds
metadata.data.seller_fee_basis_points as u128
} else {
// during primary sale, artists get all of the proceeds
10000
}
}
None => {
if primary_sale_happened {
// during secondary sale, auctioneer gets whats left after artists get their cut
(10000 - metadata.data.seller_fee_basis_points) as u128
} else {
// during primary sale, auctioneer (creator index not provided)
// get none of the proceeds
0u128
}
}
};
msg!("Numerator {:?}", numerator);
// Each artist gets a cut of the overall share all artists get. IE if 2 artists contributed and one
// did 70% and the other 30%, the artist further multiplier of A is 7000 and the other is 3000,
// because we convert their shares of 70 and 30 to basis point units of 7000 and 3000.
let artist_further_multiplier = match creator_index {
Some(index) => match &metadata.data.creators {
Some(creators) => (creators[*index as usize].share as u128) * 100u128,
None => return Err(MetaplexError::CreatorIndexExpected.into()),
},
None => 10000,
};
msg!("Artist further multiplier {:?}", artist_further_multiplier);
// Numerator represents the whittling to cut the artist or auctioneer's piece off of the
// total amount available. So if it's the auctioneer and they get 90% in a secondary sale, this would
// be (9000/10000) * bid amount, numerator is 9000. Or if it's the artists collective cut, this would
// be 1000.
amount_available_to_split = amount_available_to_split
.checked_mul(numerator)
.ok_or(MetaplexError::NumericalOverflowError)?;
msg!(
"Amount available to split after numerator mult {:?}",
amount_available_to_split,
);
// Artist further multiplier is the numerator of the fraction that is multiplied for the specific
// artist involved. So if artist A gets 70% of the total artist cut then we'd multiply the
// artist contribution by a further 7/10, so this would be 7000 basis points, so we're doing *7000
// here.
amount_available_to_split = amount_available_to_split
.checked_mul(artist_further_multiplier)
.ok_or(MetaplexError::NumericalOverflowError)?;
msg!(
"Amount available to split after artist further multiplier mult {:?}",
amount_available_to_split,
);
if amount_available_to_split == 0 {
// cant do checked_ceil_div on 0
return Ok(0u64);
}
let proportion_divisor = match winning_config_index {
Some(val) => auction_manager.settings.winning_configs[*val as usize]
.items
.len() as u128,
None => 1,
};
// Since we have multiple prizes need to split each prize's contribution by it's portion of config
let proportional_amount_available_to_split = amount_available_to_split
.checked_div(proportion_divisor)
.ok_or(MetaplexError::NumericalOverflowError)?;
msg!(
"Divided the amount by {:?} to get {:?} due to sharing reward with other prizes",
proportion_divisor,
proportional_amount_available_to_split
);
// We do two 10000's - one for the first numerator/10000 fraction and one for the artist contribution
// For the auctioneer's case, the second 10000 cancels out to 1 because there is no further
// whittling there (auctioneer shares with nobody) but for the artist they may be sharing
// with another artist, say a 70/30 split, so we need to further multiply the amount available by
// 7/10ths or something.
let final_amount_available_to_split = proportional_amount_available_to_split
.checked_div(10000 * 10000)
.ok_or(MetaplexError::NumericalOverflowError)?;
msg!("Final amount mult {:?}", final_amount_available_to_split);
Ok(final_amount_available_to_split as u64)
}
pub fn process_empty_payment_account(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: EmptyPaymentAccountArgs,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let accept_payment_info = next_account_info(account_info_iter)?;
let destination_info = next_account_info(account_info_iter)?;
let auction_manager_info = next_account_info(account_info_iter)?;
let payout_ticket_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let master_edition_info = next_account_info(account_info_iter)?;
let safety_deposit_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(&rent_info)?;
let auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
let store = Store::from_account_info(store_info)?;
let safety_deposit = SafetyDepositBox::from_account_info(safety_deposit_info)?;
let metadata = Metadata::from_account_info(metadata_info)?;
let auction = AuctionData::from_account_info(auction_info)?;
let destination: Account = assert_initialized(destination_info)?;
let accept_payment: Account = assert_initialized(accept_payment_info)?;
if auction_manager.store != *store_info.key {
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
}
msg!(
"At this point, accept payment has {:?} in it",
accept_payment.amount
);
// Before continuing further, assert all bid monies have been pushed to the main escrow
// account so that we have a complete (less the unredeemed participation nft bids) accounting
// to work with
for i in 0..auction.num_winners() {
if !auction_manager.state.winning_config_states[i as usize].money_pushed_to_accept_payment {
return Err(MetaplexError::NotAllBidsClaimed.into());
}
}
if *token_program_info.key != store.token_program {
return Err(MetaplexError::AuctionManagerTokenProgramMismatch.into());
}
assert_owned_by(auction_manager_info, program_id)?;
if !payout_ticket_info.data_is_empty() {
assert_owned_by(payout_ticket_info, program_id)?;
}
assert_owned_by(destination_info, token_program_info.key)?;
assert_owned_by(accept_payment_info, token_program_info.key)?;
assert_owned_by(metadata_info, &store.token_metadata_program)?;
if *master_edition_info.key != solana_program::system_program::id() {
assert_owned_by(master_edition_info, &store.token_metadata_program)?;
}
assert_owned_by(safety_deposit_info, &store.token_vault_program)?;
assert_owned_by(store_info, program_id)?;
assert_owned_by(vault_info, &store.token_vault_program)?;
assert_owned_by(auction_info, &store.auction_program)?;
assert_rent_exempt(rent, destination_info)?;
// Assert the winning config points to the safety deposit you sent up
assert_winning_config_safety_deposit_validity(
&auction_manager,
&safety_deposit,
args.winning_config_index,
args.winning_config_item_index,
)?;
// assert the destination account matches the ownership expected to creator or auction manager authority
// given in the argument's creator index
assert_destination_ownership_validity(
&auction_manager,
&metadata,
destination_info,
&destination,
&store,
args.creator_index,
)?;
// further assert that the vault and safety deposit are correctly matched to the auction manager
if auction_manager.vault != *vault_info.key {
return Err(MetaplexError::AuctionManagerVaultMismatch.into());
}
if auction_manager.auction != *auction_info.key {
return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
}
if safety_deposit.vault != *vault_info.key {
return Err(MetaplexError::SafetyDepositBoxVaultMismatch.into());
}
// assert that the metadata sent up is the metadata in the safety deposit
if metadata.mint != safety_deposit.token_mint {
// Could be a limited edition, in which case printing tokens or auth tokens were offered, not the original.
let master_edition: MasterEdition = MasterEdition::from_account_info(master_edition_info)?;
if master_edition.printing_mint != safety_deposit.token_mint
&& master_edition.one_time_printing_authorization_mint != safety_deposit.token_mint
{
return Err(MetaplexError::SafetyDepositBoxMetadataMismatch.into());
}
}
// make sure the accept payment account is right
if auction_manager.accept_payment != *accept_payment_info.key {
return Err(MetaplexError::AcceptPaymentMismatch.into());
}
if destination.mint != accept_payment.mint {
return Err(MetaplexError::AcceptPaymentMintMismatch.into());
}
let winning_config_index_key: String = match args.winning_config_index {
Some(val) => val.to_string(),
None => "participation".to_owned(),
};
let winning_config_item_index_key: String = match args.winning_config_item_index {
Some(val) => val.to_string(),
None => "0".to_owned(),
};
let creator_index_key: String = match args.creator_index {
Some(val) => val.to_string(),
None => "auctioneer".to_owned(),
};
let payout_bump = assert_derivation(
program_id,
payout_ticket_info,
&[
PREFIX.as_bytes(),
auction_manager_info.key.as_ref(),
winning_config_index_key.as_bytes(),
winning_config_item_index_key.as_bytes(),
creator_index_key.as_bytes(),
&safety_deposit_info.key.as_ref(),
&destination.owner.as_ref(),
],
)?;
let payout_seeds = &[
PREFIX.as_bytes(),
auction_manager_info.key.as_ref(),
winning_config_index_key.as_bytes(),
winning_config_item_index_key.as_bytes(),
creator_index_key.as_bytes(),
&safety_deposit_info.key.as_ref(),
&destination.owner.as_ref(),
&[payout_bump],
];
if payout_ticket_info.data_is_empty() {
create_or_allocate_account_raw(
*program_id,
payout_ticket_info,
rent_info,
system_info,
payer_info,
MAX_PAYOUT_TICKET_SIZE,
payout_seeds,
)?;
}
let mut payout_ticket = PayoutTicket::from_account_info(payout_ticket_info)?;
payout_ticket.recipient = destination.owner;
payout_ticket.key = Key::PayoutTicketV1;
let amount = calculate_owed_amount(
&auction_manager,
&auction,
&metadata,
&args.winning_config_index,
&args.winning_config_item_index,
&args.creator_index,
)?;
let final_amount = amount
.checked_sub(payout_ticket.amount_paid)
.ok_or(MetaplexError::NumericalOverflowError)?;
if final_amount > 0 {
payout_ticket.amount_paid = payout_ticket
.amount_paid
.checked_add(final_amount)
.ok_or(MetaplexError::NumericalOverflowError)?;
let bump_seed = assert_derivation(
program_id,
auction_manager_info,
&[PREFIX.as_bytes(), &auction_manager.auction.as_ref()],
)?;
let authority_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[bump_seed],
];
spl_token_transfer(
accept_payment_info.clone(),
destination_info.clone(),
final_amount,
auction_manager_info.clone(),
authority_seeds,
token_program_info.clone(),
)?;
}
payout_ticket.serialize(&mut *payout_ticket_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,177 @@
use {
crate::{
error::MetaplexError,
state::{
AuctionManager, AuctionManagerSettings, AuctionManagerStatus, Key, ParticipationState,
Store, WinningConfigState, WinningConfigStateItem, MAX_AUCTION_MANAGER_SIZE, PREFIX,
},
utils::{
assert_derivation, assert_initialized, assert_owned_by, create_or_allocate_account_raw,
},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program_option::COption,
pubkey::Pubkey,
},
spl_auction::processor::{AuctionData, AuctionState},
spl_token::state::Account,
spl_token_vault::state::{Vault, VaultState},
};
pub fn process_init_auction_manager(
program_id: &Pubkey,
accounts: &[AccountInfo],
auction_manager_settings: AuctionManagerSettings,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let auction_manager_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let authority_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let accept_payment_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let vault = Vault::from_account_info(vault_info)?;
let auction = AuctionData::from_account_info(auction_info)?;
let accept_payment: Account = assert_initialized(accept_payment_info)?;
// Assert it is real
let store = Store::from_account_info(store_info)?;
assert_owned_by(vault_info, &store.token_vault_program)?;
assert_owned_by(auction_info, &store.auction_program)?;
assert_owned_by(store_info, program_id)?;
assert_owned_by(accept_payment_info, &store.token_program)?;
if auction.state != AuctionState::Created {
return Err(MetaplexError::AuctionMustBeCreated.into());
}
if vault.authority != *auction_manager_info.key {
return Err(MetaplexError::VaultAuthorityMismatch.into());
}
if auction.authority != *auction_manager_info.key {
return Err(MetaplexError::AuctionAuthorityMismatch.into());
}
let bump_seed = assert_derivation(
program_id,
auction_manager_info,
&[PREFIX.as_bytes(), &auction_info.key.as_ref()],
)?;
assert_derivation(
&store.auction_program,
auction_info,
&[
spl_auction::PREFIX.as_bytes(),
&store.auction_program.as_ref(),
&vault_info.key.as_ref(),
],
)?;
if auction.token_mint != accept_payment.mint {
return Err(MetaplexError::AuctionAcceptPaymentMintMismatch.into());
}
if accept_payment.owner != *auction_manager_info.key {
return Err(MetaplexError::AcceptPaymentOwnerMismatch.into());
}
if accept_payment.delegate != COption::None {
return Err(MetaplexError::DelegateShouldBeNone.into());
}
if accept_payment.close_authority != COption::None {
return Err(MetaplexError::CloseAuthorityShouldBeNone.into());
}
if vault.state != VaultState::Combined {
return Err(MetaplexError::VaultNotCombined.into());
}
if vault.token_type_count == 0 {
return Err(MetaplexError::VaultCannotEmpty.into());
}
let mut winning_config_states: Vec<WinningConfigState> = vec![];
let mut winning_item_count: u8 = 0;
for winning_config in &auction_manager_settings.winning_configs {
let mut winning_config_state_items = vec![];
let mut safety_deposit_box_found_lookup: Vec<bool> = vec![];
for _ in 0..vault.token_type_count {
safety_deposit_box_found_lookup.push(false)
}
for item in &winning_config.items {
// If this blows then they have more than 255 total items which is unacceptable in current impl
winning_item_count = winning_item_count
.checked_add(1)
.ok_or(MetaplexError::NumericalOverflowError)?;
// Should never have same deposit index appear twice in one config.
let lookup = safety_deposit_box_found_lookup[item.safety_deposit_box_index as usize];
if lookup {
return Err(MetaplexError::DuplicateWinningConfigItemDetected.into());
} else {
safety_deposit_box_found_lookup[item.safety_deposit_box_index as usize] = true
}
if item.safety_deposit_box_index > vault.token_type_count {
return Err(MetaplexError::InvalidSafetyDepositBox.into());
}
winning_config_state_items.push(WinningConfigStateItem {
claimed: false,
primary_sale_happened: false,
})
}
winning_config_states.push(WinningConfigState {
items: winning_config_state_items,
money_pushed_to_accept_payment: false,
})
}
let authority_seeds = &[PREFIX.as_bytes(), &auction_info.key.as_ref(), &[bump_seed]];
create_or_allocate_account_raw(
*program_id,
auction_manager_info,
rent_info,
system_info,
payer_info,
MAX_AUCTION_MANAGER_SIZE,
authority_seeds,
)?;
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
auction_manager.key = Key::AuctionManagerV1;
auction_manager.store = *store_info.key;
auction_manager.state.status = AuctionManagerStatus::Initialized;
auction_manager.settings = auction_manager_settings;
auction_manager.vault = *vault_info.key;
auction_manager.auction = *auction_info.key;
auction_manager.authority = *authority_info.key;
auction_manager.accept_payment = *accept_payment_info.key;
auction_manager.state.winning_config_items_validated = 0;
auction_manager.state.winning_config_states = winning_config_states;
if auction_manager.settings.participation_config.is_some() {
auction_manager.state.participation_state = Some(ParticipationState {
collected_to_accept_payment: 0,
validated: false,
primary_sale_happened: false,
printing_authorization_token_account: None,
})
}
auction_manager.serialize(&mut *auction_manager_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,278 @@
use {
crate::{
error::MetaplexError,
state::{
AuctionManager, NonWinningConstraint, ParticipationConfig, Store, WinningConstraint,
PREFIX,
},
utils::{
assert_derivation, assert_initialized, assert_owned_by,
assert_store_safety_vault_manager_match, transfer_safety_deposit_box_items,
},
},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
},
spl_auction::processor::{AuctionData, AuctionDataExtended, AuctionState},
spl_token::{instruction::close_account, state::Account},
spl_token_metadata::{instruction::mint_printing_tokens_via_token, state::MasterEdition},
spl_token_vault::state::SafetyDepositBox,
};
fn mint_printing_tokens<'a: 'b, 'b>(
program: &AccountInfo<'a>,
destination: &AccountInfo<'a>,
token: &AccountInfo<'a>,
one_time_printing_authorization_mint: &AccountInfo<'a>,
printing_mint: &AccountInfo<'a>,
burn_authority: &AccountInfo<'a>,
metadata: &AccountInfo<'a>,
master_edition: &AccountInfo<'a>,
token_program_info: &AccountInfo<'a>,
rent_info: &AccountInfo<'a>,
supply: u64,
authority_signer_seeds: &'b [&'b [u8]],
) -> ProgramResult {
let result = invoke_signed(
&mint_printing_tokens_via_token(
*program.key,
*destination.key,
*token.key,
*one_time_printing_authorization_mint.key,
*printing_mint.key,
*burn_authority.key,
*metadata.key,
*master_edition.key,
supply,
),
&[
program.clone(),
destination.clone(),
token.clone(),
one_time_printing_authorization_mint.clone(),
printing_mint.clone(),
burn_authority.clone(),
master_edition.clone(),
metadata.clone(),
token_program_info.clone(),
rent_info.clone(),
],
&[authority_signer_seeds],
);
result.map_err(|_| MetaplexError::PrintingAuthorizationTokensFailed.into())
}
#[allow(clippy::unnecessary_cast)]
#[allow(clippy::absurd_extreme_comparisons)]
pub fn process_populate_participation_printing_account<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let safety_deposit_token_store_info = next_account_info(account_info_iter)?;
let transient_one_time_holding_info = next_account_info(account_info_iter)?;
let participation_printing_holding_account_info = next_account_info(account_info_iter)?;
let one_time_printing_authorization_mint_info = next_account_info(account_info_iter)?;
let printing_mint_info = next_account_info(account_info_iter)?;
let safety_deposit_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let fraction_mint_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let auction_extended_info = next_account_info(account_info_iter)?;
let auction_manager_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let token_vault_program_info = next_account_info(account_info_iter)?;
let token_metadata_program_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let master_edition_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let transfer_authority_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
let safety_deposit = SafetyDepositBox::from_account_info(safety_deposit_info)?;
let safety_deposit_token_store: Account = assert_initialized(&safety_deposit_token_store_info)?;
let auction = AuctionData::from_account_info(auction_info)?;
let auction_extended = AuctionDataExtended::from_account_info(auction_extended_info)?;
let master_edition = MasterEdition::from_account_info(master_edition_info)?;
let transient_one_time_auth_holding_account: Account =
assert_initialized(transient_one_time_holding_info)?;
let participation_printing_account: Account =
assert_initialized(participation_printing_holding_account_info)?;
let store = Store::from_account_info(store_info)?;
let config: &ParticipationConfig;
if let Some(part_config) = &auction_manager.settings.participation_config {
config = part_config
} else {
return Err(MetaplexError::NotEligibleForParticipation.into());
}
if auction_manager.auction != *auction_info.key {
return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
}
if auction.state != AuctionState::Ended {
return Err(MetaplexError::AuctionHasNotEnded.into());
}
assert_store_safety_vault_manager_match(
&auction_manager,
&safety_deposit_info,
&vault_info,
&store.token_vault_program,
)?;
assert_owned_by(transient_one_time_holding_info, token_program_info.key)?;
assert_owned_by(safety_deposit_token_store_info, token_program_info.key)?;
assert_owned_by(
participation_printing_holding_account_info,
token_program_info.key,
)?;
assert_owned_by(
one_time_printing_authorization_mint_info,
token_program_info.key,
)?;
assert_owned_by(printing_mint_info, token_program_info.key)?;
assert_owned_by(safety_deposit_info, &store.token_vault_program)?;
assert_owned_by(vault_info, &store.token_vault_program)?;
assert_owned_by(fraction_mint_info, token_program_info.key)?;
assert_owned_by(auction_info, &store.auction_program)?;
assert_owned_by(auction_extended_info, &store.auction_program)?;
assert_owned_by(auction_manager_info, program_id)?;
assert_owned_by(store_info, program_id)?;
assert_owned_by(master_edition_info, &store.token_metadata_program)?;
assert_owned_by(metadata_info, &store.token_metadata_program)?;
if transient_one_time_auth_holding_account.owner != *auction_manager_info.key {
return Err(MetaplexError::IncorrectOwner.into());
}
if transient_one_time_auth_holding_account.mint
!= master_edition.one_time_printing_authorization_mint
{
return Err(MetaplexError::TransientAuthAccountMintMismatch.into());
}
if store.token_program != *token_program_info.key {
return Err(MetaplexError::TokenProgramMismatch.into());
}
if store.token_vault_program != *token_vault_program_info.key {
return Err(MetaplexError::TokenProgramMismatch.into());
}
if store.token_metadata_program != *token_metadata_program_info.key {
return Err(MetaplexError::TokenProgramMismatch.into());
}
if master_edition.one_time_printing_authorization_mint != safety_deposit.token_mint {
return Err(MetaplexError::SafetyDepositBoxMasterEditionOneTimeAuthMintMismatch.into());
}
if master_edition.one_time_printing_authorization_mint
!= *one_time_printing_authorization_mint_info.key
{
return Err(MetaplexError::MasterEditionOneTimeAuthMintMismatch.into());
}
if master_edition.printing_mint != *printing_mint_info.key {
return Err(MetaplexError::MasterEditionMintMismatch.into());
}
if let Some(state) = &auction_manager.state.participation_state {
if let Some(token) = state.printing_authorization_token_account {
if *participation_printing_holding_account_info.key != token {
return Err(MetaplexError::PrintingAuthorizationTokenAccountMismatch.into());
}
}
}
assert_derivation(
&store.auction_program,
auction_extended_info,
&[
spl_auction::PREFIX.as_bytes(),
store.auction_program.as_ref(),
vault_info.key.as_ref(),
spl_auction::EXTENDED.as_bytes(),
],
)?;
if participation_printing_account.amount == 0 && safety_deposit_token_store.amount > 0 {
let auction_bump_seed = assert_derivation(
program_id,
auction_manager_info,
&[PREFIX.as_bytes(), &auction_manager.auction.as_ref()],
)?;
let auction_auth_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[auction_bump_seed],
];
transfer_safety_deposit_box_items(
token_vault_program_info.clone(),
transient_one_time_holding_info.clone(),
safety_deposit_info.clone(),
safety_deposit_token_store_info.clone(),
vault_info.clone(),
fraction_mint_info.clone(),
auction_manager_info.clone(),
transfer_authority_info.clone(),
rent_info.clone(),
1,
auction_auth_seeds,
)?;
let mut amount_to_mint = auction_extended.total_uncancelled_bids;
if config.winner_constraint == WinningConstraint::NoParticipationPrize {
amount_to_mint = amount_to_mint
.checked_sub(auction.num_winners())
.ok_or(MetaplexError::NumericalOverflowError)?;
} else if config.non_winning_constraint == NonWinningConstraint::NoParticipationPrize {
amount_to_mint = auction.num_winners();
}
mint_printing_tokens(
token_metadata_program_info,
participation_printing_holding_account_info,
transient_one_time_holding_info,
one_time_printing_authorization_mint_info,
printing_mint_info,
auction_manager_info,
metadata_info,
master_edition_info,
token_program_info,
rent_info,
amount_to_mint,
auction_auth_seeds,
)?;
// Close transient to save sol for payer
invoke_signed(
&close_account(
token_program_info.key,
transient_one_time_holding_info.key,
payer_info.key,
auction_manager_info.key,
&[auction_manager_info.key],
)?,
&[
token_program_info.clone(),
transient_one_time_holding_info.clone(),
payer_info.clone(),
auction_manager_info.clone(),
],
&[auction_auth_seeds],
)?;
}
Ok(())
}

View File

@ -0,0 +1,225 @@
use {
crate::{
error::MetaplexError,
state::{AuctionManager, WinningConfigItem, WinningConfigType, PREFIX},
utils::{
assert_derivation, common_redeem_checks, common_redeem_finish,
common_winning_config_checks, transfer_safety_deposit_box_items, CommonRedeemCheckArgs,
CommonRedeemFinishArgs, CommonRedeemReturn, CommonWinningConfigCheckReturn,
},
},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
},
spl_auction::processor::AuctionData,
spl_token_metadata::{
instruction::set_reservation_list,
state::{get_reservation_list, Reservation},
},
};
#[allow(clippy::too_many_arguments)]
pub fn reserve_list_if_needed<'a>(
program_id: &'a Pubkey,
auction_manager: &AuctionManager,
auction: &AuctionData,
winning_config_item: &WinningConfigItem,
master_edition_info: &AccountInfo<'a>,
reservation_list_info: &AccountInfo<'a>,
auction_manager_info: &AccountInfo<'a>,
signer_seeds: &[&[u8]],
) -> ProgramResult {
let reservation_list = get_reservation_list(reservation_list_info)?;
if reservation_list.supply_snapshot().is_none() {
let mut reservations: Vec<Reservation> = vec![];
// Auction specifically does not expose internal state workings as it may change someday,
// but it does expose a point get-winner-at-index method. Right now this is just array access
// but may be invocation someday. It's inefficient style but better for the interface maintenance
// in the long run if we move to better storage solutions (so that this action doesnt need to change if
// storage does.)
for n in 0..auction_manager.settings.winning_configs.len() {
match auction.winner_at(n) {
Some(address) => {
let spots: u64 = auction_manager.settings.winning_configs[n]
.items
.iter()
.filter(|i| {
i.safety_deposit_box_index
== winning_config_item.safety_deposit_box_index
})
.map(|i| i.amount as u64)
.sum();
reservations.push(Reservation {
address,
// Select all items in a winning config matching the same safety deposit box
// as the one being redeemed here (likely only one)
// and then sum them to get the total spots to reserve for this winner
spots_remaining: spots,
total_spots: spots,
})
}
None => break,
}
}
invoke_signed(
&set_reservation_list(
*program_id,
*master_edition_info.key,
*reservation_list_info.key,
*auction_manager_info.key,
reservations,
),
&[
master_edition_info.clone(),
reservation_list_info.clone(),
auction_manager_info.clone(),
],
&[&signer_seeds],
)?;
}
Ok(())
}
pub fn process_redeem_bid<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let auction_manager_info = next_account_info(account_info_iter)?;
let safety_deposit_token_store_info = next_account_info(account_info_iter)?;
let destination_info = next_account_info(account_info_iter)?;
let bid_redemption_info = next_account_info(account_info_iter)?;
let safety_deposit_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let fraction_mint_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let bidder_metadata_info = next_account_info(account_info_iter)?;
let bidder_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let token_vault_program_info = next_account_info(account_info_iter)?;
let token_metadata_program_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let transfer_authority_info = next_account_info(account_info_iter)?;
let CommonRedeemReturn {
auction_manager,
redemption_bump_seed,
bidder_metadata,
auction,
rent: _rent,
win_index,
token_metadata_program: _t,
} = common_redeem_checks(CommonRedeemCheckArgs {
program_id,
auction_manager_info,
safety_deposit_token_store_info,
destination_info,
bid_redemption_info,
safety_deposit_info,
vault_info,
auction_info,
bidder_metadata_info,
bidder_info,
token_program_info,
token_vault_program_info,
token_metadata_program_info,
rent_info,
store_info,
is_participation: false,
})?;
let mut winning_item_index = None;
if !bidder_metadata.cancelled {
if let Some(winning_index) = win_index {
if winning_index < auction_manager.settings.winning_configs.len() {
// Okay, so they placed in the auction winning prizes section!
let CommonWinningConfigCheckReturn {
winning_config_item,
winning_item_index: wii,
} = common_winning_config_checks(
&auction_manager,
&safety_deposit_info,
winning_index,
)?;
winning_item_index = wii;
if winning_config_item.winning_config_type != WinningConfigType::TokenOnlyTransfer
&& winning_config_item.winning_config_type != WinningConfigType::Printing
{
return Err(MetaplexError::WrongBidEndpointForPrize.into());
}
let auction_bump_seed = assert_derivation(
program_id,
auction_manager_info,
&[PREFIX.as_bytes(), &auction_manager.auction.as_ref()],
)?;
let auction_auth_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[auction_bump_seed],
];
if winning_config_item.winning_config_type == WinningConfigType::Printing {
let master_edition_info = next_account_info(account_info_iter)?;
let reservation_list_info = next_account_info(account_info_iter)?;
reserve_list_if_needed(
token_metadata_program_info.key,
&auction_manager,
&auction,
&winning_config_item,
master_edition_info,
reservation_list_info,
auction_manager_info,
auction_auth_seeds,
)?;
}
transfer_safety_deposit_box_items(
token_vault_program_info.clone(),
destination_info.clone(),
safety_deposit_info.clone(),
safety_deposit_token_store_info.clone(),
vault_info.clone(),
fraction_mint_info.clone(),
auction_manager_info.clone(),
transfer_authority_info.clone(),
rent_info.clone(),
winning_config_item.amount as u64,
auction_auth_seeds,
)?;
}
}
}
common_redeem_finish(CommonRedeemFinishArgs {
program_id,
auction_manager,
auction_manager_info,
bidder_metadata_info,
rent_info,
system_info,
payer_info,
bid_redemption_info,
winning_index: win_index,
redemption_bump_seed,
bid_redeemed: true,
participation_redeemed: false,
winning_item_index,
})?;
Ok(())
}

View File

@ -0,0 +1,148 @@
use {
crate::{
error::MetaplexError,
state::{WinningConfigType, PREFIX},
utils::{
assert_owned_by, common_redeem_checks, common_redeem_finish,
common_winning_config_checks, transfer_metadata_ownership,
transfer_safety_deposit_box_items, CommonRedeemCheckArgs, CommonRedeemFinishArgs,
CommonRedeemReturn, CommonWinningConfigCheckReturn,
},
},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
},
};
pub fn process_full_rights_transfer_bid<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let auction_manager_info = next_account_info(account_info_iter)?;
let safety_deposit_token_store_info = next_account_info(account_info_iter)?;
let destination_info = next_account_info(account_info_iter)?;
let bid_redemption_info = next_account_info(account_info_iter)?;
let safety_deposit_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let fraction_mint_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let bidder_metadata_info = next_account_info(account_info_iter)?;
let bidder_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let token_vault_program_info = next_account_info(account_info_iter)?;
let token_metadata_program_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let new_metadata_authority_info = next_account_info(account_info_iter)?;
let transfer_authority_info = next_account_info(account_info_iter)?;
let CommonRedeemReturn {
auction_manager,
redemption_bump_seed,
bidder_metadata,
auction: _a,
rent: _rent,
win_index,
token_metadata_program,
} = common_redeem_checks(CommonRedeemCheckArgs {
program_id,
auction_manager_info,
safety_deposit_token_store_info,
destination_info,
bid_redemption_info,
safety_deposit_info,
vault_info,
auction_info,
bidder_metadata_info,
bidder_info,
token_program_info,
token_vault_program_info,
token_metadata_program_info,
store_info,
rent_info,
is_participation: false,
})?;
assert_owned_by(metadata_info, &token_metadata_program)?;
let mut winning_item_index = None;
if !bidder_metadata.cancelled {
if let Some(winning_index) = win_index {
if winning_index < auction_manager.settings.winning_configs.len() {
let CommonWinningConfigCheckReturn {
winning_config_item,
winning_item_index: wii,
} = common_winning_config_checks(
&auction_manager,
&safety_deposit_info,
winning_index,
)?;
winning_item_index = wii;
if winning_config_item.winning_config_type != WinningConfigType::FullRightsTransfer
{
return Err(MetaplexError::WrongBidEndpointForPrize.into());
}
// Someone is selling off their master edition. We need to transfer it, as well as ownership of their
// metadata.
let auction_seeds = &[PREFIX.as_bytes(), &auction_manager.auction.as_ref()];
let (_, auction_bump_seed) =
Pubkey::find_program_address(auction_seeds, &program_id);
let auction_authority_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[auction_bump_seed],
];
transfer_metadata_ownership(
token_metadata_program_info.clone(),
metadata_info.clone(),
auction_manager_info.clone(),
new_metadata_authority_info.clone(),
auction_authority_seeds,
)?;
transfer_safety_deposit_box_items(
token_vault_program_info.clone(),
destination_info.clone(),
safety_deposit_info.clone(),
safety_deposit_token_store_info.clone(),
vault_info.clone(),
fraction_mint_info.clone(),
auction_manager_info.clone(),
transfer_authority_info.clone(),
rent_info.clone(),
1,
auction_authority_seeds,
)?;
}
}
};
common_redeem_finish(CommonRedeemFinishArgs {
program_id,
auction_manager,
auction_manager_info,
bidder_metadata_info,
rent_info,
system_info,
payer_info,
bid_redemption_info,
redemption_bump_seed,
winning_index: win_index,
bid_redeemed: true,
participation_redeemed: false,
winning_item_index,
})?;
Ok(())
}

View File

@ -0,0 +1,205 @@
use {
crate::{
error::MetaplexError,
state::{
NonWinningConstraint, ParticipationConfig, ParticipationState, WinningConstraint,
PREFIX,
},
utils::{
assert_initialized, assert_owned_by, common_redeem_checks, common_redeem_finish,
spl_token_transfer, CommonRedeemCheckArgs, CommonRedeemFinishArgs, CommonRedeemReturn,
},
},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
},
spl_token::state::Account,
};
#[allow(clippy::unnecessary_cast)]
#[allow(clippy::absurd_extreme_comparisons)]
pub fn process_redeem_participation_bid<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let auction_manager_info = next_account_info(account_info_iter)?;
let safety_deposit_token_store_info = next_account_info(account_info_iter)?;
let destination_info = next_account_info(account_info_iter)?;
let bid_redemption_info = next_account_info(account_info_iter)?;
let safety_deposit_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
// We keep it here to keep API base identical to the other redeem calls for ease of use by callers
let _fraction_mint_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let bidder_metadata_info = next_account_info(account_info_iter)?;
let bidder_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let token_vault_program_info = next_account_info(account_info_iter)?;
let token_metadata_program_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let transfer_authority_info = next_account_info(account_info_iter)?;
let accept_payment_info = next_account_info(account_info_iter)?;
let bidder_token_account_info = next_account_info(account_info_iter)?;
let participation_printing_holding_account_info = next_account_info(account_info_iter)?;
let CommonRedeemReturn {
mut auction_manager,
redemption_bump_seed,
bidder_metadata,
auction,
rent: _rent,
win_index,
token_metadata_program: _t,
} = common_redeem_checks(CommonRedeemCheckArgs {
program_id,
auction_manager_info,
safety_deposit_token_store_info,
destination_info,
bid_redemption_info,
safety_deposit_info,
vault_info,
auction_info,
bidder_metadata_info,
bidder_info,
token_program_info,
token_vault_program_info,
token_metadata_program_info,
rent_info,
store_info,
is_participation: true,
})?;
assert_owned_by(accept_payment_info, token_program_info.key)?;
assert_owned_by(bidder_token_account_info, token_program_info.key)?;
assert_owned_by(
participation_printing_holding_account_info,
token_program_info.key,
)?;
let participation_printing_account: Account =
assert_initialized(participation_printing_holding_account_info)?;
if participation_printing_account.amount == 0 {
return Err(MetaplexError::ParticipationPrintingEmpty.into());
}
if let Some(state) = &auction_manager.state.participation_state {
if let Some(token) = state.printing_authorization_token_account {
if *participation_printing_holding_account_info.key != token {
return Err(MetaplexError::PrintingAuthorizationTokenAccountMismatch.into());
}
}
}
let bidder_token: Account = assert_initialized(bidder_token_account_info)?;
if bidder_token.mint != auction.token_mint {
return Err(MetaplexError::AcceptPaymentMintMismatch.into());
}
if *accept_payment_info.key != auction_manager.accept_payment {
return Err(MetaplexError::AcceptPaymentMismatch.into());
}
let config: &ParticipationConfig;
if let Some(part_config) = &auction_manager.settings.participation_config {
config = part_config
} else {
return Err(MetaplexError::NotEligibleForParticipation.into());
}
let mut gets_participation =
config.non_winning_constraint != NonWinningConstraint::NoParticipationPrize;
if !bidder_metadata.cancelled {
if let Some(winning_index) = auction.is_winner(bidder_info.key) {
if winning_index < auction_manager.settings.winning_configs.len() {
// Okay, so they placed in the auction winning prizes section!
gets_participation =
config.winner_constraint == WinningConstraint::ParticipationPrizeGiven;
}
}
}
if gets_participation {
let seeds = &[PREFIX.as_bytes(), &auction_manager.auction.as_ref()];
let (_, bump_seed) = Pubkey::find_program_address(seeds, &program_id);
let mint_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[bump_seed],
];
spl_token_transfer(
participation_printing_holding_account_info.clone(),
destination_info.clone(),
1,
auction_manager_info.clone(),
mint_seeds,
token_program_info.clone(),
)?;
let mut price: u64 = 0;
if win_index.is_none() {
if let Some(fixed_price) = config.fixed_price {
price = fixed_price;
} else if config.non_winning_constraint == NonWinningConstraint::GivenForBidPrice {
price = bidder_metadata.last_bid;
}
}
if bidder_token.amount.saturating_sub(price) < 0 as u64 {
return Err(MetaplexError::NotEnoughBalanceForParticipation.into());
}
if price > 0 {
if let Some(state) = &auction_manager.state.participation_state {
// Can't really edit something behind an Option reference...
// just make new one.
auction_manager.state.participation_state = Some(ParticipationState {
collected_to_accept_payment: state
.collected_to_accept_payment
.checked_add(price)
.ok_or(MetaplexError::NumericalOverflowError)?,
primary_sale_happened: state.primary_sale_happened,
validated: state.validated,
printing_authorization_token_account: state
.printing_authorization_token_account,
});
}
spl_token_transfer(
bidder_token_account_info.clone(),
accept_payment_info.clone(),
price,
transfer_authority_info.clone(),
&[],
token_program_info.clone(),
)?;
}
} else {
return Err(MetaplexError::NotEligibleForParticipation.into());
}
common_redeem_finish(CommonRedeemFinishArgs {
program_id,
auction_manager,
auction_manager_info,
bidder_metadata_info,
rent_info,
system_info,
payer_info,
bid_redemption_info,
winning_index: None,
redemption_bump_seed,
bid_redeemed: false,
participation_redeemed: true,
winning_item_index: None,
})?;
Ok(())
}

View File

@ -0,0 +1,90 @@
use {
crate::{
error::MetaplexError,
state::{Key, Store, MAX_STORE_SIZE, PREFIX},
utils::{
assert_derivation, assert_owned_by, assert_signer, create_or_allocate_account_raw,
},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
},
};
pub fn process_set_store<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
public: bool,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let store_info = next_account_info(account_info_iter)?;
let admin_wallet_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let token_vault_program_info = next_account_info(account_info_iter)?;
let token_metadata_program_info = next_account_info(account_info_iter)?;
let auction_program_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
assert_signer(payer_info)?;
assert_signer(admin_wallet_info)?;
if !store_info.data_is_empty() {
assert_owned_by(store_info, program_id)?;
}
let store_bump = assert_derivation(
program_id,
store_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
admin_wallet_info.key.as_ref(),
],
)?;
if store_info.data_is_empty() {
create_or_allocate_account_raw(
*program_id,
store_info,
rent_info,
system_info,
payer_info,
MAX_STORE_SIZE,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
admin_wallet_info.key.as_ref(),
&[store_bump],
],
)?;
}
let mut store = Store::from_account_info(store_info)?;
store.key = Key::StoreV1;
store.public = public;
// Keys can only be set once, once set from all 0s, they are immutable.
if store.token_program == solana_program::system_program::id() {
store.token_program = *token_program_info.key;
}
if store.token_program != spl_token::id() {
return Err(MetaplexError::InvalidTokenProgram.into());
}
if store.token_vault_program == solana_program::system_program::id() {
store.token_vault_program = *token_vault_program_info.key;
}
if store.token_metadata_program == solana_program::system_program::id() {
store.token_metadata_program = *token_metadata_program_info.key;
}
if store.auction_program == solana_program::system_program::id() {
store.auction_program = *auction_program_info.key;
}
store.serialize(&mut *store_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,84 @@
use {
crate::{
state::{Key, WhitelistedCreator, MAX_WHITELISTED_CREATOR_SIZE, PREFIX},
utils::{
assert_derivation, assert_owned_by, assert_signer, create_or_allocate_account_raw,
},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
},
};
pub fn process_set_whitelisted_creator<'a>(
program_id: &'a Pubkey,
accounts: &'a [AccountInfo<'a>],
activated: bool,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let whitelisted_creator_info = next_account_info(account_info_iter)?;
let admin_wallet_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let creator_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
assert_signer(payer_info)?;
assert_signer(admin_wallet_info)?;
if !whitelisted_creator_info.data_is_empty() {
assert_owned_by(whitelisted_creator_info, program_id)?;
}
assert_owned_by(store_info, program_id)?;
assert_derivation(
program_id,
store_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
admin_wallet_info.key.as_ref(),
],
)?;
let creator_bump = assert_derivation(
program_id,
whitelisted_creator_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
store_info.key.as_ref(),
creator_info.key.as_ref(),
],
)?;
if whitelisted_creator_info.data_is_empty() {
create_or_allocate_account_raw(
*program_id,
whitelisted_creator_info,
rent_info,
system_info,
payer_info,
MAX_WHITELISTED_CREATOR_SIZE,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
store_info.key.as_ref(),
creator_info.key.as_ref(),
&[creator_bump],
],
)?;
}
let mut whitelisted_creator = WhitelistedCreator::from_account_info(whitelisted_creator_info)?;
whitelisted_creator.key = Key::WhitelistedCreatorV1;
whitelisted_creator.address = *creator_info.key;
whitelisted_creator.activated = activated;
whitelisted_creator.serialize(&mut *whitelisted_creator_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,93 @@
use {
crate::{
error::MetaplexError,
state::{AuctionManager, AuctionManagerStatus, Store, PREFIX},
utils::{assert_authority_correct, assert_owned_by},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
},
spl_auction::instruction::{start_auction_instruction, StartAuctionArgs},
};
pub fn issue_start_auction<'a>(
auction_program: AccountInfo<'a>,
authority: AccountInfo<'a>,
auction: AccountInfo<'a>,
clock: AccountInfo<'a>,
vault: Pubkey,
signer_seeds: &[&[u8]],
) -> ProgramResult {
invoke_signed(
&start_auction_instruction(
*auction_program.key,
*authority.key,
StartAuctionArgs { resource: vault },
),
&[auction_program, authority, auction, clock],
&[&signer_seeds],
)?;
Ok(())
}
pub fn process_start_auction(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let auction_manager_info = next_account_info(account_info_iter)?;
let auction_info = next_account_info(account_info_iter)?;
let authority_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let auction_program_info = next_account_info(account_info_iter)?;
let clock_info = next_account_info(account_info_iter)?;
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
let store = Store::from_account_info(store_info)?;
assert_authority_correct(&auction_manager, authority_info)?;
assert_owned_by(auction_info, &store.auction_program)?;
assert_owned_by(auction_manager_info, program_id)?;
assert_owned_by(store_info, program_id)?;
if auction_manager.store != *store_info.key {
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
}
if auction_manager.auction != *auction_info.key {
return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
}
if store.auction_program != *auction_program_info.key {
return Err(MetaplexError::AuctionManagerAuctionProgramMismatch.into());
}
if auction_manager.state.status != AuctionManagerStatus::Validated {
return Err(MetaplexError::AuctionManagerMustBeValidated.into());
}
let seeds = &[PREFIX.as_bytes(), &auction_manager.auction.as_ref()];
let (_, bump_seed) = Pubkey::find_program_address(seeds, &program_id);
let authority_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[bump_seed],
];
issue_start_auction(
auction_program_info.clone(),
auction_manager_info.clone(),
auction_info.clone(),
clock_info.clone(),
auction_manager.vault,
authority_seeds,
)?;
auction_manager.state.status = AuctionManagerStatus::Running;
auction_manager.serialize(&mut *auction_manager_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,173 @@
use {
crate::{
error::MetaplexError,
state::{AuctionManager, AuctionManagerStatus, ParticipationState, Store},
utils::{
assert_at_least_one_creator_matches_or_store_public_and_all_verified,
assert_authority_correct, assert_derivation, assert_initialized, assert_owned_by,
assert_rent_exempt, assert_store_safety_vault_manager_match,
},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program_option::COption,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
},
spl_token::state::Account,
spl_token_metadata::state::{MasterEdition, Metadata},
spl_token_vault::state::{SafetyDepositBox, Vault},
};
pub fn process_validate_participation(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let auction_manager_info = next_account_info(account_info_iter)?;
let open_edition_metadata_info = next_account_info(account_info_iter)?;
let open_master_edition_info = next_account_info(account_info_iter)?;
let printing_authorization_token_account_info = next_account_info(account_info_iter)?;
let authority_info = next_account_info(account_info_iter)?;
let whitelisted_creator_info = next_account_info(account_info_iter)?;
let store_info = next_account_info(account_info_iter)?;
let safety_deposit_box_info = next_account_info(account_info_iter)?;
let safety_deposit_box_token_store_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(&rent_info)?;
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
let store = Store::from_account_info(store_info)?;
let vault = Vault::from_account_info(vault_info)?;
let safety_deposit_token_store: Account =
assert_initialized(safety_deposit_box_token_store_info)?;
let safety_deposit = SafetyDepositBox::from_account_info(safety_deposit_box_info)?;
let printing_token_account: Account =
assert_initialized(printing_authorization_token_account_info)?;
let open_edition_metadata = Metadata::from_account_info(open_edition_metadata_info)?;
let master_edition = MasterEdition::from_account_info(open_master_edition_info)?;
// top level authority and ownership check
assert_authority_correct(&auction_manager, authority_info)?;
assert_owned_by(auction_manager_info, program_id)?;
assert_owned_by(open_edition_metadata_info, &store.token_metadata_program)?;
assert_owned_by(open_master_edition_info, &store.token_metadata_program)?;
assert_owned_by(
printing_authorization_token_account_info,
&store.token_program,
)?;
if *whitelisted_creator_info.key != solana_program::system_program::id() {
if whitelisted_creator_info.data_is_empty() {
return Err(MetaplexError::Uninitialized.into());
}
assert_owned_by(whitelisted_creator_info, program_id)?;
}
assert_owned_by(store_info, program_id)?;
assert_owned_by(safety_deposit_box_info, &store.token_vault_program)?;
assert_owned_by(safety_deposit_box_token_store_info, &store.token_program)?;
assert_owned_by(vault_info, &store.token_vault_program)?;
// is it the right vault, safety deposit, and token store?
assert_store_safety_vault_manager_match(
&auction_manager,
&safety_deposit_box_info,
vault_info,
&store.token_vault_program,
)?;
// do the vault and store belong to this AM?
if auction_manager.store != *store_info.key {
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
}
if auction_manager.vault != *vault_info.key {
return Err(MetaplexError::AuctionManagerVaultMismatch.into());
}
// Check creators
assert_at_least_one_creator_matches_or_store_public_and_all_verified(
program_id,
&auction_manager,
&open_edition_metadata,
whitelisted_creator_info,
store_info,
)?;
// Make sure master edition is the right master edition for this metadata given
assert_derivation(
&store.token_metadata_program,
open_master_edition_info,
&[
spl_token_metadata::state::PREFIX.as_bytes(),
store.token_metadata_program.as_ref(),
&open_edition_metadata.mint.as_ref(),
spl_token_metadata::state::EDITION.as_bytes(),
],
)?;
// Assert the holding account for authorization tokens is rent filled, owned correctly, and ours
assert_owned_by(
printing_authorization_token_account_info,
&store.token_program,
)?;
assert_rent_exempt(rent, printing_authorization_token_account_info)?;
if printing_token_account.owner != *auction_manager_info.key {
return Err(MetaplexError::IncorrectOwner.into());
}
if printing_token_account.mint != master_edition.printing_mint {
return Err(MetaplexError::PrintingTokenAccountMintMismatch.into());
}
if printing_token_account.delegate != COption::None {
return Err(MetaplexError::DelegateShouldBeNone.into());
}
if printing_token_account.close_authority != COption::None {
return Err(MetaplexError::CloseAuthorityShouldBeNone.into());
}
if master_edition.max_supply.is_some() {
return Err(MetaplexError::CantUseLimitedSupplyEditionsWithOpenEditionAuction.into());
}
if master_edition.one_time_printing_authorization_mint != safety_deposit_token_store.mint {
return Err(MetaplexError::MasterEditionOneTimeAuthorizationMintMismatch.into());
}
if let Some(participation_config) = &auction_manager.settings.participation_config {
if participation_config.safety_deposit_box_index > vault.token_type_count {
return Err(MetaplexError::InvalidSafetyDepositBox.into());
}
if participation_config.safety_deposit_box_index != safety_deposit.order {
return Err(MetaplexError::SafetyDepositIndexMismatch.into());
}
if let Some(state) = auction_manager.state.participation_state {
if state.validated {
return Err(MetaplexError::AlreadyValidated.into());
}
auction_manager.state.participation_state = Some(ParticipationState {
collected_to_accept_payment: state.collected_to_accept_payment,
primary_sale_happened: open_edition_metadata.primary_sale_happened,
validated: true,
printing_authorization_token_account: Some(
*printing_authorization_token_account_info.key,
),
});
}
if auction_manager.settings.winning_configs.is_empty() {
auction_manager.state.status = AuctionManagerStatus::Validated;
}
auction_manager.serialize(&mut *auction_manager_info.data.borrow_mut())?;
}
Ok(())
}

View File

@ -0,0 +1,347 @@
use {
crate::{
error::MetaplexError,
state::{
AuctionManager, AuctionManagerStatus, Key, OriginalAuthorityLookup,
SafetyDepositValidationTicket, Store, WinningConfigType, MAX_AUTHORITY_LOOKUP_SIZE,
MAX_VALIDATION_TICKET_SIZE, PREFIX,
},
utils::{
assert_at_least_one_creator_matches_or_store_public_and_all_verified,
assert_authority_correct, assert_derivation, assert_initialized, assert_owned_by,
assert_store_safety_vault_manager_match, create_or_allocate_account_raw,
transfer_metadata_ownership,
},
},
borsh::BorshSerialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
},
spl_token::state::{Account, Mint},
spl_token_metadata::{
state::{MasterEdition, Metadata},
utils::assert_update_authority_is_correct,
},
spl_token_vault::state::{SafetyDepositBox, Vault},
};
pub fn make_safety_deposit_validation<'a>(
program_id: &Pubkey,
auction_manager_info: &AccountInfo<'a>,
safety_deposit_info: &AccountInfo<'a>,
safety_deposit_validation_ticket_info: &AccountInfo<'a>,
payer_info: &AccountInfo<'a>,
rent_info: &AccountInfo<'a>,
system_info: &AccountInfo<'a>,
) -> ProgramResult {
let bump = assert_derivation(
program_id,
safety_deposit_validation_ticket_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
auction_manager_info.key.as_ref(),
safety_deposit_info.key.as_ref(),
],
)?;
create_or_allocate_account_raw(
*program_id,
safety_deposit_validation_ticket_info,
rent_info,
system_info,
payer_info,
MAX_VALIDATION_TICKET_SIZE,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
auction_manager_info.key.as_ref(),
safety_deposit_info.key.as_ref(),
&[bump],
],
)?;
let mut validation =
SafetyDepositValidationTicket::from_account_info(safety_deposit_validation_ticket_info)?;
validation.key = Key::SafetyDepositValidationTicketV1;
validation.address = *safety_deposit_info.key;
validation.serialize(&mut *safety_deposit_validation_ticket_info.data.borrow_mut())?;
Ok(())
}
pub fn process_validate_safety_deposit_box(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let safety_deposit_validation_ticket_info = next_account_info(account_info_iter)?;
let auction_manager_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let original_authority_lookup_info = next_account_info(account_info_iter)?;
let whitelisted_creator_info = next_account_info(account_info_iter)?;
let auction_manager_store_info = next_account_info(account_info_iter)?;
let safety_deposit_info = next_account_info(account_info_iter)?;
let safety_deposit_token_store_info = next_account_info(account_info_iter)?;
let mint_info = next_account_info(account_info_iter)?;
let edition_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let authority_info = next_account_info(account_info_iter)?;
let metadata_authority_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let token_metadata_program_info = next_account_info(account_info_iter)?;
let system_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
if !safety_deposit_validation_ticket_info.data_is_empty() {
return Err(MetaplexError::AlreadyValidated.into());
}
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
let safety_deposit = SafetyDepositBox::from_account_info(safety_deposit_info)?;
let safety_deposit_token_store: Account = assert_initialized(safety_deposit_token_store_info)?;
let metadata = Metadata::from_account_info(metadata_info)?;
let store = Store::from_account_info(auction_manager_store_info)?;
// Is it a real vault?
let _vault = Vault::from_account_info(vault_info)?;
// Is it a real mint?
let _mint: Mint = assert_initialized(mint_info)?;
assert_owned_by(auction_manager_info, program_id)?;
assert_owned_by(metadata_info, &store.token_metadata_program)?;
if !original_authority_lookup_info.data_is_empty() {
return Err(MetaplexError::AlreadyInitialized.into());
}
if *whitelisted_creator_info.key != solana_program::system_program::id() {
if whitelisted_creator_info.data_is_empty() {
return Err(MetaplexError::Uninitialized.into());
}
assert_owned_by(whitelisted_creator_info, program_id)?;
}
assert_owned_by(auction_manager_store_info, program_id)?;
assert_owned_by(safety_deposit_info, &store.token_vault_program)?;
assert_owned_by(safety_deposit_token_store_info, &store.token_program)?;
assert_owned_by(mint_info, &store.token_program)?;
assert_owned_by(edition_info, &store.token_metadata_program)?;
assert_owned_by(vault_info, &store.token_vault_program)?;
if *token_metadata_program_info.key != store.token_metadata_program {
return Err(MetaplexError::AuctionManagerTokenMetadataMismatch.into());
}
assert_authority_correct(&auction_manager, authority_info)?;
assert_store_safety_vault_manager_match(
&auction_manager,
&safety_deposit_info,
vault_info,
&store.token_vault_program,
)?;
assert_at_least_one_creator_matches_or_store_public_and_all_verified(
program_id,
&auction_manager,
&metadata,
whitelisted_creator_info,
auction_manager_store_info,
)?;
if auction_manager.store != *auction_manager_store_info.key {
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
}
if *mint_info.key != safety_deposit.token_mint {
return Err(MetaplexError::SafetyDepositBoxMintMismatch.into());
}
if *token_metadata_program_info.key != store.token_metadata_program {
return Err(MetaplexError::AuctionManagerTokenMetadataProgramMismatch.into());
}
// We want to ensure that the mint you are using with this token is one
// we can actually transfer to and from using our token program invocations, which
// we can check by asserting ownership by the token program we recorded in init.
if *mint_info.owner != store.token_program {
return Err(MetaplexError::TokenProgramMismatch.into());
}
let mut total_amount_requested: u64 = 0;
// At this point we know we have at least one config and they may have different amounts but all
// point at the same safety deposit box and so have the same winning config type.
// We default to TokenOnlyTransfer but this will get set by the loop.
let mut winning_config_type: WinningConfigType = WinningConfigType::TokenOnlyTransfer;
let mut winning_config_items_validated: u8 = 0;
let mut all_winning_config_items: u8 = 0;
for i in 0..auction_manager.settings.winning_configs.len() {
let possible_config = &auction_manager.settings.winning_configs[i];
for j in 0..possible_config.items.len() {
let possible_item = &possible_config.items[j];
all_winning_config_items = all_winning_config_items
.checked_add(1)
.ok_or(MetaplexError::NumericalOverflowError)?;
if possible_item.safety_deposit_box_index == safety_deposit.order {
winning_config_type = possible_item.winning_config_type;
winning_config_items_validated = winning_config_items_validated
.checked_add(1)
.ok_or(MetaplexError::NumericalOverflowError)?;
// Build array to sum total amount
total_amount_requested =
match total_amount_requested.checked_add(possible_item.amount.into()) {
Some(val) => val,
None => return Err(MetaplexError::NumericalOverflowError.into()),
};
// Record that primary sale happened at time of validation for later royalties reconcilation
auction_manager.state.winning_config_states[i].items[j].primary_sale_happened =
metadata.primary_sale_happened;
}
}
}
if total_amount_requested == 0 {
return Err(MetaplexError::SafetyDepositBoxNotUsedInAuction.into());
}
let edition_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
store.token_metadata_program.as_ref(),
&metadata.mint.as_ref(),
spl_token_metadata::state::EDITION.as_bytes(),
];
let (edition_key, _) =
Pubkey::find_program_address(edition_seeds, &store.token_metadata_program);
let seeds = &[PREFIX.as_bytes(), &auction_manager.auction.as_ref()];
let (_, bump_seed) = Pubkey::find_program_address(seeds, &program_id);
let authority_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
&[bump_seed],
];
// Supply logic check
match winning_config_type {
WinningConfigType::FullRightsTransfer => {
assert_update_authority_is_correct(&metadata, metadata_authority_info)?;
if safety_deposit.token_mint != metadata.mint {
return Err(MetaplexError::SafetyDepositBoxMetadataMismatch.into());
}
if edition_key != *edition_info.key {
return Err(MetaplexError::InvalidEditionAddress.into());
}
if safety_deposit_token_store.amount != 1 {
return Err(MetaplexError::StoreIsEmpty.into());
}
let original_authority_lookup_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
metadata_info.key.as_ref(),
];
let (expected_key, original_bump_seed) =
Pubkey::find_program_address(original_authority_lookup_seeds, &program_id);
let original_authority_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
metadata_info.key.as_ref(),
&[original_bump_seed],
];
if expected_key != *original_authority_lookup_info.key {
return Err(MetaplexError::OriginalAuthorityLookupKeyMismatch.into());
}
// We may need to transfer authority back, or to the new owner, so we need to keep track
// of original ownership
create_or_allocate_account_raw(
*program_id,
original_authority_lookup_info,
rent_info,
system_info,
payer_info,
MAX_AUTHORITY_LOOKUP_SIZE,
original_authority_seeds,
)?;
let mut original_authority_lookup =
OriginalAuthorityLookup::from_account_info(original_authority_lookup_info)?;
original_authority_lookup.key = Key::OriginalAuthorityLookupV1;
original_authority_lookup.original_authority = *metadata_authority_info.key;
transfer_metadata_ownership(
token_metadata_program_info.clone(),
metadata_info.clone(),
metadata_authority_info.clone(),
auction_manager_info.clone(),
authority_seeds,
)?;
original_authority_lookup
.serialize(&mut *original_authority_lookup_info.data.borrow_mut())?;
}
WinningConfigType::TokenOnlyTransfer => {
if safety_deposit.token_mint != metadata.mint {
return Err(MetaplexError::SafetyDepositBoxMetadataMismatch.into());
}
if safety_deposit_token_store.amount < total_amount_requested {
return Err(MetaplexError::NotEnoughTokensToSupplyWinners.into());
}
}
WinningConfigType::Printing => {
if edition_key != *edition_info.key {
return Err(MetaplexError::InvalidEditionAddress.into());
}
let master_edition = MasterEdition::from_account_info(edition_info)?;
if safety_deposit.token_mint != master_edition.printing_mint {
return Err(MetaplexError::SafetyDepositBoxMasterMintMismatch.into());
}
if safety_deposit_token_store.amount != total_amount_requested {
return Err(MetaplexError::NotEnoughTokensToSupplyWinners.into());
}
}
}
auction_manager.state.winning_config_items_validated = match auction_manager
.state
.winning_config_items_validated
.checked_add(winning_config_items_validated)
{
Some(val) => val,
None => return Err(MetaplexError::NumericalOverflowError.into()),
};
if auction_manager.state.winning_config_items_validated == all_winning_config_items {
let mut participation_okay = true;
if let Some(state) = &auction_manager.state.participation_state {
participation_okay = state.validated
}
if participation_okay {
auction_manager.state.status = AuctionManagerStatus::Validated
}
}
auction_manager.serialize(&mut *auction_manager_info.data.borrow_mut())?;
make_safety_deposit_validation(
program_id,
auction_manager_info,
safety_deposit_info,
safety_deposit_validation_ticket_info,
payer_info,
rent_info,
system_info,
)?;
Ok(())
}

View File

@ -0,0 +1,361 @@
use {
crate::utils::try_from_slice_checked,
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey},
};
/// prefix used for PDAs to avoid certain collision attacks (https://en.wikipedia.org/wiki/Collision_attack#Chosen-prefix_collision_attack)
pub const PREFIX: &str = "metaplex";
pub const MAX_WINNERS: usize = 200;
pub const MAX_WINNER_SIZE: usize = 6 * MAX_WINNERS;
// Add 150 padding for future keys and booleans
// DONT TRUST MEM SIZE OF! IT DOESNT SIZE THINGS PROPERLY! TRUST YOUR OWN MIND AND ITS COUNTING ABILITY!
pub const MAX_AUCTION_MANAGER_SIZE: usize = 1 + // key
32 + // store
32 + // authority
32 + // auction
32 + // vault
32 + // accept_payment
1 + //status
1 + // winning configs validated
8 + // u64 borsh uses to determine number of elements in winning config state vec
8 + // u64 for numbr of elements in winning config state items
MAX_WINNER_SIZE + // total number of bytes for max possible use between WinnerConfig and WinnerConfigStates
// for all winner places.
1 + // Whether or not participation state exists
8 + // participation_collected_to_accept_payment
1 + // Whether or not participation is a primary sale'd metadata or not at time of auction
1 + // was participation validated
32 + // participation printing token holding account pubkey
8 + // u64 borsh uses to determine number of elements in winning config vec
8 + // u64 for number of items in winning config items vec
1 + // Whether or not participation config exists
1 + // participation winner constraint
1 + // participation non winner constraint
1 + // u8 participation_config's safety deposit box index
9 + // option<u64> participation fixed price in borsh is a u8 for option and actual u64
150; // padding;
// Add padding for future booleans/enums
pub const MAX_STORE_SIZE: usize = 2 + 32 + 32 + 32 + 32 + 100;
pub const MAX_WHITELISTED_CREATOR_SIZE: usize = 2 + 32 + 10;
pub const MAX_PAYOUT_TICKET_SIZE: usize = 1 + 32 + 8;
pub const MAX_VALIDATION_TICKET_SIZE: usize = 1 + 32 + 10;
pub const MAX_BID_REDEMPTION_TICKET_SIZE: usize = 3;
pub const MAX_AUTHORITY_LOOKUP_SIZE: usize = 33;
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug, Copy)]
pub enum Key {
Uninitialized,
OriginalAuthorityLookupV1,
BidRedemptionTicketV1,
StoreV1,
WhitelistedCreatorV1,
PayoutTicketV1,
SafetyDepositValidationTicketV1,
AuctionManagerV1,
}
/// An Auction Manager can support an auction that is an English auction and limited edition and open edition
/// all at once. Need to support all at once. We use u8 keys to point to safety deposit indices in Vault
/// as opposed to the pubkeys to save on space. Ordering of safety deposits is guaranteed fixed by vault
/// implementation.
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
pub struct AuctionManager {
pub key: Key,
pub store: Pubkey,
pub authority: Pubkey,
pub auction: Pubkey,
pub vault: Pubkey,
pub accept_payment: Pubkey,
pub state: AuctionManagerState,
pub settings: AuctionManagerSettings,
}
impl AuctionManager {
pub fn from_account_info(a: &AccountInfo) -> Result<AuctionManager, ProgramError> {
let am: AuctionManager = try_from_slice_checked(
&a.data.borrow_mut(),
Key::AuctionManagerV1,
MAX_AUCTION_MANAGER_SIZE,
)?;
Ok(am)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
pub struct AuctionManagerState {
pub status: AuctionManagerStatus,
/// When all configs are validated the auction is started and auction manager moves to Running
pub winning_config_items_validated: u8,
pub winning_config_states: Vec<WinningConfigState>,
pub participation_state: Option<ParticipationState>,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
pub struct AuctionManagerSettings {
/// The safety deposit box index in the vault containing the winning items, in order of place
/// The same index can appear multiple times if that index contains n tokens for n appearances (this will be checked)
pub winning_configs: Vec<WinningConfig>,
/// The participation config is separated because it is structurally a bit different,
/// having different options and also because it has no real "winning place" in the array.
pub participation_config: Option<ParticipationConfig>,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub struct ParticipationState {
/// We have this variable below to keep track in the case of the participation NFTs, whose
/// income will trickle in over time, how much the artists have in the escrow account and
/// how much would/should be owed to them if they try to claim it relative to the winning bids.
/// It's abit tougher than a straightforward bid which has a price attached to it, because
/// there are many bids of differing amounts (in the case of GivenForBidPrice) and they dont all
/// come in at one time, so this little ledger here keeps track.
pub collected_to_accept_payment: u64,
/// Record of primary sale or not at time of auction creation, set during validation step
pub primary_sale_happened: bool,
pub validated: bool,
/// An account for printing authorization tokens that are made with the one time use token
/// after the auction ends. Provided during validation step.
pub printing_authorization_token_account: Option<Pubkey>,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub struct ParticipationConfig {
/// Setups:
/// 1. Winners get participation + not charged extra
/// 2. Winners dont get participation prize
pub winner_constraint: WinningConstraint,
/// Setups:
/// 1. Losers get prize for free
/// 2. Losers get prize but pay fixed price
/// 3. Losers get prize but pay bid price
pub non_winning_constraint: NonWinningConstraint,
/// The safety deposit box index in the vault containing the template for the participation prize
pub safety_deposit_box_index: u8,
/// Setting this field disconnects the participation prizes price from the bid. Any bid you submit, regardless
/// of amount, charges you the same fixed price.
pub fixed_price: Option<u64>,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub enum WinningConstraint {
NoParticipationPrize,
ParticipationPrizeGiven,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub enum NonWinningConstraint {
NoParticipationPrize,
GivenForFixedPrice,
GivenForBidPrice,
}
#[repr(C)]
#[derive(Clone, PartialEq, BorshSerialize, BorshDeserialize, Copy, Debug)]
pub enum WinningConfigType {
/// You may be selling your one-of-a-kind NFT for the first time, but not it's accompanying Metadata,
/// of which you would like to retain ownership. You get 100% of the payment the first sale, then
/// royalties forever after.
///
/// You may be re-selling something like a Limited/Open Edition print from another auction,
/// a master edition record token by itself (Without accompanying metadata/printing ownership), etc.
/// This means artists will get royalty fees according to the top level royalty % on the metadata
/// split according to their percentages of contribution.
///
/// No metadata ownership is transferred in this instruction, which means while you may be transferring
/// the token for a limited/open edition away, you would still be (nominally) the owner of the limited edition
/// metadata, though it confers no rights or privileges of any kind.
TokenOnlyTransfer,
/// Means you are auctioning off the master edition record and it's metadata ownership as well as the
/// token itself. The other person will be able to mint authorization tokens and make changes to the
/// artwork.
FullRightsTransfer,
/// Means you are using authorization tokens to print off editions during the auction
Printing,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
pub struct WinningConfig {
// For now these are just array-of-array proxies but wanted to make them first class
// structs in case we want to attach other top level metadata someday.
pub items: Vec<WinningConfigItem>,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
pub struct WinningConfigState {
pub items: Vec<WinningConfigStateItem>,
/// Ticked to true when money is pushed to accept_payment account from auction bidding pot
pub money_pushed_to_accept_payment: bool,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy, Debug)]
pub struct WinningConfigItem {
pub safety_deposit_box_index: u8,
pub amount: u8,
pub winning_config_type: WinningConfigType,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy, Debug)]
pub struct WinningConfigStateItem {
/// Record of primary sale or not at time of auction creation, set during validation step
pub primary_sale_happened: bool,
/// Ticked to true when a prize is claimed by person who won it
pub claimed: bool,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq)]
pub enum AuctionManagerStatus {
Initialized,
Validated,
Running,
Disbursing,
Finished,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy)]
pub struct OriginalAuthorityLookup {
pub key: Key,
pub original_authority: Pubkey,
}
impl OriginalAuthorityLookup {
pub fn from_account_info(a: &AccountInfo) -> Result<OriginalAuthorityLookup, ProgramError> {
let pt: OriginalAuthorityLookup = try_from_slice_checked(
&a.data.borrow_mut(),
Key::OriginalAuthorityLookupV1,
MAX_AUTHORITY_LOOKUP_SIZE,
)?;
Ok(pt)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy)]
pub struct BidRedemptionTicket {
pub key: Key,
pub participation_redeemed: bool,
pub items_redeemed: u8,
}
impl BidRedemptionTicket {
pub fn from_account_info(a: &AccountInfo) -> Result<BidRedemptionTicket, ProgramError> {
let pt: BidRedemptionTicket = try_from_slice_checked(
&a.data.borrow_mut(),
Key::BidRedemptionTicketV1,
MAX_BID_REDEMPTION_TICKET_SIZE,
)?;
Ok(pt)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy)]
pub struct PayoutTicket {
pub key: Key,
pub recipient: Pubkey,
pub amount_paid: u64,
}
impl PayoutTicket {
pub fn from_account_info(a: &AccountInfo) -> Result<PayoutTicket, ProgramError> {
let pt: PayoutTicket = try_from_slice_checked(
&a.data.borrow_mut(),
Key::PayoutTicketV1,
MAX_PAYOUT_TICKET_SIZE,
)?;
Ok(pt)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy)]
pub struct Store {
pub key: Key,
pub public: bool,
pub auction_program: Pubkey,
pub token_vault_program: Pubkey,
pub token_metadata_program: Pubkey,
pub token_program: Pubkey,
}
impl Store {
pub fn from_account_info(a: &AccountInfo) -> Result<Store, ProgramError> {
let store: Store =
try_from_slice_checked(&a.data.borrow_mut(), Key::StoreV1, MAX_STORE_SIZE)?;
Ok(store)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy)]
pub struct WhitelistedCreator {
pub key: Key,
pub address: Pubkey,
pub activated: bool,
}
impl WhitelistedCreator {
pub fn from_account_info(a: &AccountInfo) -> Result<WhitelistedCreator, ProgramError> {
let wc: WhitelistedCreator = try_from_slice_checked(
&a.data.borrow_mut(),
Key::WhitelistedCreatorV1,
MAX_WHITELISTED_CREATOR_SIZE,
)?;
Ok(wc)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Copy)]
pub struct SafetyDepositValidationTicket {
pub key: Key,
pub address: Pubkey,
}
impl SafetyDepositValidationTicket {
pub fn from_account_info(
a: &AccountInfo,
) -> Result<SafetyDepositValidationTicket, ProgramError> {
let store: SafetyDepositValidationTicket = try_from_slice_checked(
&a.data.borrow_mut(),
Key::SafetyDepositValidationTicketV1,
MAX_VALIDATION_TICKET_SIZE,
)?;
Ok(store)
}
}

View File

@ -0,0 +1,780 @@
use {
crate::{
error::MetaplexError,
state::{
AuctionManager, AuctionManagerStatus, BidRedemptionTicket, Key,
OriginalAuthorityLookup, Store, WhitelistedCreator, WinningConfigItem,
MAX_BID_REDEMPTION_TICKET_SIZE, PREFIX,
},
},
arrayref::array_ref,
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::AccountInfo,
borsh::try_from_slice_unchecked,
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
},
spl_auction::processor::{AuctionData, AuctionState, BidderMetadata},
spl_token::instruction::{set_authority, AuthorityType},
spl_token_metadata::{
instruction::update_metadata_accounts,
state::{Metadata, EDITION},
},
spl_token_vault::instruction::create_withdraw_tokens_instruction,
std::convert::TryInto,
};
/// assert initialized account
pub fn assert_initialized<T: Pack + IsInitialized>(
account_info: &AccountInfo,
) -> Result<T, ProgramError> {
let account: T = T::unpack_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(MetaplexError::Uninitialized.into())
} else {
Ok(account)
}
}
pub fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult {
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
Err(MetaplexError::NotRentExempt.into())
} else {
Ok(())
}
}
pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> ProgramResult {
if account.owner != owner {
Err(MetaplexError::IncorrectOwner.into())
} else {
Ok(())
}
}
pub fn assert_signer(account_info: &AccountInfo) -> ProgramResult {
if !account_info.is_signer {
Err(ProgramError::MissingRequiredSignature)
} else {
Ok(())
}
}
pub fn assert_store_safety_vault_manager_match(
auction_manager: &AuctionManager,
safety_deposit_info: &AccountInfo,
vault_info: &AccountInfo,
token_vault_program: &Pubkey,
) -> ProgramResult {
if auction_manager.vault != *vault_info.key {
return Err(MetaplexError::AuctionManagerVaultMismatch.into());
}
let data = safety_deposit_info.data.borrow();
let vault_key = Pubkey::new_from_array(*array_ref![data, 1, 32]);
let token_mint_key = Pubkey::new_from_array(*array_ref![data, 33, 32]);
assert_derivation(
&token_vault_program,
safety_deposit_info,
&[
spl_token_vault::state::PREFIX.as_bytes(),
vault_info.key.as_ref(),
token_mint_key.as_ref(),
],
)?;
if *vault_info.key != vault_key {
return Err(MetaplexError::SafetyDepositBoxVaultMismatch.into());
}
Ok(())
}
pub fn assert_at_least_one_creator_matches_or_store_public_and_all_verified(
program_id: &Pubkey,
auction_manager: &AuctionManager,
metadata: &Metadata,
whitelisted_creator_info: &AccountInfo,
store_info: &AccountInfo,
) -> ProgramResult {
let store = Store::from_account_info(store_info)?;
if store.public {
return Ok(());
}
if let Some(creators) = &metadata.data.creators {
// does it exist? It better!
let existing_whitelist_creator: WhitelistedCreator =
match WhitelistedCreator::from_account_info(whitelisted_creator_info) {
Ok(val) => val,
Err(_) => return Err(MetaplexError::InvalidWhitelistedCreator.into()),
};
if !existing_whitelist_creator.activated {
return Err(MetaplexError::WhitelistedCreatorInactive.into());
}
let mut found = false;
for creator in creators {
// Now find at least one creator that can make this pda in the list
let (key, _) = Pubkey::find_program_address(
&[
PREFIX.as_bytes(),
program_id.as_ref(),
auction_manager.store.as_ref(),
creator.address.as_ref(),
],
program_id,
);
if key == *whitelisted_creator_info.key {
found = true;
}
if !creator.verified {
return Err(MetaplexError::CreatorHasNotVerifiedMetadata.into());
}
}
if found {
return Ok(());
}
}
Err(MetaplexError::InvalidWhitelistedCreator.into())
}
pub fn assert_authority_correct(
auction_manager: &AuctionManager,
authority_info: &AccountInfo,
) -> ProgramResult {
if auction_manager.authority != *authority_info.key {
return Err(MetaplexError::AuctionManagerAuthorityMismatch.into());
}
assert_signer(authority_info)?;
Ok(())
}
/// Create account almost from scratch, lifted from
/// https://github.com/solana-labs/solana-program-library/blob/7d4873c61721aca25464d42cc5ef651a7923ca79/associated-token-account/program/src/processor.rs#L51-L98
#[inline(always)]
pub fn create_or_allocate_account_raw<'a>(
program_id: Pubkey,
new_account_info: &AccountInfo<'a>,
rent_sysvar_info: &AccountInfo<'a>,
system_program_info: &AccountInfo<'a>,
payer_info: &AccountInfo<'a>,
size: usize,
signer_seeds: &[&[u8]],
) -> Result<(), ProgramError> {
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let required_lamports = rent
.minimum_balance(size)
.max(1)
.saturating_sub(new_account_info.lamports());
if required_lamports > 0 {
msg!("Transfer {} lamports to the new account", required_lamports);
invoke(
&system_instruction::transfer(&payer_info.key, new_account_info.key, required_lamports),
&[
payer_info.clone(),
new_account_info.clone(),
system_program_info.clone(),
],
)?;
}
msg!("Allocate space for the account");
invoke_signed(
&system_instruction::allocate(new_account_info.key, size.try_into().unwrap()),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
msg!("Assign the account to the owning program");
invoke_signed(
&system_instruction::assign(new_account_info.key, &program_id),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
msg!("Completed assignation!");
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn transfer_safety_deposit_box_items<'a>(
token_vault_program: AccountInfo<'a>,
destination: AccountInfo<'a>,
safety_deposit_box: AccountInfo<'a>,
safety_deposit_token_store: AccountInfo<'a>,
vault: AccountInfo<'a>,
fraction_mint: AccountInfo<'a>,
vault_authority: AccountInfo<'a>,
transfer_authority: AccountInfo<'a>,
rent: AccountInfo<'a>,
amount: u64,
signer_seeds: &[&[u8]],
) -> ProgramResult {
invoke_signed(
&create_withdraw_tokens_instruction(
*token_vault_program.key,
*destination.key,
*safety_deposit_box.key,
*safety_deposit_token_store.key,
*vault.key,
*fraction_mint.key,
*vault_authority.key,
*transfer_authority.key,
amount,
),
&[
token_vault_program,
destination,
safety_deposit_box,
safety_deposit_token_store,
vault,
fraction_mint,
vault_authority,
transfer_authority,
rent,
],
&[&signer_seeds],
)?;
Ok(())
}
pub fn transfer_metadata_ownership<'a>(
token_metadata_program: AccountInfo<'a>,
metadata_info: AccountInfo<'a>,
update_authority: AccountInfo<'a>,
new_update_authority: AccountInfo<'a>,
signer_seeds: &[&[u8]],
) -> ProgramResult {
invoke_signed(
&update_metadata_accounts(
*token_metadata_program.key,
*metadata_info.key,
*update_authority.key,
Some(*new_update_authority.key),
None,
Some(true),
),
&[
update_authority,
new_update_authority,
metadata_info,
token_metadata_program,
],
&[&signer_seeds],
)?;
Ok(())
}
pub fn transfer_mint_authority<'a>(
new_authority_seeds: &[&[u8]],
new_authority_key: &Pubkey,
new_authority_info: &AccountInfo<'a>,
mint_info: &AccountInfo<'a>,
mint_authority_info: &AccountInfo<'a>,
token_program_info: &AccountInfo<'a>,
) -> ProgramResult {
msg!("Setting mint authority");
invoke_signed(
&set_authority(
token_program_info.key,
mint_info.key,
Some(new_authority_key),
AuthorityType::MintTokens,
mint_authority_info.key,
&[&mint_authority_info.key],
)
.unwrap(),
&[
mint_authority_info.clone(),
mint_info.clone(),
token_program_info.clone(),
new_authority_info.clone(),
],
&[new_authority_seeds],
)?;
msg!("Setting freeze authority");
invoke_signed(
&set_authority(
token_program_info.key,
mint_info.key,
Some(&new_authority_key),
AuthorityType::FreezeAccount,
mint_authority_info.key,
&[&mint_authority_info.key],
)
.unwrap(),
&[
mint_authority_info.clone(),
mint_info.clone(),
token_program_info.clone(),
new_authority_info.clone(),
],
&[new_authority_seeds],
)?;
Ok(())
}
pub struct CommonRedeemReturn {
pub redemption_bump_seed: u8,
pub auction_manager: AuctionManager,
pub auction: AuctionData,
pub bidder_metadata: BidderMetadata,
pub rent: Rent,
pub win_index: Option<usize>,
pub token_metadata_program: Pubkey,
}
pub struct CommonRedeemCheckArgs<'a> {
pub program_id: &'a Pubkey,
pub auction_manager_info: &'a AccountInfo<'a>,
pub safety_deposit_token_store_info: &'a AccountInfo<'a>,
pub destination_info: &'a AccountInfo<'a>,
pub bid_redemption_info: &'a AccountInfo<'a>,
pub safety_deposit_info: &'a AccountInfo<'a>,
pub vault_info: &'a AccountInfo<'a>,
pub auction_info: &'a AccountInfo<'a>,
pub bidder_metadata_info: &'a AccountInfo<'a>,
pub bidder_info: &'a AccountInfo<'a>,
pub token_program_info: &'a AccountInfo<'a>,
pub token_vault_program_info: &'a AccountInfo<'a>,
pub token_metadata_program_info: &'a AccountInfo<'a>,
pub store_info: &'a AccountInfo<'a>,
pub rent_info: &'a AccountInfo<'a>,
pub is_participation: bool,
}
#[allow(clippy::too_many_arguments)]
pub fn common_redeem_checks(
args: CommonRedeemCheckArgs,
) -> Result<CommonRedeemReturn, ProgramError> {
let CommonRedeemCheckArgs {
program_id,
auction_manager_info,
safety_deposit_token_store_info,
destination_info,
bid_redemption_info,
safety_deposit_info,
vault_info,
auction_info,
bidder_metadata_info,
bidder_info,
token_program_info,
token_vault_program_info,
token_metadata_program_info,
rent_info,
store_info,
is_participation,
} = args;
let rent = &Rent::from_account_info(&rent_info)?;
let mut auction_manager: AuctionManager =
AuctionManager::from_account_info(auction_manager_info)?;
let auction = AuctionData::from_account_info(auction_info)?;
let store_data = store_info.data.borrow();
let bidder_metadata = BidderMetadata::from_account_info(bidder_metadata_info)?;
let win_index = auction.is_winner(bidder_info.key);
if !bid_redemption_info.data_is_empty() {
let bid_redemption: BidRedemptionTicket =
BidRedemptionTicket::from_account_info(bid_redemption_info)?;
let possible_items_to_redeem = match win_index {
Some(val) => auction_manager.settings.winning_configs[val].items.len(),
None => 0,
};
if (is_participation && bid_redemption.participation_redeemed)
|| (!is_participation
&& bid_redemption.items_redeemed == possible_items_to_redeem as u8)
{
return Err(MetaplexError::BidAlreadyRedeemed.into());
}
}
let auction_program = Pubkey::new_from_array(*array_ref![store_data, 2, 32]);
let token_vault_program = Pubkey::new_from_array(*array_ref![store_data, 34, 32]);
let token_metadata_program = Pubkey::new_from_array(*array_ref![store_data, 66, 32]);
let token_program = Pubkey::new_from_array(*array_ref![store_data, 98, 32]);
assert_signer(bidder_info)?;
assert_owned_by(&destination_info, token_program_info.key)?;
assert_owned_by(&auction_manager_info, &program_id)?;
assert_owned_by(safety_deposit_token_store_info, token_program_info.key)?;
if !bid_redemption_info.data_is_empty() {
assert_owned_by(bid_redemption_info, &program_id)?;
}
assert_owned_by(safety_deposit_info, &token_vault_program)?;
assert_owned_by(vault_info, &token_vault_program)?;
assert_owned_by(auction_info, &auction_program)?;
assert_owned_by(bidder_metadata_info, &auction_program)?;
assert_owned_by(store_info, &program_id)?;
assert_store_safety_vault_manager_match(
&auction_manager,
&safety_deposit_info,
&vault_info,
&token_vault_program,
)?;
// looking out for you!
assert_rent_exempt(rent, &destination_info)?;
if auction_manager.auction != *auction_info.key {
return Err(MetaplexError::AuctionManagerAuctionMismatch.into());
}
if *store_info.key != auction_manager.store {
return Err(MetaplexError::AuctionManagerStoreMismatch.into());
}
if token_program != *token_program_info.key {
return Err(MetaplexError::AuctionManagerTokenProgramMismatch.into());
}
if token_vault_program != *token_vault_program_info.key {
return Err(MetaplexError::AuctionManagerTokenVaultProgramMismatch.into());
}
if token_metadata_program != *token_metadata_program_info.key {
return Err(MetaplexError::AuctionManagerTokenMetadataProgramMismatch.into());
}
if auction.state != AuctionState::Ended {
return Err(MetaplexError::AuctionHasNotEnded.into());
}
// No-op if already set.
auction_manager.state.status = AuctionManagerStatus::Disbursing;
let redemption_path = [
PREFIX.as_bytes(),
auction_manager.auction.as_ref(),
bidder_metadata_info.key.as_ref(),
];
let (redemption_key, redemption_bump_seed) =
Pubkey::find_program_address(&redemption_path, &program_id);
if redemption_key != *bid_redemption_info.key {
return Err(MetaplexError::BidRedemptionMismatch.into());
}
assert_derivation(
&auction_program,
bidder_metadata_info,
&[
spl_auction::PREFIX.as_bytes(),
auction_program.as_ref(),
auction_info.key.as_ref(),
bidder_info.key.as_ref(),
"metadata".as_bytes(),
],
)?;
if bidder_metadata.bidder_pubkey != *bidder_info.key {
return Err(MetaplexError::BidderMetadataBidderMismatch.into());
}
Ok(CommonRedeemReturn {
redemption_bump_seed,
auction_manager,
auction,
bidder_metadata,
rent: *rent,
win_index,
token_metadata_program,
})
}
pub struct CommonRedeemFinishArgs<'a> {
pub program_id: &'a Pubkey,
pub auction_manager: AuctionManager,
pub auction_manager_info: &'a AccountInfo<'a>,
pub bidder_metadata_info: &'a AccountInfo<'a>,
pub rent_info: &'a AccountInfo<'a>,
pub system_info: &'a AccountInfo<'a>,
pub payer_info: &'a AccountInfo<'a>,
pub bid_redemption_info: &'a AccountInfo<'a>,
pub winning_index: Option<usize>,
pub redemption_bump_seed: u8,
pub bid_redeemed: bool,
pub participation_redeemed: bool,
pub winning_item_index: Option<usize>,
}
#[allow(clippy::too_many_arguments)]
pub fn common_redeem_finish(args: CommonRedeemFinishArgs) -> ProgramResult {
let CommonRedeemFinishArgs {
program_id,
mut auction_manager,
auction_manager_info,
bidder_metadata_info,
rent_info,
system_info,
payer_info,
bid_redemption_info,
winning_index,
redemption_bump_seed,
bid_redeemed,
participation_redeemed,
winning_item_index,
} = args;
if bid_redeemed {
if let Some(index) = winning_index {
if let Some(item_index) = winning_item_index {
auction_manager.state.winning_config_states[index].items[item_index].claimed = true;
}
}
}
let mut bid_redemption: BidRedemptionTicket;
if bid_redeemed || participation_redeemed {
let redemption_seeds = &[
PREFIX.as_bytes(),
auction_manager.auction.as_ref(),
bidder_metadata_info.key.as_ref(),
&[redemption_bump_seed],
];
if bid_redemption_info.data_is_empty() {
create_or_allocate_account_raw(
*program_id,
&bid_redemption_info,
&rent_info,
&system_info,
&payer_info,
MAX_BID_REDEMPTION_TICKET_SIZE,
redemption_seeds,
)?;
bid_redemption = BidRedemptionTicket::from_account_info(bid_redemption_info)?;
} else {
bid_redemption = BidRedemptionTicket::from_account_info(bid_redemption_info)?;
}
bid_redemption.key = Key::BidRedemptionTicketV1;
if participation_redeemed {
bid_redemption.participation_redeemed = true
} else if bid_redeemed {
bid_redemption.items_redeemed += 1;
}
bid_redemption.serialize(&mut *bid_redemption_info.data.borrow_mut())?;
}
let mut open_claims = false;
for state in &auction_manager.state.winning_config_states {
for item in &state.items {
if !item.claimed {
open_claims = true;
break;
}
}
}
if !open_claims {
auction_manager.state.status = AuctionManagerStatus::Finished
}
auction_manager.serialize(&mut *auction_manager_info.data.borrow_mut())?;
Ok(())
}
pub struct CommonWinningConfigCheckReturn {
pub winning_config_item: WinningConfigItem,
pub winning_item_index: Option<usize>,
}
pub fn common_winning_config_checks(
auction_manager: &AuctionManager,
safety_deposit_info: &AccountInfo,
winning_index: usize,
) -> Result<CommonWinningConfigCheckReturn, ProgramError> {
let winning_config = &auction_manager.settings.winning_configs[winning_index];
let winning_config_state = &auction_manager.state.winning_config_states[winning_index];
let mut winning_item_index = None;
for i in 0..winning_config.items.len() {
let order: usize = 97;
if winning_config.items[i].safety_deposit_box_index
== safety_deposit_info.data.borrow()[order]
{
winning_item_index = Some(i);
break;
}
}
let winning_config_item = match winning_item_index {
Some(index) => winning_config.items[index],
None => return Err(MetaplexError::SafetyDepositBoxNotUsedInAuction.into()),
};
let winning_config_state_item = match winning_item_index {
Some(index) => winning_config_state.items[index],
None => return Err(MetaplexError::SafetyDepositBoxNotUsedInAuction.into()),
};
if winning_config_state_item.claimed {
return Err(MetaplexError::PrizeAlreadyClaimed.into());
}
Ok(CommonWinningConfigCheckReturn {
winning_config_item,
winning_item_index,
})
}
#[allow(clippy::too_many_arguments)]
pub fn shift_authority_back_to_originating_user<'a>(
program_id: &Pubkey,
auction_manager: &AuctionManager,
auction_manager_info: &AccountInfo<'a>,
master_metadata_info: &AccountInfo<'a>,
original_authority: &AccountInfo<'a>,
original_authority_lookup_info: &AccountInfo<'a>,
printing_mint_info: &AccountInfo<'a>,
token_program_info: &AccountInfo<'a>,
authority_seeds: &[&[u8]],
) -> ProgramResult {
let original_authority_lookup_seeds = &[
PREFIX.as_bytes(),
&auction_manager.auction.as_ref(),
master_metadata_info.key.as_ref(),
];
let (expected_key, _) =
Pubkey::find_program_address(original_authority_lookup_seeds, &program_id);
if expected_key != *original_authority_lookup_info.key {
return Err(MetaplexError::OriginalAuthorityLookupKeyMismatch.into());
}
let original_authority_lookup: OriginalAuthorityLookup =
OriginalAuthorityLookup::from_account_info(original_authority_lookup_info)?;
if original_authority_lookup.original_authority != *original_authority.key {
return Err(MetaplexError::OriginalAuthorityMismatch.into());
}
transfer_mint_authority(
authority_seeds,
original_authority.key,
original_authority,
printing_mint_info,
auction_manager_info,
token_program_info,
)?;
Ok(())
}
// TODO due to a weird stack access violation bug we had to remove the args struct from this method
// to get redemptions working again after integrating new Auctions program. Try to bring it back one day
#[inline(always)]
pub fn spl_token_transfer<'a: 'b, 'b>(
source: AccountInfo<'a>,
destination: AccountInfo<'a>,
amount: u64,
authority: AccountInfo<'a>,
authority_signer_seeds: &'b [&'b [u8]],
token_program: AccountInfo<'a>,
) -> ProgramResult {
let result = invoke_signed(
&spl_token::instruction::transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[source, destination, authority, token_program],
&[authority_signer_seeds],
);
result.map_err(|_| MetaplexError::TokenTransferFailed.into())
}
pub fn assert_edition_valid(
program_id: &Pubkey,
mint: &Pubkey,
edition_account_info: &AccountInfo,
) -> ProgramResult {
let edition_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
program_id.as_ref(),
&mint.as_ref(),
EDITION.as_bytes(),
];
let (edition_key, _) = Pubkey::find_program_address(edition_seeds, program_id);
if edition_key != *edition_account_info.key {
return Err(MetaplexError::InvalidEditionKey.into());
}
Ok(())
}
// TODO due to a weird stack access violation bug we had to remove the args struct from this method
// to get redemptions working again after integrating new Auctions program. Try to bring it back one day.
pub fn spl_token_mint_to<'a: 'b, 'b>(
mint: AccountInfo<'a>,
destination: AccountInfo<'a>,
amount: u64,
authority: AccountInfo<'a>,
authority_signer_seeds: &'b [&'b [u8]],
token_program: AccountInfo<'a>,
) -> ProgramResult {
let result = invoke_signed(
&spl_token::instruction::mint_to(
token_program.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[mint, destination, authority, token_program],
&[authority_signer_seeds],
);
result.map_err(|_| MetaplexError::TokenMintToFailed.into())
}
pub fn assert_derivation(
program_id: &Pubkey,
account: &AccountInfo,
path: &[&[u8]],
) -> Result<u8, ProgramError> {
let (key, bump) = Pubkey::find_program_address(&path, program_id);
if key != *account.key {
return Err(MetaplexError::DerivedKeyInvalid.into());
}
Ok(bump)
}
pub fn try_from_slice_checked<T: BorshDeserialize>(
data: &[u8],
data_type: Key,
data_size: usize,
) -> Result<T, ProgramError> {
if (data[0] != data_type as u8 && data[0] != Key::Uninitialized as u8)
|| data.len() != data_size
{
return Err(MetaplexError::DataTypeMismatch.into());
}
let result: T = try_from_slice_unchecked(data)?;
Ok(result)
}

View File

@ -0,0 +1,28 @@
[package]
name = "spl-metaplex-test-client"
version = "0.1.0"
description = "Metaplex Library Metaplex Test Client"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
publish = false
[dependencies]
solana-client = "1.6.10"
solana-program = "1.6.10"
solana-sdk = "1.6.10"
bincode = "1.3.2"
arrayref = "0.3.6"
borsh = "0.8.2"
serde_json = "1.0"
serde_derive = "1.0"
serde = { version = "1.0.100", default-features = false }
clap = "2.33.3"
solana-clap-utils = "1.6"
solana-cli-config = "1.6"
spl-auction = { path = "../../auction/program", features = [ "no-entrypoint" ] }
spl-token-metadata = { path = "../../token-metadata/program", features = [ "no-entrypoint" ] }
spl-token-vault = { path = "../../token-vault/program", features = [ "no-entrypoint" ] }
spl-metaplex = { path = "../program", features = [ "no-entrypoint" ] }
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }

View File

@ -0,0 +1,477 @@
use {
crate::{
settings_utils::{parse_settings, JsonAuctionManagerSettings},
vault_utils::{activate_vault, add_token_to_vault, combine_vault, initialize_vault},
AUCTION_PROGRAM_PUBKEY, PROGRAM_PUBKEY, TOKEN_PROGRAM_PUBKEY, VAULT_PROGRAM_PUBKEY,
},
clap::ArgMatches,
solana_clap_utils::input_parsers::pubkey_of,
solana_client::rpc_client::RpcClient,
solana_program::{
borsh::try_from_slice_unchecked, instruction::Instruction, program_pack::Pack,
},
solana_sdk::{
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
system_instruction::create_account,
transaction::Transaction,
},
spl_auction::{
instruction::create_auction_instruction,
processor::{create_auction::CreateAuctionArgs, PriceFloor, WinnerLimit},
},
spl_metaplex::{
instruction::create_init_auction_manager_instruction,
instruction::create_set_store_instruction,
instruction::create_validate_participation_instruction, state::AuctionManager,
},
spl_token::{
instruction::{initialize_account, initialize_mint},
state::{Account, Mint},
},
spl_token_metadata::state::{MasterEdition, Metadata, EDITION},
spl_token_vault::{
instruction::create_update_external_price_account_instruction,
state::MAX_EXTERNAL_ACCOUNT_SIZE,
},
std::{convert::TryInto, fs::File, io::Write, str::FromStr},
};
fn find_or_initialize_external_account<'a>(
app_matches: &ArgMatches,
payer: &Keypair,
vault_program_key: &Pubkey,
token_key: &Pubkey,
client: &RpcClient,
payer_mint_key: &'a Keypair,
external_keypair: &'a Keypair,
) -> Pubkey {
let external_key: Pubkey;
if !app_matches.is_present("external_price_account") {
let mut instructions: Vec<Instruction> = vec![];
let mut signers: Vec<&Keypair> = vec![&payer, &external_keypair];
instructions.push(create_account(
&payer.pubkey(),
&payer_mint_key.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
));
instructions.push(
initialize_mint(
&token_key,
&payer_mint_key.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
);
instructions.push(create_account(
&payer.pubkey(),
&external_keypair.pubkey(),
client
.get_minimum_balance_for_rent_exemption(MAX_EXTERNAL_ACCOUNT_SIZE)
.unwrap(),
MAX_EXTERNAL_ACCOUNT_SIZE as u64,
&vault_program_key,
));
instructions.push(create_update_external_price_account_instruction(
*vault_program_key,
external_keypair.pubkey(),
0,
payer_mint_key.pubkey(),
true,
));
signers.push(&payer_mint_key);
signers.push(&external_keypair);
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
external_key = external_keypair.pubkey();
} else {
external_key = pubkey_of(app_matches, "external_price_account").unwrap();
}
external_key
}
fn find_or_initialize_store(
app_matches: &ArgMatches,
payer: &Keypair,
client: &RpcClient,
) -> Pubkey {
let admin = read_keypair_file(
app_matches
.value_of("admin")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let program_key = Pubkey::from_str(PROGRAM_PUBKEY).unwrap();
let admin_key = admin.pubkey();
let seeds = &[
spl_metaplex::state::PREFIX.as_bytes(),
&program_key.as_ref(),
&admin_key.as_ref(),
];
let (store_key, _) = Pubkey::find_program_address(seeds, &program_key);
let instructions = [create_set_store_instruction(
program_key,
store_key,
admin.pubkey(),
payer.pubkey(),
true,
)];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&[&admin, &payer], recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
println!("Store created {:?}", store_key);
store_key
}
fn find_or_initialize_auction(
app_matches: &ArgMatches,
vault_key: &Pubkey,
program_key: &Pubkey,
auction_program_key: &Pubkey,
payer_mint_key: &Pubkey,
payer: &Keypair,
client: &RpcClient,
) -> Pubkey {
let auction_key: Pubkey;
if !app_matches.is_present("auction") {
let signers: Vec<&Keypair> = vec![&payer];
let winner_limit = app_matches
.value_of("winner_limit")
.unwrap_or("0")
.parse::<u64>()
.unwrap();
let gap_time = app_matches
.value_of("gap_time")
.unwrap_or("1200")
.parse::<u64>()
.unwrap();
let end_time = app_matches
.value_of("end_time")
.unwrap_or("1200")
.parse::<u64>()
.unwrap();
let auction_path = [
spl_auction::PREFIX.as_bytes(),
auction_program_key.as_ref(),
&vault_key.to_bytes(),
];
// Derive the address we'll store the auction in, and confirm it matches what we expected the
// user to provide.
let (actual_auction_key, _) =
Pubkey::find_program_address(&auction_path, auction_program_key);
// You'll notice that the authority IS what will become the auction manager ;)
let authority_seeds = &[
spl_metaplex::state::PREFIX.as_bytes(),
&actual_auction_key.as_ref(),
];
let (auction_manager_key, _) = Pubkey::find_program_address(authority_seeds, &program_key);
let instructions = [create_auction_instruction(
*auction_program_key,
payer.pubkey(),
CreateAuctionArgs {
resource: *vault_key,
authority: auction_manager_key,
end_auction_at: Some(end_time.try_into().unwrap()),
end_auction_gap: Some(gap_time.try_into().unwrap()),
winners: match winner_limit {
0 => WinnerLimit::Unlimited(0),
val => WinnerLimit::Capped(val.try_into().unwrap()),
},
token_mint: *payer_mint_key,
price_floor: PriceFloor::None([0; 32]),
},
)];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
auction_key = actual_auction_key;
} else {
auction_key = pubkey_of(app_matches, "auction").unwrap();
}
auction_key
}
fn add_tokens_to_vault_activate_and_return_mints_and_open_edition(
app_matches: &ArgMatches,
json_settings: &JsonAuctionManagerSettings,
vault_key: &Pubkey,
payer: &Keypair,
auction_manager_key: &Pubkey,
client: &RpcClient,
) -> (Vec<Pubkey>, Option<Pubkey>, Option<Pubkey>, Option<Pubkey>) {
let mut mint_keys: Vec<Pubkey> = vec![];
let open_edition_mint_key: Option<Pubkey>;
let mut open_edition_safety_deposit: Option<Pubkey> = None;
let mut open_edition_safety_deposit_store: Option<Pubkey> = None;
if !app_matches.is_present("vault") {
for config in &json_settings.winning_configs {
for item in &config.items {
let (_, actual_mint, _) = add_token_to_vault(
&payer,
vault_key,
&payer,
client,
item.amount.into(),
match &item.mint {
Some(val) => Some(Pubkey::from_str(&val).unwrap()),
None => None,
},
match &item.account {
Some(val) => Some(Pubkey::from_str(&val).unwrap()),
None => None,
},
!matches!(item.winning_config_type, 0),
item.desired_supply,
false,
);
mint_keys.push(actual_mint);
}
}
if let Some(config) = &json_settings.participation_config {
let (safety_deposit_box, actual_open_edition_mint, store) = add_token_to_vault(
&payer,
vault_key,
&payer,
client,
1,
match &config.mint {
Some(val) => Some(Pubkey::from_str(&val).unwrap()),
None => None,
},
match &config.account {
Some(val) => Some(Pubkey::from_str(&val).unwrap()),
None => None,
},
true,
None,
true,
);
open_edition_mint_key = Some(actual_open_edition_mint);
open_edition_safety_deposit = Some(safety_deposit_box);
open_edition_safety_deposit_store = Some(store);
} else {
open_edition_mint_key = None; // Return nothing, it wont be used
}
activate_vault(&payer, vault_key, &payer, client);
combine_vault(&payer, auction_manager_key, vault_key, &payer, client);
} else {
open_edition_mint_key = match &json_settings.participation_config {
Some(val) => match &val.mint {
Some(mint) => Some(Pubkey::from_str(&mint).unwrap()),
None => None, // If a config was provided for existing vault but no mint, cant do anything here.
},
None => None, // Return nothing, it wont be used
}
}
(
mint_keys,
open_edition_mint_key,
open_edition_safety_deposit,
open_edition_safety_deposit_store,
)
}
pub fn initialize_auction_manager(
app_matches: &ArgMatches,
payer: Keypair,
client: RpcClient,
) -> (Pubkey, AuctionManager) {
let program_key = Pubkey::from_str(PROGRAM_PUBKEY).unwrap();
let vault_program_key = Pubkey::from_str(VAULT_PROGRAM_PUBKEY).unwrap();
let auction_program_key = Pubkey::from_str(AUCTION_PROGRAM_PUBKEY).unwrap();
let accept_payment_account_key = Keypair::new();
let printing_token_account_key = Keypair::new();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let authority = pubkey_of(app_matches, "authority").unwrap_or_else(|| payer.pubkey());
let store_key = find_or_initialize_store(app_matches, &payer, &client);
let (settings, json_settings) = parse_settings(app_matches.value_of("settings_file").unwrap());
let vault_key: Pubkey;
let mut instructions: Vec<Instruction> = vec![];
let mut signers: Vec<&Keypair> = vec![&payer, &accept_payment_account_key];
let payer_mint_key = Keypair::new();
let external_keypair = Keypair::new();
let external_key = find_or_initialize_external_account(
app_matches,
&payer,
&vault_program_key,
&token_key,
&client,
&payer_mint_key,
&external_keypair,
);
// Create vault first, so we can use it to make auction, then add stuff to vault.
if !app_matches.is_present("vault") {
vault_key = initialize_vault(&payer, &external_key, &payer, &client);
} else {
vault_key = pubkey_of(app_matches, "vault").unwrap();
}
let auction_key = find_or_initialize_auction(
app_matches,
&vault_key,
&program_key,
&auction_program_key,
&payer_mint_key.pubkey(),
&payer,
&client,
);
let seeds = &[
spl_metaplex::state::PREFIX.as_bytes(),
&auction_key.as_ref(),
];
let (auction_manager_key, _) = Pubkey::find_program_address(seeds, &program_key);
let (actual_mints, open_edition_mint_key, open_edition_safety_deposit, open_edition_store) =
add_tokens_to_vault_activate_and_return_mints_and_open_edition(
app_matches,
&json_settings,
&vault_key,
&payer,
&auction_manager_key,
&client,
);
let actual_mints_to_json = serde_json::to_string(&actual_mints).unwrap();
let mut file = File::create(auction_manager_key.to_string() + ".json").unwrap();
file.write_all(&actual_mints_to_json.as_bytes()).unwrap();
println!("Printed mints to file {:?}.json", auction_manager_key);
let token_metadata = spl_token_metadata::id();
instructions.push(create_account(
&payer.pubkey(),
&accept_payment_account_key.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(
&token_key,
&accept_payment_account_key.pubkey(),
&payer_mint_key.pubkey(),
&auction_manager_key,
)
.unwrap(),
);
instructions.push(create_init_auction_manager_instruction(
program_key,
auction_manager_key,
vault_key,
auction_key,
authority,
payer.pubkey(),
accept_payment_account_key.pubkey(),
store_key,
settings,
));
if let Some(mint_key) = open_edition_mint_key {
let metadata_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
&token_metadata.as_ref(),
&mint_key.as_ref(),
];
let (metadata_key, _) =
Pubkey::find_program_address(metadata_seeds, &spl_token_metadata::id());
let metadata_account = client.get_account(&metadata_key).unwrap();
let metadata: Metadata = try_from_slice_unchecked(&metadata_account.data).unwrap();
let metadata_authority = metadata.update_authority;
let edition_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
token_metadata.as_ref(),
mint_key.as_ref(),
EDITION.as_bytes(),
];
let (edition_key, _) = Pubkey::find_program_address(edition_seeds, &token_metadata);
let master_edition_account = client.get_account(&edition_key).unwrap();
let master_edition: MasterEdition =
try_from_slice_unchecked(&master_edition_account.data).unwrap();
let open_edition_printing_mint = master_edition.printing_mint;
instructions.push(create_account(
&payer.pubkey(),
&printing_token_account_key.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(
&token_key,
&printing_token_account_key.pubkey(),
&open_edition_printing_mint,
&auction_manager_key,
)
.unwrap(),
);
signers.push(&printing_token_account_key);
instructions.push(create_validate_participation_instruction(
program_key,
auction_manager_key,
metadata_key,
edition_key,
printing_token_account_key.pubkey(),
authority,
metadata_authority,
store_key,
open_edition_safety_deposit.unwrap(),
open_edition_store.unwrap(),
vault_key,
));
}
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let account = client.get_account(&auction_manager_key).unwrap();
let manager: AuctionManager = try_from_slice_unchecked(&account.data).unwrap();
(auction_manager_key, manager)
}

View File

@ -0,0 +1,305 @@
mod initialize_auction_manager;
mod place_bid;
mod redeem_bid;
mod settings_utils;
mod show;
mod start_auction;
mod validate_safety_deposits;
mod vault_utils;
use {
clap::{crate_description, crate_name, crate_version, App, Arg, SubCommand},
initialize_auction_manager::initialize_auction_manager,
place_bid::make_bid,
redeem_bid::redeem_bid_wrapper,
show::send_show,
solana_clap_utils::input_validators::{is_url, is_valid_pubkey, is_valid_signer},
solana_client::rpc_client::RpcClient,
solana_sdk::signature::read_keypair_file,
start_auction::send_start_auction,
validate_safety_deposits::validate_safety_deposits,
};
pub const VAULT_PROGRAM_PUBKEY: &str = "vau1zxA2LbssAUEF7Gpw91zMM1LvXrvpzJtmZ58rPsn";
pub const AUCTION_PROGRAM_PUBKEY: &str = "auctxRXPeJoc4817jDhf4HbjnhEcr1cCXenosMhK5R8";
pub const PROGRAM_PUBKEY: &str = "p1exdMJcjVao65QdewkaZRUnU6VPSXhus9n2GzWfh98";
pub const TOKEN_PROGRAM_PUBKEY: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
fn main() {
let app_matches = App::new(crate_name!())
.about(crate_description!())
.version(crate_version!())
.arg(
Arg::with_name("keypair")
.long("keypair")
.value_name("KEYPAIR")
.validator(is_valid_signer)
.takes_value(true)
.global(true)
.help("Filepath or URL to a keypair"),
)
.arg(
Arg::with_name("json_rpc_url")
.long("url")
.value_name("URL")
.takes_value(true)
.global(true)
.validator(is_url)
.help("JSON RPC URL for the cluster [default: devnet]"),
)
.arg(
Arg::with_name("admin")
.long("admin")
.value_name("ADMIN")
.required(false)
.validator(is_valid_signer)
.takes_value(true)
.help("Admin of the store you want to use, defaults to your key"),
)
.subcommand(
SubCommand::with_name("init")
.about("Initialize an Auction Manager")
.arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY")
.required(false)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of authority, defaults to you otherwise"),
)
.arg(
Arg::with_name("external_price_account")
.long("external_price_account")
.value_name("EXTERNAL_PRICE_ACCOUNT")
.required(false)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of external price account, if one not provided, one will be made. Needs to be same as the one on the Vault."),
)
.arg(
Arg::with_name("vault")
.long("vault")
.value_name("VAULT")
.required(false)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of vault. If one not provided, one will be made."),
)
.arg(
Arg::with_name("auction")
.long("auction")
.value_name("AUCTION")
.required(false)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of auction. If one not provided, one will be made."),
)
.arg(
Arg::with_name("winner_limit")
.long("winner_limit")
.value_name("WINNER_LIMIT")
.required(false)
.takes_value(true)
.help("Defaults to unlimited (0), ignored if existing auction provided."),
).arg(
Arg::with_name("gap_time")
.long("gap_time")
.value_name("GAP_TIME")
.required(false)
.takes_value(true)
.help("Defaults to 1200 slots, ignored if existing auction provided."),
)
.arg(
Arg::with_name("end_time")
.long("end_time")
.value_name("END_TIME")
.required(false)
.takes_value(true)
.help("Defaults to 1200 slots, ignored if existing auction provided."),
)
.arg(
Arg::with_name("settings_file")
.long("settings_file")
.value_name("SETTINGS_FILE")
.takes_value(true)
.required(true)
.help("File path or uri to settings file (json) for setting up Auction Managers. See settings_sample.json, and you can follow the JSON structs in settings_utils.rs to customize the AuctionManagerSetting struct that gets created for shipping."),
),
).subcommand(
SubCommand::with_name("validate")
.about("Validate one (or all) of the winning configurations of your auction manager by slot.")
.arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY")
.required(false)
.validator(is_valid_signer)
.takes_value(true)
.help("Pubkey of authority, defaults to you otherwise"),
)
.arg(
Arg::with_name("metadata_authority")
.long("metadata_authority")
.value_name("METADATA_AUTHORITY")
.required(false)
.validator(is_valid_signer)
.takes_value(true)
.help("Pubkey of the metadata authority on the given winning configuration(s), defaults to you otherwise"),
)
.arg(
Arg::with_name("auction_manager")
.long("auction_manager")
.value_name("AUCTION_MANAGER")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of auction manager."),
)
.arg(
Arg::with_name("winner_config_slot")
.long("winner_config_slot")
.value_name("WINNER_CONFIG_SLOT")
.required(false)
.takes_value(true)
.help("Pass in a specific 0-indexed slot in the array to validate that slot, if not passed, all will be validated."),
)
).subcommand(
SubCommand::with_name("show")
.about("Print out the manager data for a given manager address.")
.arg(
Arg::with_name("auction_manager")
.long("auction_manager")
.value_name("AUCTION_MANAGER")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of auction manager."),
)
).subcommand(
SubCommand::with_name("place_bid")
.about("Place a bid on a specific slot, receive a bidder metadata address in return.")
.arg(
Arg::with_name("auction_manager")
.long("auction_manager")
.value_name("AUCTION_MANAGER")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of auction manager."),
).arg(
Arg::with_name("wallet")
.long("wallet")
.value_name("WALLET")
.required(false)
.validator(is_valid_signer)
.takes_value(true)
.help("Valid wallet, defaults to you."),
).arg(
Arg::with_name("mint_it")
.long("mint_it")
.value_name("MINT_IT")
.required(false)
.takes_value(false)
.help("Attempts to mint the tokens. Useful on devnet and you need to have authority as payer over the token_mint on the auction."),
)
.arg(
Arg::with_name("price")
.long("price")
.value_name("PRICE")
.required(true)
.takes_value(true)
.help("The price in sol you want to bid"),
)
).subcommand(
SubCommand::with_name("redeem_bid")
.about("Redeem a bid")
.arg(
Arg::with_name("auction_manager")
.long("auction_manager")
.value_name("AUCTION_MANAGER")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of auction manager."),
).arg(
Arg::with_name("wallet")
.long("wallet")
.value_name("WALLET")
.required(false)
.validator(is_valid_signer)
.takes_value(true)
.help("Wallet that placed the bid, defaults to you."),
).arg(
Arg::with_name("mint_it")
.long("mint_it")
.value_name("MINT_IT")
.required(false)
.takes_value(false)
.help("Attempts to mint tokens to pay for the open edition. Useful on devnet and you need to have authority as payer over the token_mint on the auction."),
)
).subcommand(
SubCommand::with_name("start_auction")
.about("Starts an auction on an auction manager that has been fully validated")
.arg(
Arg::with_name("auction_manager")
.long("auction_manager")
.value_name("AUCTION_MANAGER")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Pubkey of auction manager."),
).arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY")
.required(false)
.validator(is_valid_signer)
.takes_value(true)
.help("Pubkey of authority, defaults to you otherwise"),
)
)
.get_matches();
let client = RpcClient::new(
app_matches
.value_of("json_rpc_url")
.unwrap_or(&"https://devnet.solana.com".to_owned())
.to_owned(),
);
let (sub_command, sub_matches) = app_matches.subcommand();
let payer = read_keypair_file(app_matches.value_of("keypair").unwrap()).unwrap();
match (sub_command, sub_matches) {
("init", Some(arg_matches)) => {
let (key, manager) = initialize_auction_manager(arg_matches, payer, client);
println!(
"Created auction manager with address {:?} and output {:?}",
key, manager
);
}
("validate", Some(arg_matches)) => {
validate_safety_deposits(arg_matches, payer, client);
println!("Validated all winning configs passed in.",);
}
("place_bid", Some(arg_matches)) => {
make_bid(arg_matches, payer, client);
}
("redeem_bid", Some(arg_matches)) => {
redeem_bid_wrapper(arg_matches, payer, client);
}
("start_auction", Some(arg_matches)) => {
send_start_auction(arg_matches, payer, client);
}
("show", Some(arg_matches)) => {
send_show(arg_matches, payer, client);
}
_ => unreachable!(),
}
}

View File

@ -0,0 +1,189 @@
use {
crate::{AUCTION_PROGRAM_PUBKEY, TOKEN_PROGRAM_PUBKEY},
clap::ArgMatches,
solana_clap_utils::input_parsers::pubkey_of,
solana_client::rpc_client::RpcClient,
solana_program::{
borsh::try_from_slice_unchecked, program_pack::Pack, system_instruction::create_account,
},
solana_sdk::{
pubkey::Pubkey,
signature::write_keypair_file,
signature::{read_keypair_file, Keypair, Signer},
transaction::Transaction,
},
spl_auction::{
instruction::place_bid_instruction,
processor::{place_bid::PlaceBidArgs, AuctionData, BidderMetadata, BidderPot},
},
spl_metaplex::state::{AuctionManager, Store},
spl_token::{
instruction::{approve, initialize_account, mint_to},
state::Account,
},
std::str::FromStr,
};
pub fn make_bid(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
let auction_program_key = Pubkey::from_str(AUCTION_PROGRAM_PUBKEY).unwrap();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let wallet: Keypair;
if !app_matches.is_present("wallet") {
wallet = Keypair::new();
} else {
wallet = read_keypair_file(app_matches.value_of("wallet").unwrap()).unwrap();
}
let amount = app_matches
.value_of("price")
.unwrap()
.parse::<u64>()
.unwrap();
let auction_manager_key = pubkey_of(app_matches, "auction_manager").unwrap();
let account = client.get_account(&auction_manager_key).unwrap();
let manager: AuctionManager = try_from_slice_unchecked(&account.data).unwrap();
let store_account = client.get_account(&manager.store).unwrap();
let store: Store = try_from_slice_unchecked(&store_account.data).unwrap();
let auction_account = client.get_account(&manager.auction).unwrap();
let auction: AuctionData = try_from_slice_unchecked(&auction_account.data).unwrap();
let wallet_key = wallet.pubkey();
let bidder_pot_seeds = &[
spl_auction::PREFIX.as_bytes(),
&auction_program_key.as_ref(),
manager.auction.as_ref(),
wallet_key.as_ref(),
];
let (bidder_pot_pubkey, _) =
Pubkey::find_program_address(bidder_pot_seeds, &auction_program_key);
let bidder_pot_account = client.get_account(&bidder_pot_pubkey);
let transfer_authority = Keypair::new();
let mut signers = vec![&wallet, &transfer_authority, &payer];
let mut instructions = vec![];
let bidder_pot_token: Pubkey;
let new_bidder_pot = Keypair::new();
match bidder_pot_account {
Ok(val) => {
let bidder_pot: BidderPot = try_from_slice_unchecked(&val.data).unwrap();
bidder_pot_token = bidder_pot.bidder_pot;
}
Err(_) => {
bidder_pot_token = new_bidder_pot.pubkey();
signers.push(&new_bidder_pot);
instructions.push(create_account(
&payer.pubkey(),
&new_bidder_pot.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(
&token_key,
&new_bidder_pot.pubkey(),
&auction.token_mint,
&manager.auction,
)
.unwrap(),
);
}
}
// Make sure you can afford the bid.
if app_matches.is_present("mint_it") {
if !app_matches.is_present("wallet") {
instructions.push(create_account(
&payer.pubkey(),
&wallet.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(
&token_key,
&wallet.pubkey(),
&auction.token_mint,
&payer.pubkey(),
)
.unwrap(),
);
}
instructions.push(
mint_to(
&token_key,
&auction.token_mint,
&wallet.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
amount + 2,
)
.unwrap(),
);
}
instructions.push(
approve(
&token_key,
&wallet.pubkey(),
&transfer_authority.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
amount,
)
.unwrap(),
);
instructions.push(place_bid_instruction(
auction_program_key,
// Can use any account as bidder key, so we just reuse spl token account as bidder. Traditionally
// this would be your sol wallet.
wallet.pubkey(),
wallet.pubkey(),
bidder_pot_token,
auction.token_mint,
transfer_authority.pubkey(),
payer.pubkey(),
PlaceBidArgs {
amount,
resource: manager.vault,
},
));
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let wallet_key = wallet.pubkey();
let meta_path = [
spl_auction::PREFIX.as_bytes(),
store.auction_program.as_ref(),
manager.auction.as_ref(),
wallet_key.as_ref(),
"metadata".as_bytes(),
];
let (meta_key, _) = Pubkey::find_program_address(&meta_path, &store.auction_program);
let bidding_metadata = client.get_account(&meta_key).unwrap();
let _bid: BidderMetadata = try_from_slice_unchecked(&bidding_metadata.data).unwrap();
write_keypair_file(&wallet, wallet.pubkey().to_string() + ".json").unwrap();
println!(
"Because no wallet provided, created new one at {:?}.json, it was used to place the bid. Please use it for redemption as a signer.",
wallet.pubkey()
);
println!("Created bid {:?}", meta_key);
}

View File

@ -0,0 +1,549 @@
use {
crate::{
settings_utils::parse_metadata_keys, PROGRAM_PUBKEY, TOKEN_PROGRAM_PUBKEY,
VAULT_PROGRAM_PUBKEY,
},
arrayref::array_ref,
clap::ArgMatches,
solana_clap_utils::input_parsers::pubkey_of,
solana_client::rpc_client::RpcClient,
solana_program::{
borsh::try_from_slice_unchecked, instruction::Instruction, program_pack::Pack,
system_instruction::create_account,
},
solana_sdk::{
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
transaction::Transaction,
},
spl_auction::processor::{AuctionData, BidderMetadata},
spl_metaplex::{
instruction::{
create_redeem_bid_instruction, create_redeem_full_rights_transfer_bid_instruction,
create_redeem_participation_bid_instruction,
},
state::{AuctionManager, Store, WinningConfigItem, WinningConfigType},
},
spl_token::{
instruction::{approve, initialize_account, mint_to},
state::Account,
},
spl_token_metadata::state::{MasterEdition, EDITION},
spl_token_vault::state::{SafetyDepositBox, Vault, SAFETY_DEPOSIT_KEY},
std::{collections::HashMap, str::FromStr},
};
struct BaseAccountList {
auction_manager: Pubkey,
store: Pubkey,
destination: Pubkey,
bid_redemption: Pubkey,
safety_deposit_box: Pubkey,
fraction_mint: Pubkey,
vault: Pubkey,
auction: Pubkey,
bidder_metadata: Pubkey,
bidder: Pubkey,
payer: Pubkey,
token_vault_program: Pubkey,
}
#[allow(clippy::too_many_arguments)]
fn redeem_bid_token_only_type<'a>(
base_account_list: BaseAccountList,
manager: &AuctionManager,
winning_config_item: &WinningConfigItem,
safety_deposit: &SafetyDepositBox,
program_id: &Pubkey,
token_program: &Pubkey,
instructions: &'a mut Vec<Instruction>,
client: &RpcClient,
) -> Vec<Instruction> {
println!("You are redeeming a normal token.");
let BaseAccountList {
auction_manager,
store,
destination,
bid_redemption,
safety_deposit_box,
fraction_mint,
vault,
auction,
bidder_metadata,
bidder,
payer,
token_vault_program,
} = base_account_list;
let transfer_seeds = [
spl_token_vault::state::PREFIX.as_bytes(),
token_vault_program.as_ref(),
];
let (transfer_authority, _) =
Pubkey::find_program_address(&transfer_seeds, &token_vault_program);
instructions.push(create_account(
&payer,
&destination,
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_program,
));
// For limited editions, we need owner to be payer to be used in token metadata
let owner_key = match winning_config_item.winning_config_type {
spl_metaplex::state::WinningConfigType::TokenOnlyTransfer => &bidder,
spl_metaplex::state::WinningConfigType::Printing => &payer,
_ => &bidder,
};
instructions.push(
initialize_account(
&token_program,
&destination,
&safety_deposit.token_mint,
owner_key,
)
.unwrap(),
);
instructions.push(
approve(
token_program,
&base_account_list.destination,
&transfer_authority,
&owner_key,
&[owner_key],
winning_config_item.amount.into(),
)
.unwrap(),
);
instructions.push(create_redeem_bid_instruction(
*program_id,
auction_manager,
store,
destination,
bid_redemption,
safety_deposit_box,
vault,
fraction_mint,
auction,
bidder_metadata,
bidder,
payer,
manager.store,
transfer_authority,
));
let mut new_instructions: Vec<Instruction> = vec![];
for instr in instructions.iter() {
new_instructions.push(instr.clone());
}
new_instructions
}
#[allow(clippy::too_many_arguments)]
fn redeem_bid_open_edition_type<'a>(
base_account_list: BaseAccountList,
manager: &AuctionManager,
safety_deposit: &SafetyDepositBox,
program_id: &Pubkey,
token_program: &Pubkey,
instructions: &'a mut Vec<Instruction>,
token_metadata_key: &Pubkey,
transfer_authority: &Keypair,
client: &RpcClient,
app_matches: &ArgMatches,
bidding_metadata_obj: BidderMetadata,
) -> Vec<Instruction> {
println!("You are redeeming an open edition.");
let BaseAccountList {
auction_manager,
store,
destination,
bid_redemption,
safety_deposit_box,
fraction_mint,
vault,
auction,
bidder_metadata,
bidder,
payer,
token_vault_program: _t,
} = base_account_list;
let master_edition_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
&token_metadata_key.as_ref(),
safety_deposit.token_mint.as_ref(),
EDITION.as_bytes(),
];
let (master_edition_key, _) =
Pubkey::find_program_address(master_edition_seeds, &token_metadata_key);
let master_edition_account = client.get_account(&master_edition_key).unwrap();
let master_edition: MasterEdition =
try_from_slice_unchecked(&master_edition_account.data).unwrap();
let mut price = bidding_metadata_obj.last_bid;
if let Some(config) = &manager.settings.participation_config {
if let Some(fixed_price) = config.fixed_price {
price = fixed_price
}
}
if app_matches.is_present("mint_it") {
let auction_acct = client.get_account(&auction).unwrap();
let auction: AuctionData = try_from_slice_unchecked(&auction_acct.data).unwrap();
instructions.push(
mint_to(
token_program,
&auction.token_mint,
&base_account_list.bidder,
&payer,
&[&payer],
price + 2,
)
.unwrap(),
);
}
instructions.push(
approve(
token_program,
&base_account_list.bidder,
&transfer_authority.pubkey(),
&payer,
&[&payer],
price,
)
.unwrap(),
);
instructions.push(create_account(
&payer,
&destination,
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_program,
));
instructions.push(
initialize_account(
&token_program,
&destination,
&master_edition.printing_mint,
&payer,
)
.unwrap(),
);
let state = manager.state.participation_state.clone();
instructions.push(create_redeem_participation_bid_instruction(
*program_id,
auction_manager,
store,
destination,
bid_redemption,
safety_deposit_box,
vault,
fraction_mint,
auction,
bidder_metadata,
bidder,
payer,
manager.store,
transfer_authority.pubkey(),
manager.accept_payment,
bidder,
state.unwrap().printing_authorization_token_account.unwrap(),
));
let mut new_instructions: Vec<Instruction> = vec![];
for instr in instructions.iter() {
new_instructions.push(instr.clone());
}
new_instructions
}
#[allow(clippy::too_many_arguments)]
fn redeem_bid_rights_transfer<'a>(
base_account_list: BaseAccountList,
manager: &AuctionManager,
safety_deposit: &SafetyDepositBox,
program_id: &Pubkey,
token_program: &Pubkey,
instructions: &'a mut Vec<Instruction>,
token_metadata_key: &Pubkey,
client: &RpcClient,
) -> Vec<Instruction> {
println!("You are redeeming a master edition.");
let BaseAccountList {
auction_manager,
store,
destination,
bid_redemption,
safety_deposit_box,
fraction_mint,
vault,
auction,
bidder_metadata,
bidder,
payer,
token_vault_program,
} = base_account_list;
let master_metadata_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
&token_metadata_key.as_ref(),
&safety_deposit.token_mint.as_ref(),
];
let (master_metadata_key, _) =
Pubkey::find_program_address(master_metadata_seeds, &token_metadata_key);
let transfer_seeds = [
spl_token_vault::state::PREFIX.as_bytes(),
token_vault_program.as_ref(),
];
let (transfer_authority, _) =
Pubkey::find_program_address(&transfer_seeds, &token_vault_program);
instructions.push(create_account(
&payer,
&destination,
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_program,
));
instructions.push(
initialize_account(
&token_program,
&destination,
&safety_deposit.token_mint,
&bidder,
)
.unwrap(),
);
instructions.push(
approve(
token_program,
&base_account_list.destination,
&transfer_authority,
&base_account_list.bidder,
&[&base_account_list.bidder],
1,
)
.unwrap(),
);
instructions.push(create_redeem_full_rights_transfer_bid_instruction(
*program_id,
auction_manager,
store,
destination,
bid_redemption,
safety_deposit_box,
vault,
fraction_mint,
auction,
bidder_metadata,
bidder,
payer,
manager.store,
master_metadata_key,
bidder,
transfer_authority,
));
let mut new_instructions: Vec<Instruction> = vec![];
for instr in instructions.iter() {
new_instructions.push(instr.clone());
}
new_instructions
}
pub fn redeem_bid_wrapper(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
let program_key = Pubkey::from_str(PROGRAM_PUBKEY).unwrap();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let token_metadata_key = spl_token_metadata::id();
let token_vault_program = Pubkey::from_str(VAULT_PROGRAM_PUBKEY).unwrap();
let wallet = read_keypair_file(
app_matches
.value_of("wallet")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let auction_manager_key = pubkey_of(app_matches, "auction_manager").unwrap();
let mint_map = parse_metadata_keys(&(auction_manager_key.to_string() + ".json"));
let account = client.get_account(&auction_manager_key).unwrap();
let manager: AuctionManager = try_from_slice_unchecked(&account.data).unwrap();
let store_account = client.get_account(&manager.store).unwrap();
let store: Store = try_from_slice_unchecked(&store_account.data).unwrap();
let all_vault_accounts = client.get_program_accounts(&token_vault_program).unwrap();
let mut safety_deposits = HashMap::new();
for acc in &all_vault_accounts {
let obj = &acc.1;
let obj_key = &acc.0;
let type_of_obj = obj.data[0];
if type_of_obj == SAFETY_DEPOSIT_KEY {
let pubkey_arr = array_ref![obj.data, 1, 32];
let pubkey = Pubkey::new_from_array(*pubkey_arr);
if pubkey == manager.vault {
let safety_deposit: SafetyDepositBox = try_from_slice_unchecked(&obj.data).unwrap();
safety_deposits.insert(safety_deposit.order, (safety_deposit, obj_key));
}
}
}
let wallet_key = wallet.pubkey();
let meta_path = [
spl_auction::PREFIX.as_bytes(),
store.auction_program.as_ref(),
manager.auction.as_ref(),
wallet_key.as_ref(),
"metadata".as_bytes(),
];
let (meta_key, _) = Pubkey::find_program_address(&meta_path, &store.auction_program);
let bidding_metadata = client.get_account(&meta_key).unwrap();
let auction_data = client.get_account(&manager.auction).unwrap();
let vault_data = client.get_account(&manager.vault).unwrap();
let auction: AuctionData = try_from_slice_unchecked(&auction_data.data).unwrap();
let bid: BidderMetadata = try_from_slice_unchecked(&bidding_metadata.data).unwrap();
let vault: Vault = try_from_slice_unchecked(&vault_data.data).unwrap();
let redemption_path = [
spl_metaplex::state::PREFIX.as_bytes(),
manager.auction.as_ref(),
&meta_key.as_ref(),
];
let (bid_redemption_key, _) = Pubkey::find_program_address(&redemption_path, &program_key);
if let Some(winning_index) = auction.is_winner(&bid.bidder_pubkey) {
let destination = Keypair::new();
let winning_config = &manager.settings.winning_configs[winning_index];
for item in &winning_config.items {
let safety_deposit_result =
safety_deposits.get(&item.safety_deposit_box_index).unwrap();
let safety_deposit = &safety_deposit_result.0;
let safety_deposit_key = safety_deposit_result.1;
let signers: Vec<&Keypair> = vec![&wallet, &payer, &destination];
let mut instructions: Vec<Instruction> = vec![];
let base_account_list = BaseAccountList {
auction_manager: auction_manager_key,
store: safety_deposit.store,
destination: destination.pubkey(),
bid_redemption: bid_redemption_key,
safety_deposit_box: *safety_deposit_key,
fraction_mint: vault.fraction_mint,
vault: manager.vault,
auction: manager.auction,
bidder_metadata: meta_key,
bidder: wallet.pubkey(),
payer: payer.pubkey(),
token_vault_program,
};
let instructions = match item.winning_config_type {
WinningConfigType::TokenOnlyTransfer | WinningConfigType::Printing => {
redeem_bid_token_only_type(
base_account_list,
&manager,
item,
safety_deposit,
&program_key,
&token_key,
&mut instructions,
&client,
)
}
WinningConfigType::FullRightsTransfer => redeem_bid_rights_transfer(
base_account_list,
&manager,
safety_deposit,
&program_key,
&token_key,
&mut instructions,
&token_metadata_key,
&client,
),
};
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
println!(
"Sent prize to {:?}. If this is a Limited Edition, this is actually an authorization token to receive your prize from token metadata. To get it, you can run the following: Ex: ./target/debug/spl-token-metadata-test-client mint_new_edition_from_master_edition_via_token --mint {:?} --account {:?}. Now let's see if you have an open edition to redeem...",
destination.pubkey(), mint_map[safety_deposit.order as usize], destination.pubkey()
)
}
} else {
println!("You are not a winner, but lets see if you have open editions to redeem...");
}
if let Some(participation_config) = &manager.settings.participation_config {
println!("This auction has an open edition. Submitting!");
let safety_deposit_result = safety_deposits
.get(&participation_config.safety_deposit_box_index)
.unwrap();
let destination = Keypair::new();
let safety_deposit = &safety_deposit_result.0;
let safety_deposit_key = safety_deposit_result.1;
let transfer_authority = Keypair::new();
let signers = vec![&wallet, &transfer_authority, &payer, &destination];
let mut instructions: Vec<Instruction> = vec![];
let base_account_list = BaseAccountList {
auction_manager: auction_manager_key,
store: safety_deposit.store,
destination: destination.pubkey(),
bid_redemption: bid_redemption_key,
safety_deposit_box: *safety_deposit_key,
fraction_mint: vault.fraction_mint,
vault: manager.vault,
auction: manager.auction,
bidder_metadata: meta_key,
bidder: wallet.pubkey(),
payer: payer.pubkey(),
token_vault_program,
};
let instructions = redeem_bid_open_edition_type(
base_account_list,
&manager,
safety_deposit,
&program_key,
&token_key,
&mut instructions,
&token_metadata_key,
&transfer_authority,
&client,
&app_matches,
bid,
);
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
println!("Open edition authorization token sent to {:?}. To receive your open edition, you can call token metadata now with it. Ex: ./target/debug/spl-token-metadata-test-client mint_new_edition_from_master_edition_via_token --mint {:?} --account {:?}", destination.pubkey(), safety_deposit.token_mint, destination.pubkey());
}
}

View File

@ -0,0 +1,19 @@
{
"winning_configs": {
"items": [
{
"safety_deposit_box_index": 0,
"amount": 1,
"winning_config_type": 0
}
]
},
"participation_config": {
"winner_constraint": 1,
"non_winning_constraint": 2,
"fixed_price": null,
"safety_deposit_box_index": 1
}
}

View File

@ -0,0 +1,93 @@
use {
serde::{Deserialize, Serialize},
solana_program::pubkey::Pubkey,
spl_metaplex::state::{
AuctionManagerSettings, NonWinningConstraint, ParticipationConfig, WinningConfig,
WinningConfigItem, WinningConfigType, WinningConstraint,
},
std::fs::File,
};
#[derive(Serialize, Deserialize, Clone)]
pub struct JsonWinningConfig {
pub items: Vec<JsonWinningConfigItem>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct JsonWinningConfigItem {
pub safety_deposit_box_index: u8,
pub amount: u8,
pub winning_config_type: u8,
pub desired_supply: Option<u64>,
pub mint: Option<String>,
pub account: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct JsonParticipationConfig {
pub safety_deposit_box_index: u8,
pub mint: Option<String>,
pub account: Option<String>,
pub winner_constraint: u8,
pub non_winning_constraint: u8,
pub fixed_price: Option<u64>,
}
#[derive(Serialize, Deserialize)]
pub struct JsonAuctionManagerSettings {
pub winning_configs: Vec<JsonWinningConfig>,
pub participation_config: Option<JsonParticipationConfig>,
}
pub fn parse_metadata_keys(settings_file: &str) -> Vec<Pubkey> {
let file = File::open(settings_file).unwrap();
let json: Vec<[u8; 32]> = serde_json::from_reader(file).unwrap();
json.iter().map(|x| Pubkey::new(x)).collect::<Vec<_>>()
}
pub fn parse_settings(settings_file: &str) -> (AuctionManagerSettings, JsonAuctionManagerSettings) {
let file = File::open(settings_file).unwrap();
let json_settings: JsonAuctionManagerSettings = serde_json::from_reader(file).unwrap();
let mut parsed_winning_configs: Vec<WinningConfig> = vec![];
for n in 0..json_settings.winning_configs.len() {
let json_box = json_settings.winning_configs[n].clone();
let mut items: Vec<WinningConfigItem> = vec![];
for item in &json_box.items {
items.push(WinningConfigItem {
safety_deposit_box_index: item.safety_deposit_box_index,
amount: item.amount,
winning_config_type: match item.winning_config_type {
0 => WinningConfigType::TokenOnlyTransfer,
1 => WinningConfigType::FullRightsTransfer,
2 => WinningConfigType::Printing,
_ => WinningConfigType::TokenOnlyTransfer,
},
})
}
parsed_winning_configs.push(WinningConfig { items })
}
let settings = AuctionManagerSettings {
winning_configs: parsed_winning_configs,
participation_config: match &json_settings.participation_config {
Some(val) => Some(ParticipationConfig {
winner_constraint: match val.winner_constraint {
0 => WinningConstraint::NoParticipationPrize,
1 => WinningConstraint::ParticipationPrizeGiven,
_ => WinningConstraint::NoParticipationPrize,
},
non_winning_constraint: match val.non_winning_constraint {
0 => NonWinningConstraint::NoParticipationPrize,
1 => NonWinningConstraint::GivenForFixedPrice,
2 => NonWinningConstraint::GivenForBidPrice,
_ => NonWinningConstraint::NoParticipationPrize,
},
safety_deposit_box_index: val.safety_deposit_box_index,
fixed_price: val.fixed_price,
}),
None => None,
},
};
(settings, json_settings)
}

View File

@ -0,0 +1,37 @@
use {
clap::ArgMatches,
solana_clap_utils::input_parsers::pubkey_of,
solana_client::rpc_client::RpcClient,
solana_program::{borsh::try_from_slice_unchecked, pubkey::Pubkey},
solana_sdk::signature::Keypair,
spl_auction::processor::{AuctionData, AuctionDataExtended},
spl_metaplex::state::AuctionManager,
};
pub fn send_show(app_matches: &ArgMatches, _payer: Keypair, client: RpcClient) {
let auction_manager_key = pubkey_of(app_matches, "auction_manager").unwrap();
let account = client.get_account(&auction_manager_key).unwrap();
let manager: AuctionManager = try_from_slice_unchecked(&account.data).unwrap();
let auction_data = client.get_account(&manager.auction).unwrap();
let auction: AuctionData = try_from_slice_unchecked(&auction_data.data).unwrap();
let auction_program = spl_auction::id();
let seeds = &[
spl_auction::PREFIX.as_bytes(),
&auction_program.as_ref(),
manager.vault.as_ref(),
spl_auction::EXTENDED.as_bytes(),
];
let (extended, _) = Pubkey::find_program_address(seeds, &auction_program);
let auction_data = client.get_account(&extended).unwrap();
let auction_ext: AuctionDataExtended = try_from_slice_unchecked(&auction_data.data).unwrap();
let curr_slot = client.get_slot();
println!("Auction Manager: {:#?}", manager);
println!("Auction: #{:#?}", auction);
println!("Extended data: {:#?}", auction_ext);
println!(
"Current slot: {:?}, Auction ends at: {:?}",
curr_slot, auction.ended_at
)
}

View File

@ -0,0 +1,47 @@
use {
crate::PROGRAM_PUBKEY,
clap::ArgMatches,
solana_clap_utils::input_parsers::pubkey_of,
solana_client::rpc_client::RpcClient,
solana_program::borsh::try_from_slice_unchecked,
solana_sdk::{
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
transaction::Transaction,
},
spl_metaplex::instruction::create_start_auction_instruction,
spl_metaplex::state::AuctionManager,
std::str::FromStr,
};
pub fn send_start_auction(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
let program_key = Pubkey::from_str(PROGRAM_PUBKEY).unwrap();
let authority = read_keypair_file(
app_matches
.value_of("authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let auction_manager_key = pubkey_of(app_matches, "auction_manager").unwrap();
let account = client.get_account(&auction_manager_key).unwrap();
let manager: AuctionManager = try_from_slice_unchecked(&account.data).unwrap();
let instructions = [create_start_auction_instruction(
program_key,
auction_manager_key,
manager.auction,
authority.pubkey(),
manager.store,
)];
let signers = [&payer];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
println!("Started auction.");
}

View File

@ -0,0 +1,173 @@
use {
crate::{settings_utils::parse_metadata_keys, PROGRAM_PUBKEY, VAULT_PROGRAM_PUBKEY},
arrayref::array_ref,
clap::ArgMatches,
solana_clap_utils::input_parsers::pubkey_of,
solana_client::rpc_client::RpcClient,
solana_program::borsh::try_from_slice_unchecked,
solana_sdk::{
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
transaction::Transaction,
},
spl_metaplex::{
instruction::create_validate_safety_deposit_box_instruction,
state::{AuctionManager, WinningConfig},
},
spl_token_metadata::state::{Key, MasterEdition, EDITION},
spl_token_vault::state::{SafetyDepositBox, SAFETY_DEPOSIT_KEY},
std::{collections::HashMap, str::FromStr},
};
pub fn validate_safety_deposits(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
let program_key = Pubkey::from_str(PROGRAM_PUBKEY).unwrap();
let vault_program_key = Pubkey::from_str(VAULT_PROGRAM_PUBKEY).unwrap();
let token_metadata_key = spl_token_metadata::id();
let admin = read_keypair_file(
app_matches
.value_of("admin")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let admin_key = admin.pubkey();
let store_seeds = &[
spl_metaplex::state::PREFIX.as_bytes(),
&program_key.as_ref(),
&admin_key.as_ref(),
];
let (store_key, _) = Pubkey::find_program_address(store_seeds, &program_key);
let authority = read_keypair_file(
app_matches
.value_of("authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let metadata_authority = read_keypair_file(
app_matches
.value_of("metadata_authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let auction_manager_key = pubkey_of(app_matches, "auction_manager").unwrap();
let mint_map = parse_metadata_keys(&(auction_manager_key.to_string() + ".json"));
let account = client.get_account(&auction_manager_key).unwrap();
let manager: AuctionManager = try_from_slice_unchecked(&account.data).unwrap();
let all_vault_accounts = client.get_program_accounts(&vault_program_key).unwrap();
let mut safety_deposits = HashMap::new();
for acc in &all_vault_accounts {
let obj = &acc.1;
let obj_key = &acc.0;
let type_of_obj = obj.data[0];
if type_of_obj == SAFETY_DEPOSIT_KEY {
let pubkey_arr = array_ref![obj.data, 1, 32];
let pubkey = Pubkey::new_from_array(*pubkey_arr);
if pubkey == manager.vault {
let safety_deposit: SafetyDepositBox = try_from_slice_unchecked(&obj.data).unwrap();
safety_deposits.insert(safety_deposit.order, (safety_deposit, *obj_key));
}
}
}
let winner_config_slot = app_matches
.value_of("winner_config_slot")
.unwrap_or("-1")
.parse::<i64>()
.unwrap();
let mut configs_to_validate: Vec<&WinningConfig> = vec![];
if winner_config_slot == -1 {
for config in &manager.settings.winning_configs {
configs_to_validate.push(config);
}
} else {
configs_to_validate.push(&manager.settings.winning_configs[winner_config_slot as usize]);
}
for n in 0..configs_to_validate.len() {
let config = &configs_to_validate[n];
for item in &config.items {
let (config_box, box_key) =
safety_deposits.get(&item.safety_deposit_box_index).unwrap();
let metadata_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
&token_metadata_key.as_ref(),
&mint_map[n].as_ref(),
];
let (metadata_key, _) =
Pubkey::find_program_address(metadata_seeds, &token_metadata_key);
let edition_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
&token_metadata_key.as_ref(),
mint_map[n].as_ref(),
EDITION.as_bytes(),
];
let (edition_key, _) = Pubkey::find_program_address(edition_seeds, &token_metadata_key);
let original_authority_seeds = &[
spl_metaplex::state::PREFIX.as_bytes(),
manager.auction.as_ref(),
metadata_key.as_ref(),
];
let (original_authority_key, _) =
Pubkey::find_program_address(original_authority_seeds, &program_key);
let master_edition_account = client.get_account(&edition_key);
let edition_printing_mint: Option<Pubkey>;
let edition_printing_mint_authority: Option<Pubkey>;
match master_edition_account {
Ok(acct) => {
if acct.data[0] == Key::MasterEditionV1 as u8 {
let master_edition: MasterEdition =
try_from_slice_unchecked(&acct.data).unwrap();
edition_printing_mint = Some(master_edition.printing_mint);
edition_printing_mint_authority = Some(payer.pubkey());
} else {
edition_printing_mint = None;
edition_printing_mint_authority = None;
}
}
Err(_) => {
edition_printing_mint = None;
edition_printing_mint_authority = None;
}
}
let instructions = [create_validate_safety_deposit_box_instruction(
program_key,
auction_manager_key,
metadata_key,
original_authority_key,
solana_program::system_program::id(),
store_key,
*box_key,
config_box.store,
config_box.token_mint,
edition_key,
manager.vault,
authority.pubkey(),
metadata_authority.pubkey(),
payer.pubkey(),
edition_printing_mint,
edition_printing_mint_authority,
)];
let signers = [&payer, &authority, &metadata_authority];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
println!("Validated safety deposit box {:?} which contained token account {:?} in winning slot {:?}", box_key, config_box.store, n);
}
}
}

View File

@ -0,0 +1,644 @@
use {
crate::{TOKEN_PROGRAM_PUBKEY, VAULT_PROGRAM_PUBKEY},
solana_client::rpc_client::RpcClient,
solana_program::{borsh::try_from_slice_unchecked, program_pack::Pack},
solana_sdk::{
pubkey::Pubkey,
signature::{Keypair, Signer},
system_instruction::create_account,
transaction::Transaction,
},
spl_token::{
instruction::{approve, initialize_account, initialize_mint, mint_to},
state::{Account, Mint},
},
spl_token_metadata::{
instruction::{create_master_edition, create_metadata_accounts},
state::EDITION,
},
spl_token_vault::{
instruction::{
create_activate_vault_instruction, create_add_token_to_inactive_vault_instruction,
create_combine_vault_instruction, create_init_vault_instruction,
},
state::{ExternalPriceAccount, Vault, VaultState, MAX_VAULT_SIZE},
},
std::str::FromStr,
};
#[allow(clippy::clone_on_copy)]
#[allow(clippy::too_many_arguments)]
pub fn add_token_to_vault(
vault_authority: &Keypair,
vault_key: &Pubkey,
payer: &Keypair,
client: &RpcClient,
amount: u64,
existing_mint: Option<Pubkey>,
existing_account: Option<Pubkey>,
is_master_edition: bool,
token_supply: Option<u64>,
is_participation: bool,
) -> (Pubkey, Pubkey, Pubkey) {
let program_key = Pubkey::from_str(VAULT_PROGRAM_PUBKEY).unwrap();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let store = Keypair::new();
let mut instructions = vec![];
let signers = vec![payer, &store];
let token_mint = Keypair::new();
let mint_key = match existing_mint {
None => {
// Due to txn size limits, need to do this in a separate one.
let create_signers = [&payer, &token_mint];
let create_mint_instructions = [
create_account(
&payer.pubkey(),
&token_mint.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
),
initialize_mint(
&token_key,
&token_mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
];
let mut transaction =
Transaction::new_with_payer(&create_mint_instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&create_signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
token_mint.pubkey()
}
Some(val) => val,
};
// The Printing mint needs to be the store type if we're doing limited editions since we're actually
// handing out authorization tokens
let printing_mint = Keypair::new();
let one_time_printing_authorization_mint = Keypair::new();
let store_mint_key = match token_supply {
Some(_) => printing_mint.pubkey(),
None => mint_key,
};
let seeds = &[
spl_token_vault::state::PREFIX.as_bytes(),
&vault_key.as_ref(),
&store_mint_key.as_ref(),
];
let (safety_deposit_box, _) = Pubkey::find_program_address(seeds, &program_key);
let seeds = &[
spl_token_vault::state::PREFIX.as_bytes(),
&program_key.as_ref(),
];
let (authority, _) = Pubkey::find_program_address(seeds, &program_key);
let token_metadata = spl_token_metadata::id();
let metadata_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
&token_metadata.as_ref(),
&mint_key.as_ref(),
];
let (metadata_key, _) = Pubkey::find_program_address(metadata_seeds, &spl_token_metadata::id());
let edition_seeds = &[
spl_token_metadata::state::PREFIX.as_bytes(),
&token_metadata.as_ref(),
mint_key.as_ref(),
EDITION.as_bytes(),
];
let (edition_key, _) = Pubkey::find_program_address(edition_seeds, &spl_token_metadata::id());
let token_account = Keypair::new();
let token_account_key = match existing_account {
None => {
instructions.push(create_metadata_accounts(
spl_token_metadata::id(),
metadata_key,
mint_key,
payer.pubkey(),
payer.pubkey(),
payer.pubkey(),
"no".to_owned(),
"name".to_owned(),
"www.none.com".to_owned(),
None,
0,
true,
false,
));
if is_master_edition {
let master_signers = [
&payer,
&printing_mint,
&one_time_printing_authorization_mint,
];
let master_account_instructions = [
create_account(
&payer.pubkey(),
&printing_mint.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
),
initialize_mint(
&token_key,
&printing_mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
create_account(
&payer.pubkey(),
&one_time_printing_authorization_mint.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
),
initialize_mint(
&token_key,
&one_time_printing_authorization_mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
];
let mut master_transaction = Transaction::new_with_payer(
&master_account_instructions,
Some(&payer.pubkey()),
);
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
master_transaction.sign(&master_signers, recent_blockhash);
client
.send_and_confirm_transaction(&master_transaction)
.unwrap();
}
let token_account_mint = match token_supply {
Some(_) => {
if is_participation {
one_time_printing_authorization_mint.pubkey()
} else {
printing_mint.pubkey()
}
}
None => mint_key,
};
// Due to txn size limits, need to do this in a separate one.
let mut create_signers = vec![payer, &token_account];
let mut create_account_instructions = vec![
create_account(
&payer.pubkey(),
&token_account.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
),
initialize_account(
&token_key,
&token_account.pubkey(),
&token_account_mint,
&payer.pubkey(),
)
.unwrap(),
];
let extra_real_token_acct = Keypair::new();
if token_supply.is_some() {
create_signers.push(&extra_real_token_acct);
// means the token account above is actually a Printing mint account, we need a separate account to have
// at least one of the main token type in it.
create_account_instructions.push(create_account(
&payer.pubkey(),
&extra_real_token_acct.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
create_account_instructions.push(
initialize_account(
&token_key,
&extra_real_token_acct.pubkey(),
&mint_key,
&payer.pubkey(),
)
.unwrap(),
);
create_account_instructions.push(
mint_to(
&token_key,
&mint_key,
&extra_real_token_acct.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
1,
)
.unwrap(),
);
} else {
// we just need to mint the tokens to this account because we're going to transfer tokens
// out of it.
create_account_instructions.push(
mint_to(
&token_key,
&mint_key,
&token_account.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
amount,
)
.unwrap(),
);
}
let mut transaction =
Transaction::new_with_payer(&create_account_instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&create_signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
if is_master_edition {
let one_time_printing_authorization_mint_authority: Option<Pubkey> =
match token_supply {
Some(_) => Some(payer.pubkey()),
None => None,
};
instructions.push(create_master_edition(
spl_token_metadata::id(),
edition_key,
mint_key,
printing_mint.pubkey(),
one_time_printing_authorization_mint.pubkey(),
payer.pubkey(),
payer.pubkey(),
payer.pubkey(),
metadata_key,
payer.pubkey(),
token_supply,
one_time_printing_authorization_mint_authority,
));
}
token_account.pubkey()
}
Some(val) => val,
};
instructions.push(create_account(
&payer.pubkey(),
&store.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(&token_key, &store.pubkey(), &store_mint_key, &authority).unwrap(),
);
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let transfer_authority = Keypair::new();
let token_instructions = vec![
approve(
&token_key,
&token_account_key,
&transfer_authority.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
amount,
)
.unwrap(),
create_add_token_to_inactive_vault_instruction(
program_key,
safety_deposit_box,
token_account_key,
store.pubkey(),
vault_key.clone(),
vault_authority.pubkey(),
payer.pubkey(),
transfer_authority.pubkey(),
amount,
),
];
let token_signers = vec![payer, &transfer_authority];
let mut token_transaction =
Transaction::new_with_payer(&token_instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
token_transaction.sign(&token_signers, recent_blockhash);
client
.send_and_confirm_transaction(&token_transaction)
.unwrap();
let _account = client.get_account(&safety_deposit_box).unwrap();
(safety_deposit_box, mint_key, store.pubkey())
}
pub fn activate_vault(
vault_authority: &Keypair,
vault_key: &Pubkey,
payer: &Keypair,
client: &RpcClient,
) -> Option<Pubkey> {
let program_key = Pubkey::from_str(VAULT_PROGRAM_PUBKEY).unwrap();
let number_of_shares: u64 = 0;
let vault_account = client.get_account(&vault_key).unwrap();
let vault: Vault = try_from_slice_unchecked(&vault_account.data).unwrap();
let seeds = &[
spl_token_vault::state::PREFIX.as_bytes(),
&program_key.as_ref(),
];
let (mint_authority, _) = Pubkey::find_program_address(seeds, &program_key);
let instructions = [create_activate_vault_instruction(
program_key,
*vault_key,
vault.fraction_mint,
vault.fraction_treasury,
mint_authority,
vault_authority.pubkey(),
number_of_shares,
)];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
let signers = vec![payer, &vault_authority];
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let updated_vault_data = client.get_account(&vault_key).unwrap();
let updated_vault: Vault = try_from_slice_unchecked(&updated_vault_data.data).unwrap();
if updated_vault.state == VaultState::Active {
Some(*vault_key)
} else {
None
}
}
pub fn combine_vault(
vault_authority: &Keypair,
new_vault_authority: &Pubkey,
vault_key: &Pubkey,
payer: &Keypair,
client: &RpcClient,
) -> Option<Pubkey> {
let program_key = Pubkey::from_str(VAULT_PROGRAM_PUBKEY).unwrap();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let amount_of_money = 0;
let vault_account = client.get_account(&vault_key).unwrap();
let vault: Vault = try_from_slice_unchecked(&vault_account.data).unwrap();
let external_price_account = client.get_account(&vault.pricing_lookup_address).unwrap();
let external: ExternalPriceAccount =
try_from_slice_unchecked(&external_price_account.data).unwrap();
let payment_account = Keypair::new();
let seeds = &[
spl_token_vault::state::PREFIX.as_bytes(),
&program_key.as_ref(),
];
let (uncirculated_burn_authority, _) = Pubkey::find_program_address(seeds, &program_key);
let transfer_authority = Keypair::new();
let mut signers = vec![
payer,
&vault_authority,
&payment_account,
&transfer_authority,
];
let mut instructions = vec![
create_account(
&payer.pubkey(),
&payment_account.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
),
initialize_account(
&token_key,
&payment_account.pubkey(),
&external.price_mint,
&payer.pubkey(),
)
.unwrap(),
mint_to(
&token_key,
&external.price_mint,
&payment_account.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
amount_of_money,
)
.unwrap(),
approve(
&token_key,
&payment_account.pubkey(),
&transfer_authority.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
amount_of_money,
)
.unwrap(),
];
let shares_outstanding: u64 = 0;
let outstanding_shares_account = Keypair::new();
// We make an empty oustanding share account if one is not provided.
instructions.push(create_account(
&payer.pubkey(),
&outstanding_shares_account.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(
&token_key,
&outstanding_shares_account.pubkey(),
&vault.fraction_mint,
&payer.pubkey(),
)
.unwrap(),
);
signers.push(&outstanding_shares_account);
instructions.push(
approve(
&token_key,
&outstanding_shares_account.pubkey(),
&transfer_authority.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
shares_outstanding,
)
.unwrap(),
);
instructions.push(create_combine_vault_instruction(
program_key,
*vault_key,
outstanding_shares_account.pubkey(),
payment_account.pubkey(),
vault.fraction_mint,
vault.fraction_treasury,
vault.redeem_treasury,
*new_vault_authority,
vault_authority.pubkey(),
transfer_authority.pubkey(),
uncirculated_burn_authority,
vault.pricing_lookup_address,
));
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let updated_vault_data = client.get_account(&vault_key).unwrap();
let updated_vault: Vault = try_from_slice_unchecked(&updated_vault_data.data).unwrap();
if updated_vault.state == VaultState::Combined {
Some(*vault_key)
} else {
None
}
}
pub fn initialize_vault(
vault_authority: &Keypair,
external_key: &Pubkey,
payer: &Keypair,
client: &RpcClient,
) -> Pubkey {
let program_key = Pubkey::from_str(VAULT_PROGRAM_PUBKEY).unwrap();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let external_account = client.get_account(&external_key).unwrap();
let external: ExternalPriceAccount = try_from_slice_unchecked(&external_account.data).unwrap();
let fraction_mint = Keypair::new();
let redeem_mint = external.price_mint;
let redeem_treasury = Keypair::new();
let fraction_treasury = Keypair::new();
let vault = Keypair::new();
let allow_further_share_creation = false;
let seeds = &[
spl_token_vault::state::PREFIX.as_bytes(),
&program_key.as_ref(),
];
let (authority, _) = Pubkey::find_program_address(seeds, &program_key);
let instructions = [
create_account(
&payer.pubkey(),
&fraction_mint.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
),
create_account(
&payer.pubkey(),
&redeem_treasury.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
),
create_account(
&payer.pubkey(),
&fraction_treasury.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
),
create_account(
&payer.pubkey(),
&vault.pubkey(),
client
.get_minimum_balance_for_rent_exemption(MAX_VAULT_SIZE)
.unwrap(),
MAX_VAULT_SIZE as u64,
&program_key,
),
initialize_mint(
&token_key,
&fraction_mint.pubkey(),
&authority,
Some(&authority),
0,
)
.unwrap(),
initialize_account(
&token_key,
&redeem_treasury.pubkey(),
&redeem_mint,
&authority,
)
.unwrap(),
initialize_account(
&token_key,
&fraction_treasury.pubkey(),
&fraction_mint.pubkey(),
&authority,
)
.unwrap(),
create_init_vault_instruction(
program_key,
fraction_mint.pubkey(),
redeem_treasury.pubkey(),
fraction_treasury.pubkey(),
vault.pubkey(),
vault_authority.pubkey(),
*external_key,
allow_further_share_creation,
),
];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
let signers = vec![
payer,
&redeem_treasury,
&fraction_treasury,
&fraction_mint,
&vault,
];
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let _account = client.get_account(&vault.pubkey()).unwrap();
vault.pubkey()
}

61
rust/patch.crates-io.sh Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# Patches the SPL crates for developing against a local solana monorepo
#
solana_dir=$1
if [[ -z $solana_dir ]]; then
echo "Usage: $0 <path-to-solana-monorepo>"
exit 1
fi
workspace_crates=(
Cargo.toml
themis/client_ristretto/Cargo.toml
)
if [[ ! -r "$solana_dir"/scripts/read-cargo-variable.sh ]]; then
echo "$solana_dir is not a path to the solana monorepo"
exit 1
fi
set -e
solana_dir=$(cd "$solana_dir" && pwd)
cd "$(dirname "$0")"
source "$solana_dir"/scripts/read-cargo-variable.sh
solana_ver=$(readCargoVariable version "$solana_dir"/sdk/Cargo.toml)
echo "Patching in $solana_ver from $solana_dir"
echo
for crate in "${workspace_crates[@]}"; do
if grep -q '\[patch.crates-io\]' "$crate"; then
echo "$crate is already patched"
else
cat >> "$crate" <<PATCH
[patch.crates-io]
solana-account-decoder = {path = "$solana_dir/account-decoder" }
solana-banks-client = { path = "$solana_dir/banks-client"}
solana-banks-server = { path = "$solana_dir/banks-server"}
solana-bpf-loader-program = { path = "$solana_dir/programs/bpf_loader" }
solana-clap-utils = {path = "$solana_dir/clap-utils" }
solana-cli-config = {path = "$solana_dir/cli-config" }
solana-cli-output = {path = "$solana_dir/cli-output" }
solana-client = { path = "$solana_dir/client"}
solana-core = { path = "$solana_dir/core"}
solana-logger = {path = "$solana_dir/logger" }
solana-notifier = { path = "$solana_dir/notifier" }
solana-remote-wallet = {path = "$solana_dir/remote-wallet" }
solana-program = { path = "$solana_dir/sdk/program" }
solana-program-test = { path = "$solana_dir/program-test" }
solana-runtime = { path = "$solana_dir/runtime" }
solana-sdk = { path = "$solana_dir/sdk" }
solana-stake-program = { path = "$solana_dir/programs/stake" }
solana-transaction-status = { path = "$solana_dir/transaction-status" }
solana-vote-program = { path = "$solana_dir/programs/vote" }
PATCH
fi
done
./update-solana-dependencies.sh "$solana_ver"

View File

@ -0,0 +1,24 @@
[package]
name = "spl-token-metadata"
version = "0.0.1"
description = "Metaplex Metadata"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
exclude = ["js/**"]
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
num-derive = "0.3"
num-traits = "0.2"
solana-program = "1.6.10"
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }
thiserror = "1.0"
borsh = "0.8.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,128 @@
---
title: Token Metadata Program
---
## Background
Solana's programming model and the definitions of the Solana terms used in this
document are available at:
- https://docs.solana.com/apps
- https://docs.solana.com/terminology
## Source
The Token Metadata Program's source is available on
[github](https://github.com/metaplex-foundation/metaplex)
There is also an example Rust client located at
[github](https://github.com/metaplex-foundation/metaplex/tree/master/token_metadata/test/src/main.rs)
that can be perused for learning and run if desired with `cargo run --bin spl-token-metadata-test-client`. It allows testing out a variety of scenarios.
## Interface
The on-chain Token Metadata program is written in Rust and available on crates.io as
[spl-token-metadata](https://crates.io/crates/spl-token-metadata) and
[docs.rs](https://docs.rs/spl-token-metadata).
The crate provides four instructions, `create_metadata_account()`, `update_metadata_account()`, `create_master_edition()`, `mint_new_edition_from_master_edition_via_token(),` to easily create instructions for the program.
## Operational overview
This is a very simple program designed to allow metadata tagging to a given mint, with an update authority
that can change that metadata going forward. Optionally, owners of the metadata can choose to tag this metadata
as a master edition and then use this master edition to label child mints as "limited editions" of this master
edition going forward. The owners of the metadata do not need to be involved in every step of the process,
as any holder of a master edition mint token can have their mint labeled as a limited edition without
the involvement or signature of the owner, this allows for the sale and distribution of master edition prints.
## Operational flow for Master Editions
It would be useful before a dive into architecture to illustrate the flow for a master edition
as a story because it makes it easier to understand.
1. User creates a new Metadata for their mint with `create_metadata_account()` which makes new `Metadata`
2. User wishes their mint to be a master edition and ensures that there
is only required supply of one in the mint.
3. User requests the program to designate `create_master_edition()` on their metadata,
which creates new `MasterEdition` which for this example we will say has an unlimited supply. As
part of the arguments to the function the user is required to make a new mint called the Printing mint over
which they have minting authority that they tell the contract about and that the contract stores ont he
`MasterEdition`.
4. User mints a token from the Printing mint and gives it to their friend.
5. Their friend creates a new mint with supply 1 and calls `mint_new_edition_from_master_edition_via_token()`,
which creates for them new `Metadata` and `Edition` records signifying this mint as an Edition child of
the master edition original.
There is a slight variation on this theme if `create_master_edition()` is given a max_supply: minting authority
is locked within the program for the Printing mint and all minting takes place immediately in
`create_master_edition()` to a designated account the user provides and owns -
the user then uses this fixed pool as the source of their authorization tokens going forward to prevent new
supply from being generated in an unauthorized manner.
### Permissioning and Architecture
There are three different major structs in the app: Metadata, MasterEditions, and Editions. A Metadata can
have zero or one MasterEdition, OR can have zero or one Edition, but CANNOT have both a MasterEdition AND
an Edition associated with it. This is to say a Metadata is EITHER a master edition
or a edition(child record) of another master edition.
Only the minting authority on a mint can create metadata accounts. A Metadata account holds the name, symbol,
and uri of the mint, as well as the mint id. To ensure the uniqueness of
a mint's metadata, the address of a Metadata account is a program derived address composed of seeds:
```rust
["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref()]
```
A master edition is an extension account of this PDA, being simply:
```rust
["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref(), "edition".as_bytes()]
```
Any limited edition minted from this has the same address, but is of a different struct type. The reason
these two different structs(Edition and MasterEdition) share the same address is to ensure that there can
be no Metadata that has both, which would make no sense in the current architecture.
### create_metadata_account
(Mint authority must be signer)
This action creates the `Metadata` account.
### update_metadata_account
(Update authority must be signer)
This call can be called at any time by the update authority to update the URI on any metadata or
update authority on metadata, and later other fields.
### create_master_edition
(Update authority must be signer)
This can only be called once, and only if the supply on the mint is one. It will create a `MasterEdition` record.
Now other Mints can become Editions of this Metadata if they have the proper authorization token.
### mint_new_edition_from_master_edition_via_token
(Mint authority of new mint must be signer)
If one possesses a token from the Printing mint of the master edition and a brand new mint with no `Metadata`, and
that mint has only a supply of one, this mint can be turned into an `Edition` of this parent `Master Edition` by
calling this endpoint. This endpoint both creates the `Edition` and `Metadata` records and burns the token.
### Further extensions
This program is designed to be extended with further account buckets.
If say, we wanted to add metadata for youtube metadata, we could create a new struct called Youtube
and seed it with the seed
```rust
["metadata".as_bytes(), program_id.as_ref(), mint_key.as_ref(), "youtube".as_bytes()]
```
And then only those interested in that metadata need search for it, and its uniqueness is ensured. It can also
have it's own update action that follows a similar pattern to the original update action.

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,25 @@
//! Program entrypoint definitions
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
use {
crate::{error::MetadataError, processor},
solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
program_error::PrintProgramError, pubkey::Pubkey,
},
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) {
// catch the error so we can print it
error.print::<MetadataError>();
return Err(error);
}
Ok(())
}

View File

@ -0,0 +1,287 @@
//! Error types
use {
num_derive::FromPrimitive,
solana_program::{
decode_error::DecodeError,
msg,
program_error::{PrintProgramError, ProgramError},
},
thiserror::Error,
};
/// Errors that may be returned by the Metadata program.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum MetadataError {
/// Failed to unpack instruction data
#[error("Failed to unpack instruction data")]
InstructionUnpackError,
/// Failed to pack instruction data
#[error("Failed to pack instruction data")]
InstructionPackError,
/// Lamport balance below rent-exempt threshold.
#[error("Lamport balance below rent-exempt threshold")]
NotRentExempt,
/// Already initialized
#[error("Already initialized")]
AlreadyInitialized,
/// Uninitialized
#[error("Uninitialized")]
Uninitialized,
/// Metadata's key must match seed of ['metadata', program id, mint] provided
#[error(" Metadata's key must match seed of ['metadata', program id, mint] provided")]
InvalidMetadataKey,
/// Edition's key must match seed of ['metadata', program id, name, 'edition'] provided
#[error("Edition's key must match seed of ['metadata', program id, name, 'edition'] provided")]
InvalidEditionKey,
/// Update Authority given does not match
#[error("Update Authority given does not match")]
UpdateAuthorityIncorrect,
/// Update Authority needs to be signer to update metadata
#[error("Update Authority needs to be signer to update metadata")]
UpdateAuthorityIsNotSigner,
/// You must be the mint authority and signer on this transaction
#[error("You must be the mint authority and signer on this transaction")]
NotMintAuthority,
/// Mint authority provided does not match the authority on the mint
#[error("Mint authority provided does not match the authority on the mint")]
InvalidMintAuthority,
/// Name too long
#[error("Name too long")]
NameTooLong,
/// Symbol too long
#[error("Symbol too long")]
SymbolTooLong,
/// URI too long
#[error("URI too long")]
UriTooLong,
/// Update authority must be equivalent to the metadata's authority and also signer of this transaction
#[error("Update authority must be equivalent to the metadata's authority and also signer of this transaction")]
UpdateAuthorityMustBeEqualToMetadataAuthorityAndSigner,
/// Mint given does not match mint on Metadata
#[error("Mint given does not match mint on Metadata")]
MintMismatch,
/// Editions must have exactly one token
#[error("Editions must have exactly one token")]
EditionsMustHaveExactlyOneToken,
/// Maximum editions printed already
#[error("Maximum editions printed already")]
MaxEditionsMintedAlready,
/// Token mint to failed
#[error("Token mint to failed")]
TokenMintToFailed,
/// The master edition record passed must match the master record on the edition given
#[error("The master edition record passed must match the master record on the edition given")]
MasterRecordMismatch,
/// The destination account does not have the right mint
#[error("The destination account does not have the right mint")]
DestinationMintMismatch,
/// An edition can only mint one of its kind!
#[error("An edition can only mint one of its kind!")]
EditionAlreadyMinted,
/// Printing mint decimals should be zero
#[error("Printing mint decimals should be zero")]
PrintingMintDecimalsShouldBeZero,
/// OneTimePrintingAuthorizationMint mint decimals should be zero
#[error("OneTimePrintingAuthorization mint decimals should be zero")]
OneTimePrintingAuthorizationMintDecimalsShouldBeZero,
/// Edition mint decimals should be zero
#[error("EditionMintDecimalsShouldBeZero")]
EditionMintDecimalsShouldBeZero,
/// Token burn failed
#[error("Token burn failed")]
TokenBurnFailed,
/// The One Time authorization mint does not match that on the token account!
#[error("The One Time authorization mint does not match that on the token account!")]
TokenAccountOneTimeAuthMintMismatch,
/// Derived key invalid
#[error("Derived key invalid")]
DerivedKeyInvalid,
/// The Printing mint does not match that on the master edition!
#[error("The Printing mint does not match that on the master edition!")]
PrintingMintMismatch,
/// The One Time Printing Auth mint does not match that on the master edition!
#[error("The One Time Printing Auth mint does not match that on the master edition!")]
OneTimePrintingAuthMintMismatch,
/// The mint of the token account does not match the Printing mint!
#[error("The mint of the token account does not match the Printing mint!")]
TokenAccountMintMismatch,
/// Not enough tokens to mint a limited edition
#[error("Not enough tokens to mint a limited edition")]
NotEnoughTokens,
/// The mint on your authorization token holding account does not match your Printing mint!
#[error(
"The mint on your authorization token holding account does not match your Printing mint!"
)]
PrintingMintAuthorizationAccountMismatch,
/// The authorization token account has a different owner than the update authority for the master edition!
#[error("The authorization token account has a different owner than the update authority for the master edition!")]
AuthorizationTokenAccountOwnerMismatch,
/// This feature is currently disabled.
#[error("This feature is currently disabled.")]
Disabled,
/// Creators list too long
#[error("Creators list too long")]
CreatorsTooLong,
/// Creators must be at least one if set
#[error("Creators must be at least one if set")]
CreatorsMustBeAtleastOne,
/// If using a creators array, you must be one of the creators listed
#[error("If using a creators array, you must be one of the creators listed")]
MustBeOneOfCreators,
/// This metadata does not have creators
#[error("This metadata does not have creators")]
NoCreatorsPresentOnMetadata,
/// This creator address was not found
#[error("This creator address was not found")]
CreatorNotFound,
/// Basis points cannot be more than 10000
#[error("Basis points cannot be more than 10000")]
InvalidBasisPoints,
/// Primary sale can only be flipped to true and is immutable
#[error("Primary sale can only be flipped to true and is immutable")]
PrimarySaleCanOnlyBeFlippedToTrue,
/// Owner does not match that on the account given
#[error("Owner does not match that on the account given")]
OwnerMismatch,
/// This account has no tokens to be used for authorization
#[error("This account has no tokens to be used for authorization")]
NoBalanceInAccountForAuthorization,
/// Share total must equal 100 for creator array
#[error("Share total must equal 100 for creator array")]
ShareTotalMustBe100,
/// This reservation list already exists!
#[error("This reservation list already exists!")]
ReservationExists,
/// This reservation list does not exist!
#[error("This reservation list does not exist!")]
ReservationDoesNotExist,
/// This reservation list exists but was never set with reservations
#[error("This reservation list exists but was never set with reservations")]
ReservationNotSet,
/// This reservation list has already been set!
#[error("This reservation list has already been set!")]
ReservationAlreadyMade,
/// Provided more addresses than max allowed in single reservation
#[error("Provided more addresses than max allowed in single reservation")]
BeyondMaxAddressSize,
/// NumericalOverflowError
#[error("NumericalOverflowError")]
NumericalOverflowError,
/// This reservation would go beyond the maximum supply of the master edition!
#[error("This reservation would go beyond the maximum supply of the master edition!")]
ReservationBreachesMaximumSupply,
/// Address not in reservation!
#[error("Address not in reservation!")]
AddressNotInReservation,
/// You cannot unilaterally verify another creator, they must sign
#[error("You cannot unilaterally verify another creator, they must sign")]
CannotVerifyAnotherCreator,
/// You cannot unilaterally unverify another creator
#[error("You cannot unilaterally unverify another creator")]
CannotUnverifyAnotherCreator,
/// In initial reservation setting, spots remaining should equal total spots
#[error("In initial reservation setting, spots remaining should equal total spots")]
SpotMismatch,
/// Incorrect account owner
#[error("Incorrect account owner")]
IncorrectOwner,
/// printing these tokens would breach the maximum supply limit of the master edition
#[error("printing these tokens would breach the maximum supply limit of the master edition")]
PrintingWouldBreachMaximumSupply,
/// Data is immutable
#[error("Data is immutable")]
DataIsImmutable,
/// No duplicate creator addresses
#[error("No duplicate creator addresses")]
DuplicateCreatorAddress,
/// Reservation spots remaining should match total spots when first being created
#[error("Reservation spots remaining should match total spots when first being created")]
ReservationSpotsRemainingShouldMatchTotalSpotsAtStart,
/// Invalid token program
#[error("Invalid token program")]
InvalidTokenProgram,
/// Data type mismatch
#[error("Data type mismatch")]
DataTypeMismatch,
}
impl PrintProgramError for MetadataError {
fn print<E>(&self) {
msg!(&self.to_string());
}
}
impl From<MetadataError> for ProgramError {
fn from(e: MetadataError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for MetadataError {
fn type_of() -> &'static str {
"Metadata Error"
}
}

View File

@ -0,0 +1,469 @@
use {
crate::state::{Creator, Data, Reservation},
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
sysvar,
},
};
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
/// Args for update call
pub struct UpdateMetadataAccountArgs {
pub data: Option<Data>,
pub update_authority: Option<Pubkey>,
pub primary_sale_happened: Option<bool>,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
/// Args for create call
pub struct CreateMetadataAccountArgs {
/// Note that unique metadatas are disabled for now.
pub data: Data,
/// Whether you want your metadata to be updateable in the future.
pub is_mutable: bool,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct CreateMasterEditionArgs {
/// If set, means that no more than this number of editions can ever be minted. This is immutable.
pub max_supply: Option<u64>,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct MintPrintingTokensViaTokenArgs {
pub supply: u64,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct SetReservationListArgs {
/// If set, means that no more than this number of editions can ever be minted. This is immutable.
pub reservations: Vec<Reservation>,
}
/// Instructions supported by the Metadata program.
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub enum MetadataInstruction {
/// Create Metadata object.
/// 0. `[writable]` Metadata key (pda of ['metadata', program id, mint id])
/// 1. `[]` Mint of token asset
/// 2. `[signer]` Mint authority
/// 3. `[signer]` payer
/// 4. `[]` update authority info
/// 5. `[]` System program
/// 6. `[]` Rent info
CreateMetadataAccount(CreateMetadataAccountArgs),
/// Update a Metadata
/// 0. `[writable]` Metadata account
/// 1. `[signer]` Update authority key
UpdateMetadataAccount(UpdateMetadataAccountArgs),
/// Register a Metadata as a Master Edition, which means Editions can be minted.
/// Henceforth, no further tokens will be mintable from this primary mint. Will throw an error if more than one
/// token exists, and will throw an error if less than one token exists in this primary mint.
/// 0. `[writable]` Unallocated edition account with address as pda of ['metadata', program id, mint, 'edition']
/// 1. `[writable]` Metadata mint
/// 2. `[writable]` Printing mint - A mint you control that can mint tokens that can be exchanged for limited editions of your
/// master edition via the MintNewEditionFromMasterEditionViaToken endpoint
/// 3. `[writable]` One time authorization printing mint - A mint you control that prints tokens that gives the bearer permission to mint any
/// number of tokens from the printing mint one time via an endpoint with the token-metadata program for your metadata. Also burns the token.
/// 4. `[signer]` Current Update authority key on metadata
/// 5. `[signer]` Printing mint authority - THIS WILL TRANSFER AUTHORITY AWAY FROM THIS KEY.
/// 6. `[signer]` Mint authority on the metadata's mint - THIS WILL TRANSFER AUTHORITY AWAY FROM THIS KEY
/// 7. `[]` Metadata account
/// 8. `[signer]` payer
/// 9. `[]` Token program
/// 10. `[]` System program
/// 11. `[]` Rent info
/// 13. `[signer]` One time authorization printing mint authority - must be provided if using max supply. THIS WILL TRANSFER AUTHORITY AWAY FROM THIS KEY.
CreateMasterEdition(CreateMasterEditionArgs),
/// Given an authority token minted by the Printing mint of a master edition, and a brand new non-metadata-ed mint with one token
/// make a new Metadata + Edition that is a child of the master edition denoted by this authority token.
/// 0. `[writable]` New Metadata key (pda of ['metadata', program id, mint id])
/// 1. `[writable]` New Edition (pda of ['metadata', program id, mint id, 'edition'])
/// 2. `[writable]` Master Record Edition (pda of ['metadata', program id, Printing mint id, 'edition'])
/// 3. `[writable]` Mint of new token - THIS WILL TRANSFER AUTHORITY AWAY FROM THIS KEY
/// 4. `[signer]` Mint authority of new mint
/// 5. `[writable]` Printing Mint of master record edition
/// 6. `[writable]` Token account containing Printing mint token to be transferred
/// 7. `[signer]` Burn authority for this token
/// 8. `[signer]` payer
/// 9. `[signer]` update authority info of master metadata account
/// 10. `[]` Master record metadata account
/// 11. `[]` Token program
/// 12. `[]` System program
/// 13. `[]` Rent info
/// 14. `[optional/writable]` Reservation List - If present, and you are on this list, you can get
/// an edition number given by your position on the list.
MintNewEditionFromMasterEditionViaToken,
/// Allows updating the primary sale boolean on Metadata solely through owning an account
/// containing a token from the metadata's mint and being a signer on this transaction.
/// A sort of limited authority for limited update capability that is required for things like
/// Metaplex to work without needing full authority passing.
///
/// 0. `[writable]` Metadata key (pda of ['metadata', program id, mint id])
/// 1. `[signer]` Owner on the token account
/// 2. `[]` Account containing tokens from the metadata's mint
UpdatePrimarySaleHappenedViaToken,
/// Reserve up to 200 editions in sequence for up to 200 addresses in an existing reservation PDA, which can then be used later by
/// redeemers who have printing tokens as a reservation to get a specific edition number
/// as opposed to whatever one is currently listed on the master edition. Used by Auction Manager
/// to guarantee printing order on bid redemption. AM will call whenever the first person redeems a
/// printing bid to reserve the whole block
/// of winners in order and then each winner when they get their token submits their mint and account
/// with the pda that was created by that first bidder - the token metadata can then cross reference
/// these people with the list and see that bidder A gets edition #2, so on and so forth.
///
/// 0. `[writable]` Master Edition key (pda of ['metadata', program id, mint id, 'edition'])
/// 1. `[writable]` PDA for ReservationList of ['metadata', program id, master edition key, 'reservation', resource-key]
/// 3. `[signer]` The resource you tied the reservation list too
SetReservationList(SetReservationListArgs),
/// Create an empty reservation list for a resource who can come back later as a signer and fill the reservation list
/// with reservations to ensure that people who come to get editions get the number they expect. See SetReservationList for more.
///
/// 0. `[writable]` PDA for ReservationList of ['metadata', program id, master edition key, 'reservation', resource-key]
/// 1. `[signer]` Payer
/// 2. `[signer]` Update authority
/// 3. `[]` Master Edition key (pda of ['metadata', program id, mint id, 'edition'])
/// 4. `[]` A resource you wish to tie the reservation list to. This is so your later visitors who come to
/// redeem can derive your reservation list PDA with something they can easily get at. You choose what this should be.
/// 5. `[]` Metadata key (pda of ['metadata', program id, mint id])
/// 6. `[]` System program
/// 7. `[]` Rent info
CreateReservationList,
// Sign a piece of metadata that has you as an unverified creator so that it is now verified.
//
/// 0. `[writable]` Metadata (pda of ['metadata', program id, mint id])
/// 1. `[signer]` Creator
SignMetadata,
/// Using a one time authorization token from a master edition, print any number of printing tokens from the printing_mint
/// one time, burning the one time authorization token.
///
/// 0. `[writable]` Destination account
/// 1. `[writable]` Token account containing one time authorization token
/// 2. `[writable]` One time authorization mint
/// 3. `[writable]` Printing mint
/// 4. `[signer]` Burn authority
/// 5. `[]` Metadata key (pda of ['metadata', program id, mint id])
/// 6. `[]` Master Edition key (pda of ['metadata', program id, mint id, 'edition'])
/// 7. `[]` Token program
/// 8. `[]` Rent
MintPrintingTokensViaToken(MintPrintingTokensViaTokenArgs),
/// Using your update authority, mint printing tokens for your master edition.
///
/// 0. `[writable]` Destination account
/// 1. `[writable]` Printing mint
/// 2. `[signer]` Update authority
/// 3. `[]` Metadata key (pda of ['metadata', program id, mint id])
/// 4. `[]` Master Edition key (pda of ['metadata', program id, mint id, 'edition'])
/// 5. `[]` Token program
/// 6. `[]` Rent
MintPrintingTokens(MintPrintingTokensViaTokenArgs),
}
/// Creates an CreateMetadataAccounts instruction
#[allow(clippy::too_many_arguments)]
pub fn create_metadata_accounts(
program_id: Pubkey,
metadata_account: Pubkey,
mint: Pubkey,
mint_authority: Pubkey,
payer: Pubkey,
update_authority: Pubkey,
name: String,
symbol: String,
uri: String,
creators: Option<Vec<Creator>>,
seller_fee_basis_points: u16,
update_authority_is_signer: bool,
is_mutable: bool,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(metadata_account, false),
AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(mint_authority, true),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(update_authority, update_authority_is_signer),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: MetadataInstruction::CreateMetadataAccount(CreateMetadataAccountArgs {
data: Data {
name,
symbol,
uri,
seller_fee_basis_points,
creators,
},
is_mutable,
})
.try_to_vec()
.unwrap(),
}
}
/// update metadata account instruction
pub fn update_metadata_accounts(
program_id: Pubkey,
metadata_account: Pubkey,
update_authority: Pubkey,
new_update_authority: Option<Pubkey>,
data: Option<Data>,
primary_sale_happened: Option<bool>,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(metadata_account, false),
AccountMeta::new_readonly(update_authority, true),
],
data: MetadataInstruction::UpdateMetadataAccount(UpdateMetadataAccountArgs {
data,
update_authority: new_update_authority,
primary_sale_happened,
})
.try_to_vec()
.unwrap(),
}
}
/// creates a create_master_edition instruction
#[allow(clippy::too_many_arguments)]
pub fn create_master_edition(
program_id: Pubkey,
edition: Pubkey,
mint: Pubkey,
printing_mint: Pubkey,
one_time_printing_authorization_mint: Pubkey,
update_authority: Pubkey,
printing_mint_authority: Pubkey,
mint_authority: Pubkey,
metadata: Pubkey,
payer: Pubkey,
max_supply: Option<u64>,
one_time_printing_authorization_mint_authority: Option<Pubkey>,
) -> Instruction {
let mut accounts = vec![
AccountMeta::new(edition, false),
AccountMeta::new(mint, false),
AccountMeta::new(printing_mint, false),
AccountMeta::new(one_time_printing_authorization_mint, false),
AccountMeta::new_readonly(update_authority, true),
AccountMeta::new_readonly(printing_mint_authority, true),
AccountMeta::new_readonly(mint_authority, true),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(payer, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
if let Some(auth) = one_time_printing_authorization_mint_authority {
accounts.push(AccountMeta::new_readonly(auth, true));
}
Instruction {
program_id,
accounts,
data: MetadataInstruction::CreateMasterEdition(CreateMasterEditionArgs { max_supply })
.try_to_vec()
.unwrap(),
}
}
/// creates a mint_new_edition_from_master_edition instruction
#[allow(clippy::too_many_arguments)]
pub fn mint_new_edition_from_master_edition_via_token(
program_id: Pubkey,
metadata: Pubkey,
edition: Pubkey,
master_edition: Pubkey,
mint: Pubkey,
mint_authority: Pubkey,
printing_mint: Pubkey,
master_token_account: Pubkey,
burn_authority: Pubkey,
payer: Pubkey,
master_update_authority: Pubkey,
master_metadata: Pubkey,
reservation_list: Option<Pubkey>,
) -> Instruction {
let mut accounts = vec![
AccountMeta::new(metadata, false),
AccountMeta::new(edition, false),
AccountMeta::new(master_edition, false),
AccountMeta::new(mint, false),
AccountMeta::new_readonly(mint_authority, true),
AccountMeta::new(printing_mint, false),
AccountMeta::new(master_token_account, false),
AccountMeta::new_readonly(burn_authority, true),
AccountMeta::new(payer, true),
AccountMeta::new_readonly(master_update_authority, true),
AccountMeta::new_readonly(master_metadata, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
if let Some(list) = reservation_list {
accounts.push(AccountMeta::new_readonly(list, false))
}
Instruction {
program_id,
accounts,
data: MetadataInstruction::MintNewEditionFromMasterEditionViaToken
.try_to_vec()
.unwrap(),
}
}
/// creates a update_primary_sale_happened_via_token instruction
#[allow(clippy::too_many_arguments)]
pub fn update_primary_sale_happened_via_token(
program_id: Pubkey,
metadata: Pubkey,
owner: Pubkey,
token: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(owner, true),
AccountMeta::new_readonly(token, false),
],
data: MetadataInstruction::UpdatePrimarySaleHappenedViaToken
.try_to_vec()
.unwrap(),
}
}
/// creates an set_reservation_list instruction
#[allow(clippy::too_many_arguments)]
pub fn set_reservation_list(
program_id: Pubkey,
master_edition: Pubkey,
reservation_list: Pubkey,
resource: Pubkey,
reservations: Vec<Reservation>,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(master_edition, false),
AccountMeta::new(reservation_list, false),
AccountMeta::new_readonly(resource, true),
],
data: MetadataInstruction::SetReservationList(SetReservationListArgs { reservations })
.try_to_vec()
.unwrap(),
}
}
/// creates an create_reservation_list instruction
#[allow(clippy::too_many_arguments)]
pub fn create_reservation_list(
program_id: Pubkey,
reservation_list: Pubkey,
payer: Pubkey,
update_authority: Pubkey,
master_edition: Pubkey,
resource: Pubkey,
metadata: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(reservation_list, false),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(update_authority, true),
AccountMeta::new_readonly(master_edition, false),
AccountMeta::new_readonly(resource, false),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: MetadataInstruction::CreateReservationList
.try_to_vec()
.unwrap(),
}
}
/// creates an mint_printing_tokens_via_token instruction
#[allow(clippy::too_many_arguments)]
pub fn mint_printing_tokens_via_token(
program_id: Pubkey,
destination: Pubkey,
token: Pubkey,
one_time_printing_authorization_mint: Pubkey,
printing_mint: Pubkey,
burn_authority: Pubkey,
metadata: Pubkey,
master_edition: Pubkey,
supply: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(destination, false),
AccountMeta::new(token, false),
AccountMeta::new(one_time_printing_authorization_mint, false),
AccountMeta::new(printing_mint, false),
AccountMeta::new_readonly(burn_authority, true),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(master_edition, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: MetadataInstruction::MintPrintingTokensViaToken(MintPrintingTokensViaTokenArgs {
supply,
})
.try_to_vec()
.unwrap(),
}
}
/// creates an mint_printing_tokens instruction
#[allow(clippy::too_many_arguments)]
pub fn mint_printing_tokens(
program_id: Pubkey,
destination: Pubkey,
printing_mint: Pubkey,
update_authority: Pubkey,
metadata: Pubkey,
master_edition: Pubkey,
supply: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(destination, false),
AccountMeta::new(printing_mint, false),
AccountMeta::new_readonly(update_authority, true),
AccountMeta::new_readonly(metadata, false),
AccountMeta::new_readonly(master_edition, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: MetadataInstruction::MintPrintingTokens(MintPrintingTokensViaTokenArgs { supply })
.try_to_vec()
.unwrap(),
}
}

View File

@ -0,0 +1,12 @@
//! A Token Metadata program for the Solana blockchain.
pub mod entrypoint;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
pub mod utils;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");

View File

@ -0,0 +1,817 @@
use {
crate::{
error::MetadataError,
instruction::MetadataInstruction,
state::{
get_reservation_list, Data, Key, MasterEdition, Metadata, Reservation,
ReservationListV2, EDITION, MAX_MASTER_EDITION_LEN, MAX_METADATA_LEN, MAX_RESERVATIONS,
MAX_RESERVATION_LIST_SIZE, PREFIX, RESERVATION,
},
utils::{
assert_data_valid, assert_derivation, assert_initialized,
assert_mint_authority_matches_mint, assert_owned_by, assert_rent_exempt, assert_signer,
assert_supply_invariance, assert_token_program_matches_package,
assert_update_authority_is_correct, create_or_allocate_account_raw,
mint_limited_edition, spl_token_burn, spl_token_mint_to, transfer_mint_authority,
TokenBurnParams, TokenMintToParams,
},
},
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
},
spl_token::state::{Account, Mint},
};
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
let instruction = MetadataInstruction::try_from_slice(input)?;
match instruction {
MetadataInstruction::CreateMetadataAccount(args) => {
msg!("Instruction: Create Metadata Accounts");
process_create_metadata_accounts(
program_id,
accounts,
args.data,
false,
args.is_mutable,
)
}
MetadataInstruction::UpdateMetadataAccount(args) => {
msg!("Instruction: Update Metadata Accounts");
process_update_metadata_accounts(
program_id,
accounts,
args.data,
args.update_authority,
args.primary_sale_happened,
)
}
MetadataInstruction::CreateMasterEdition(args) => {
msg!("Instruction: Create Master Edition");
process_create_master_edition(program_id, accounts, args.max_supply)
}
MetadataInstruction::MintNewEditionFromMasterEditionViaToken => {
msg!("Instruction: Mint New Edition from Master Edition Via Token");
process_mint_new_edition_from_master_edition_via_token(program_id, accounts)
}
MetadataInstruction::UpdatePrimarySaleHappenedViaToken => {
msg!("Instruction: Update primary sale via token");
process_update_primary_sale_happened_via_token(program_id, accounts)
}
MetadataInstruction::SetReservationList(args) => {
msg!("Instruction: Set Reservation List");
process_set_reservation_list(program_id, accounts, args.reservations)
}
MetadataInstruction::CreateReservationList => {
msg!("Instruction: Create Reservation List");
process_create_reservation_list(program_id, accounts)
}
MetadataInstruction::SignMetadata => {
msg!("Instruction: Sign Metadata");
process_sign_metadata(program_id, accounts)
}
MetadataInstruction::MintPrintingTokensViaToken(args) => {
msg!("Instruction: Mint Printing Tokens Via Token");
process_mint_printing_tokens_via_token(program_id, accounts, args.supply)
}
MetadataInstruction::MintPrintingTokens(args) => {
msg!("Instruction: Mint Printing Tokens");
process_mint_printing_tokens(program_id, accounts, args.supply)
}
}
}
/// Create a new account instruction
pub fn process_create_metadata_accounts(
program_id: &Pubkey,
accounts: &[AccountInfo],
data: Data,
allow_direct_creator_writes: bool,
is_mutable: bool,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let metadata_account_info = next_account_info(account_info_iter)?;
let mint_info = next_account_info(account_info_iter)?;
let mint_authority_info = next_account_info(account_info_iter)?;
let payer_account_info = next_account_info(account_info_iter)?;
let update_authority_info = next_account_info(account_info_iter)?;
let system_account_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let mint: Mint = assert_initialized(mint_info)?;
assert_mint_authority_matches_mint(&mint, mint_authority_info)?;
assert_owned_by(mint_info, &spl_token::id())?;
let metadata_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
mint_info.key.as_ref(),
];
let (metadata_key, metadata_bump_seed) =
Pubkey::find_program_address(metadata_seeds, program_id);
let metadata_authority_signer_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
mint_info.key.as_ref(),
&[metadata_bump_seed],
];
if metadata_account_info.key != &metadata_key {
return Err(MetadataError::InvalidMetadataKey.into());
}
create_or_allocate_account_raw(
*program_id,
metadata_account_info,
rent_info,
system_account_info,
payer_account_info,
MAX_METADATA_LEN,
metadata_authority_signer_seeds,
)?;
let mut metadata = Metadata::from_account_info(metadata_account_info)?;
assert_data_valid(
&data,
update_authority_info.key,
&metadata,
allow_direct_creator_writes,
)?;
metadata.mint = *mint_info.key;
metadata.key = Key::MetadataV1;
metadata.data = data;
metadata.is_mutable = is_mutable;
metadata.update_authority = *update_authority_info.key;
metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?;
Ok(())
}
/// Update existing account instruction
pub fn process_update_metadata_accounts(
program_id: &Pubkey,
accounts: &[AccountInfo],
optional_data: Option<Data>,
update_authority: Option<Pubkey>,
primary_sale_happened: Option<bool>,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let metadata_account_info = next_account_info(account_info_iter)?;
let update_authority_info = next_account_info(account_info_iter)?;
let mut metadata = Metadata::from_account_info(metadata_account_info)?;
assert_owned_by(metadata_account_info, program_id)?;
assert_update_authority_is_correct(&metadata, update_authority_info)?;
if let Some(data) = optional_data {
if metadata.is_mutable {
assert_data_valid(&data, update_authority_info.key, &metadata, false)?;
metadata.data = data;
} else {
return Err(MetadataError::DataIsImmutable.into());
}
}
if let Some(val) = update_authority {
metadata.update_authority = val;
}
if let Some(val) = primary_sale_happened {
if val {
metadata.primary_sale_happened = val
} else {
return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into());
}
}
metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?;
Ok(())
}
/// Create master edition
pub fn process_create_master_edition(
program_id: &Pubkey,
accounts: &[AccountInfo],
max_supply: Option<u64>,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let edition_account_info = next_account_info(account_info_iter)?;
let mint_info = next_account_info(account_info_iter)?;
let printing_mint_info = next_account_info(account_info_iter)?;
let one_time_printing_authorization_mint_info = next_account_info(account_info_iter)?;
let update_authority_info = next_account_info(account_info_iter)?;
let printing_mint_authority_info = next_account_info(account_info_iter)?;
let mint_authority_info = next_account_info(account_info_iter)?;
let metadata_account_info = next_account_info(account_info_iter)?;
let payer_account_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let system_account_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let metadata = Metadata::from_account_info(metadata_account_info)?;
let mint: Mint = assert_initialized(mint_info)?;
let printing_mint: Mint = assert_initialized(printing_mint_info)?;
let one_time_printing_authorization_mint: Mint =
assert_initialized(one_time_printing_authorization_mint_info)?;
let bump_seed = assert_derivation(
program_id,
edition_account_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
&mint_info.key.as_ref(),
EDITION.as_bytes(),
],
)?;
assert_token_program_matches_package(token_program_info)?;
assert_mint_authority_matches_mint(&mint, mint_authority_info)?;
assert_mint_authority_matches_mint(&printing_mint, printing_mint_authority_info)?;
assert_mint_authority_matches_mint(&one_time_printing_authorization_mint, mint_authority_info)?;
assert_owned_by(metadata_account_info, program_id)?;
assert_owned_by(mint_info, &spl_token::id())?;
assert_owned_by(printing_mint_info, &spl_token::id())?;
assert_owned_by(one_time_printing_authorization_mint_info, &spl_token::id())?;
if metadata.mint != *mint_info.key {
return Err(MetadataError::MintMismatch.into());
}
if printing_mint.decimals != 0 {
return Err(MetadataError::PrintingMintDecimalsShouldBeZero.into());
}
if one_time_printing_authorization_mint.decimals != 0 {
return Err(MetadataError::OneTimePrintingAuthorizationMintDecimalsShouldBeZero.into());
}
if mint.decimals != 0 {
return Err(MetadataError::EditionMintDecimalsShouldBeZero.into());
}
assert_update_authority_is_correct(&metadata, update_authority_info)?;
if mint.supply != 1 {
return Err(MetadataError::EditionsMustHaveExactlyOneToken.into());
}
let edition_authority_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
&mint_info.key.as_ref(),
EDITION.as_bytes(),
&[bump_seed],
];
create_or_allocate_account_raw(
*program_id,
edition_account_info,
rent_info,
system_account_info,
payer_account_info,
MAX_MASTER_EDITION_LEN,
edition_authority_seeds,
)?;
let mut edition = MasterEdition::from_account_info(edition_account_info)?;
edition.key = Key::MasterEditionV1;
edition.supply = 0;
edition.max_supply = max_supply;
edition.printing_mint = *printing_mint_info.key;
edition.one_time_printing_authorization_mint = *one_time_printing_authorization_mint_info.key;
edition.serialize(&mut *edition_account_info.data.borrow_mut())?;
// While you can't mint any more of your master record, you can
// mint as many limited editions as you like, and coins to permission others
// to mint one of them in the future.
transfer_mint_authority(
edition_account_info.key,
edition_account_info,
mint_info,
mint_authority_info,
token_program_info,
)?;
// The program needs to own the printing mint to be able to print tokens via the one time printing auth
// you can get tokens out of it as update authority via another call.
transfer_mint_authority(
edition_account_info.key,
edition_account_info,
printing_mint_info,
printing_mint_authority_info,
token_program_info,
)?;
if max_supply.is_some() {
// We need to enact limited supply protocol, take away one time printing too.
let one_time_printing_authorization_mint_authority_info =
next_account_info(account_info_iter)?;
transfer_mint_authority(
&edition_account_info.key,
edition_account_info,
one_time_printing_authorization_mint_info,
one_time_printing_authorization_mint_authority_info,
token_program_info,
)?;
}
Ok(())
}
pub fn process_mint_new_edition_from_master_edition_via_token(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let new_metadata_account_info = next_account_info(account_info_iter)?;
let new_edition_account_info = next_account_info(account_info_iter)?;
let master_edition_account_info = next_account_info(account_info_iter)?;
let mint_info = next_account_info(account_info_iter)?;
let mint_authority_info = next_account_info(account_info_iter)?;
let printing_mint_info = next_account_info(account_info_iter)?;
let master_token_account_info = next_account_info(account_info_iter)?;
let burn_authority = next_account_info(account_info_iter)?;
let payer_account_info = next_account_info(account_info_iter)?;
let update_authority_info = next_account_info(account_info_iter)?;
let master_metadata_account_info = next_account_info(account_info_iter)?;
let token_program_account_info = next_account_info(account_info_iter)?;
let system_account_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let reservation_list_info = match next_account_info(account_info_iter) {
Ok(account) => Some(account),
Err(_) => None,
};
assert_token_program_matches_package(token_program_account_info)?;
assert_owned_by(mint_info, &spl_token::id())?;
assert_owned_by(printing_mint_info, &spl_token::id())?;
assert_owned_by(master_token_account_info, &spl_token::id())?;
if !new_metadata_account_info.data_is_empty() {
return Err(MetadataError::AlreadyInitialized.into());
}
if !new_edition_account_info.data_is_empty() {
return Err(MetadataError::AlreadyInitialized.into());
}
assert_owned_by(master_edition_account_info, program_id)?;
assert_owned_by(master_metadata_account_info, program_id)?;
if let Some(acct) = reservation_list_info {
assert_owned_by(acct, program_id)?;
}
let token_account: Account = assert_initialized(master_token_account_info)?;
let master_edition = MasterEdition::from_account_info(master_edition_account_info)?;
if master_edition.printing_mint != *printing_mint_info.key {
return Err(MetadataError::PrintingMintMismatch.into());
}
if token_account.mint != *printing_mint_info.key {
return Err(MetadataError::TokenAccountMintMismatch.into());
}
if token_account.amount < 1 {
return Err(MetadataError::NotEnoughTokens.into());
}
spl_token_burn(TokenBurnParams {
mint: printing_mint_info.clone(),
source: master_token_account_info.clone(),
amount: 1,
authority: burn_authority.clone(),
authority_signer_seeds: None,
token_program: token_program_account_info.clone(),
})?;
mint_limited_edition(
program_id,
new_metadata_account_info,
new_edition_account_info,
master_edition_account_info,
mint_info,
mint_authority_info,
payer_account_info,
update_authority_info,
master_metadata_account_info,
token_program_account_info,
system_account_info,
rent_info,
reservation_list_info,
)?;
Ok(())
}
pub fn process_update_primary_sale_happened_via_token(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let metadata_account_info = next_account_info(account_info_iter)?;
let owner_info = next_account_info(account_info_iter)?;
let token_account_info = next_account_info(account_info_iter)?;
let token_account: Account = assert_initialized(token_account_info)?;
let mut metadata = Metadata::from_account_info(metadata_account_info)?;
assert_owned_by(metadata_account_info, program_id)?;
assert_owned_by(token_account_info, &spl_token::id())?;
if !owner_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if token_account.owner != *owner_info.key {
return Err(MetadataError::OwnerMismatch.into());
}
if token_account.amount == 0 {
return Err(MetadataError::NoBalanceInAccountForAuthorization.into());
}
if token_account.mint != metadata.mint {
return Err(MetadataError::MintMismatch.into());
}
metadata.primary_sale_happened = true;
metadata.serialize(&mut *metadata_account_info.data.borrow_mut())?;
Ok(())
}
pub fn process_create_reservation_list(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let reservation_list_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let update_authority_info = next_account_info(account_info_iter)?;
let master_edition_info = next_account_info(account_info_iter)?;
let resource_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
assert_owned_by(master_edition_info, program_id)?;
assert_owned_by(metadata_info, program_id)?;
let metadata = Metadata::from_account_info(metadata_info)?;
assert_update_authority_is_correct(&metadata, update_authority_info)?;
if !reservation_list_info.data_is_empty() {
return Err(MetadataError::ReservationExists.into());
}
let bump = assert_derivation(
program_id,
reservation_list_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
master_edition_info.key.as_ref(),
RESERVATION.as_bytes(),
resource_info.key.as_ref(),
],
)?;
assert_derivation(
program_id,
master_edition_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
metadata.mint.as_ref(),
EDITION.as_bytes(),
],
)?;
let seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
master_edition_info.key.as_ref(),
RESERVATION.as_bytes(),
resource_info.key.as_ref(),
&[bump],
];
create_or_allocate_account_raw(
*program_id,
reservation_list_info,
rent_info,
system_program_info,
payer_info,
MAX_RESERVATION_LIST_SIZE,
seeds,
)?;
let mut reservation = ReservationListV2::from_account_info(reservation_list_info)?;
reservation.key = Key::ReservationListV2;
reservation.master_edition = *master_edition_info.key;
reservation.supply_snapshot = None;
reservation.reservations = vec![];
reservation.serialize(&mut *reservation_list_info.data.borrow_mut())?;
Ok(())
}
pub fn process_set_reservation_list(
program_id: &Pubkey,
accounts: &[AccountInfo],
reservations: Vec<Reservation>,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let master_edition_info = next_account_info(account_info_iter)?;
let reservation_list_info = next_account_info(account_info_iter)?;
let resource_info = next_account_info(account_info_iter)?;
assert_signer(resource_info)?;
assert_owned_by(master_edition_info, program_id)?;
assert_owned_by(reservation_list_info, program_id)?;
let mut master_edition = MasterEdition::from_account_info(master_edition_info)?;
if reservation_list_info.data_is_empty() {
return Err(MetadataError::ReservationDoesNotExist.into());
}
if reservations.len() > MAX_RESERVATIONS {
return Err(MetadataError::BeyondMaxAddressSize.into());
}
assert_derivation(
program_id,
reservation_list_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
master_edition_info.key.as_ref(),
RESERVATION.as_bytes(),
resource_info.key.as_ref(),
],
)?;
let mut reservation_list = get_reservation_list(reservation_list_info)?;
if reservation_list.supply_snapshot().is_some() {
return Err(MetadataError::ReservationAlreadyMade.into());
}
let mut total_len: u64 = 0;
let mut total_len_check: u64 = 0;
for reservation in &reservations {
total_len = total_len
.checked_add(reservation.spots_remaining)
.ok_or(MetadataError::NumericalOverflowError)?;
total_len_check = total_len_check
.checked_add(reservation.total_spots)
.ok_or(MetadataError::NumericalOverflowError)?;
if reservation.spots_remaining != reservation.total_spots {
return Err(
MetadataError::ReservationSpotsRemainingShouldMatchTotalSpotsAtStart.into(),
);
}
}
if total_len_check != total_len {
return Err(MetadataError::SpotMismatch.into());
}
reservation_list.set_supply_snapshot(Some(master_edition.supply));
reservation_list.set_reservations(reservations);
msg!("Master edition {:?}", master_edition);
msg!("Total new spots {:?}", total_len);
master_edition.supply = master_edition
.supply
.checked_add(total_len as u64)
.ok_or(MetadataError::NumericalOverflowError)?;
if let Some(max_supply) = master_edition.max_supply {
if master_edition.supply > max_supply {
return Err(MetadataError::ReservationBreachesMaximumSupply.into());
}
}
reservation_list.save(reservation_list_info)?;
master_edition.serialize(&mut *master_edition_info.data.borrow_mut())?;
Ok(())
}
pub fn process_sign_metadata(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let metadata_info = next_account_info(account_info_iter)?;
let creator_info = next_account_info(account_info_iter)?;
assert_signer(creator_info)?;
assert_owned_by(metadata_info, program_id)?;
let mut metadata = Metadata::from_account_info(metadata_info)?;
if let Some(creators) = &mut metadata.data.creators {
let mut found = false;
for creator in creators {
if creator.address == *creator_info.key {
creator.verified = true;
found = true;
break;
}
}
if !found {
return Err(MetadataError::CreatorNotFound.into());
}
} else {
return Err(MetadataError::NoCreatorsPresentOnMetadata.into());
}
metadata.serialize(&mut *metadata_info.data.borrow_mut())?;
Ok(())
}
pub fn process_mint_printing_tokens_via_token(
program_id: &Pubkey,
accounts: &[AccountInfo],
supply: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let destination_info = next_account_info(account_info_iter)?;
let one_time_token_account_info = next_account_info(account_info_iter)?;
let one_time_printing_authorization_mint_info = next_account_info(account_info_iter)?;
let printing_mint_info = next_account_info(account_info_iter)?;
let burn_authority_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let master_edition_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(rent_info)?;
let destination: Account = assert_initialized(destination_info)?;
let one_time_token_account: Account = assert_initialized(one_time_token_account_info)?;
let master_edition = MasterEdition::from_account_info(master_edition_info)?;
let metadata = Metadata::from_account_info(metadata_info)?;
let printing_mint: Mint = assert_initialized(printing_mint_info)?;
assert_supply_invariance(&master_edition, &printing_mint, supply)?;
assert_token_program_matches_package(token_program_info)?;
assert_rent_exempt(rent, destination_info)?;
assert_owned_by(destination_info, &spl_token::id())?;
assert_owned_by(one_time_token_account_info, &spl_token::id())?;
assert_owned_by(one_time_printing_authorization_mint_info, &spl_token::id())?;
assert_owned_by(printing_mint_info, &spl_token::id())?;
assert_owned_by(metadata_info, program_id)?;
assert_owned_by(master_edition_info, program_id)?;
if destination.mint != master_edition.printing_mint {
return Err(MetadataError::DestinationMintMismatch.into());
}
if one_time_token_account.mint != master_edition.one_time_printing_authorization_mint {
return Err(MetadataError::TokenAccountOneTimeAuthMintMismatch.into());
}
if one_time_token_account.amount == 0 {
return Err(MetadataError::NoBalanceInAccountForAuthorization.into());
}
if *printing_mint_info.key != master_edition.printing_mint {
return Err(MetadataError::PrintingMintMismatch.into());
}
if *one_time_printing_authorization_mint_info.key
!= master_edition.one_time_printing_authorization_mint
{
return Err(MetadataError::OneTimePrintingAuthMintMismatch.into());
}
spl_token_burn(TokenBurnParams {
mint: one_time_printing_authorization_mint_info.clone(),
source: one_time_token_account_info.clone(),
amount: 1,
authority: burn_authority_info.clone(),
authority_signer_seeds: None,
token_program: token_program_info.clone(),
})?;
let bump = assert_derivation(
program_id,
master_edition_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
metadata.mint.as_ref(),
EDITION.as_bytes(),
],
)?;
let authority_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
metadata.mint.as_ref(),
EDITION.as_bytes(),
&[bump],
];
spl_token_mint_to(TokenMintToParams {
mint: printing_mint_info.clone(),
destination: destination_info.clone(),
amount: supply,
authority: master_edition_info.clone(),
authority_signer_seeds: Some(authority_seeds),
token_program: token_program_info.clone(),
})?;
Ok(())
}
pub fn process_mint_printing_tokens(
program_id: &Pubkey,
accounts: &[AccountInfo],
supply: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let destination_info = next_account_info(account_info_iter)?;
let printing_mint_info = next_account_info(account_info_iter)?;
let update_authority_info = next_account_info(account_info_iter)?;
let metadata_info = next_account_info(account_info_iter)?;
let master_edition_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(rent_info)?;
let destination: Account = assert_initialized(destination_info)?;
let master_edition = MasterEdition::from_account_info(master_edition_info)?;
let metadata = Metadata::from_account_info(metadata_info)?;
let printing_mint: Mint = assert_initialized(printing_mint_info)?;
assert_token_program_matches_package(token_program_info)?;
assert_rent_exempt(rent, destination_info)?;
assert_owned_by(destination_info, &spl_token::id())?;
assert_update_authority_is_correct(&metadata, update_authority_info)?;
assert_supply_invariance(&master_edition, &printing_mint, supply)?;
assert_owned_by(printing_mint_info, &spl_token::id())?;
assert_owned_by(metadata_info, program_id)?;
assert_owned_by(master_edition_info, program_id)?;
let bump = assert_derivation(
program_id,
master_edition_info,
&[
PREFIX.as_bytes(),
program_id.as_ref(),
metadata.mint.as_ref(),
EDITION.as_bytes(),
],
)?;
let authority_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
metadata.mint.as_ref(),
EDITION.as_bytes(),
&[bump],
];
if destination.mint != master_edition.printing_mint {
return Err(MetadataError::DestinationMintMismatch.into());
}
if *printing_mint_info.key != master_edition.printing_mint {
return Err(MetadataError::PrintingMintMismatch.into());
}
spl_token_mint_to(TokenMintToParams {
mint: printing_mint_info.clone(),
destination: destination_info.clone(),
amount: supply,
authority: master_edition_info.clone(),
authority_signer_seeds: Some(authority_seeds),
token_program: token_program_info.clone(),
})?;
Ok(())
}

View File

@ -0,0 +1,331 @@
use {
crate::{error::MetadataError, utils::try_from_slice_checked},
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
pubkey::Pubkey,
},
};
/// prefix used for PDAs to avoid certain collision attacks (https://en.wikipedia.org/wiki/Collision_attack#Chosen-prefix_collision_attack)
pub const PREFIX: &str = "metadata";
/// Used in seeds to make Edition model pda address
pub const EDITION: &str = "edition";
pub const RESERVATION: &str = "reservation";
pub const MAX_NAME_LENGTH: usize = 32;
pub const MAX_SYMBOL_LENGTH: usize = 10;
pub const MAX_URI_LENGTH: usize = 200;
pub const MAX_METADATA_LEN: usize = 1
+ 32
+ 32
+ MAX_NAME_LENGTH
+ MAX_SYMBOL_LENGTH
+ MAX_URI_LENGTH
+ MAX_CREATOR_LIMIT * MAX_CREATOR_LEN
+ 2
+ 1
+ 1
+ 198;
pub const MAX_EDITION_LEN: usize = 1 + 32 + 8 + 200;
pub const MAX_MASTER_EDITION_LEN: usize = 1 + 9 + 8 + 32 + 32 + 200;
pub const MAX_CREATOR_LIMIT: usize = 5;
pub const MAX_CREATOR_LEN: usize = 32 + 1 + 1;
pub const MAX_RESERVATIONS: usize = 200;
// can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec
pub const MAX_RESERVATION_LIST_V1_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 34 + 100;
// can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec
pub const MAX_RESERVATION_LIST_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 48 + 100;
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub enum Key {
Uninitialized,
EditionV1,
MasterEditionV1,
ReservationListV1,
MetadataV1,
ReservationListV2,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct Data {
/// The name of the asset
pub name: String,
/// The symbol for the asset
pub symbol: String,
/// URI pointing to JSON representing the asset
pub uri: String,
/// Royalty basis points that goes to creators in secondary sales (0-10000)
pub seller_fee_basis_points: u16,
/// Array of creators, optional
pub creators: Option<Vec<Creator>>,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)]
pub struct Metadata {
pub key: Key,
pub update_authority: Pubkey,
pub mint: Pubkey,
pub data: Data,
// Immutable, once flipped, all sales of this metadata are considered secondary.
pub primary_sale_happened: bool,
// Whether or not the data struct is mutable, default is not
pub is_mutable: bool,
}
impl Metadata {
pub fn from_account_info(a: &AccountInfo) -> Result<Metadata, ProgramError> {
let md: Metadata =
try_from_slice_checked(&a.data.borrow_mut(), Key::MetadataV1, MAX_METADATA_LEN)?;
Ok(md)
}
}
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
pub struct MasterEdition {
pub key: Key,
pub supply: u64,
pub max_supply: Option<u64>,
/// Can be used to mint tokens that give one-time permission to mint a single limited edition.
pub printing_mint: Pubkey,
/// If you don't know how many printing tokens you are going to need, but you do know
/// you are going to need some amount in the future, you can use a token from this mint.
/// Coming back to token metadata with one of these tokens allows you to mint (one time)
/// any number of printing tokens you want. This is used for instance by Auction Manager
/// with participation NFTs, where we dont know how many people will bid and need participation
/// printing tokens to redeem, so we give it ONE of these tokens to use after the auction is over,
/// because when the auction begins we just dont know how many printing tokens we will need,
/// but at the end we will. At the end it then burns this token with token-metadata to
/// get the printing tokens it needs to give to bidders. Each bidder then redeems a printing token
/// to get their limited editions.
pub one_time_printing_authorization_mint: Pubkey,
}
impl MasterEdition {
pub fn from_account_info(a: &AccountInfo) -> Result<MasterEdition, ProgramError> {
let me: MasterEdition = try_from_slice_checked(
&a.data.borrow_mut(),
Key::MasterEditionV1,
MAX_MASTER_EDITION_LEN,
)?;
Ok(me)
}
}
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
/// All Editions should never have a supply greater than 1.
/// To enforce this, a transfer mint authority instruction will happen when
/// a normal token is turned into an Edition, and in order for a Metadata update authority
/// to do this transaction they will also need to sign the transaction as the Mint authority.
pub struct Edition {
pub key: Key,
/// Points at MasterEdition struct
pub parent: Pubkey,
/// Starting at 0 for master record, this is incremented for each edition minted.
pub edition: u64,
}
impl Edition {
pub fn from_account_info(a: &AccountInfo) -> Result<Edition, ProgramError> {
let ed: Edition =
try_from_slice_checked(&a.data.borrow_mut(), Key::EditionV1, MAX_EDITION_LEN)?;
Ok(ed)
}
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct Creator {
pub address: Pubkey,
pub verified: bool,
// In percentages, NOT basis points ;) Watch out!
pub share: u8,
}
pub trait ReservationList {
fn master_edition(&self) -> Pubkey;
fn supply_snapshot(&self) -> Option<u64>;
fn reservations(&self) -> Vec<Reservation>;
fn set_master_edition(&mut self, key: Pubkey);
fn set_supply_snapshot(&mut self, supply: Option<u64>);
fn set_reservations(&mut self, reservations: Vec<Reservation>);
fn save(&self, account: &AccountInfo) -> ProgramResult;
}
pub fn get_reservation_list(
account: &AccountInfo,
) -> Result<Box<dyn ReservationList>, ProgramError> {
let version = account.data.borrow()[0];
// For some reason when converting Key to u8 here, it becomes unreachable. Use direct constant instead.
match version {
3 => return Ok(Box::new(ReservationListV1::from_account_info(account)?)),
5 => return Ok(Box::new(ReservationListV2::from_account_info(account)?)),
_ => return Err(MetadataError::DataTypeMismatch.into()),
};
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct ReservationListV2 {
pub key: Key,
/// Present for reverse lookups
pub master_edition: Pubkey,
/// What supply counter was on master_edition when this reservation was created.
pub supply_snapshot: Option<u64>,
pub reservations: Vec<Reservation>,
}
impl ReservationList for ReservationListV2 {
fn master_edition(&self) -> Pubkey {
self.master_edition
}
fn supply_snapshot(&self) -> Option<u64> {
self.supply_snapshot
}
fn reservations(&self) -> Vec<Reservation> {
self.reservations.clone()
}
fn set_master_edition(&mut self, key: Pubkey) {
self.master_edition = key
}
fn set_supply_snapshot(&mut self, supply: Option<u64>) {
self.supply_snapshot = supply;
}
fn set_reservations(&mut self, reservations: Vec<Reservation>) {
self.reservations = reservations
}
fn save(&self, account: &AccountInfo) -> ProgramResult {
self.serialize(&mut *account.data.borrow_mut())?;
Ok(())
}
}
impl ReservationListV2 {
pub fn from_account_info(a: &AccountInfo) -> Result<ReservationListV2, ProgramError> {
let res: ReservationListV2 = try_from_slice_checked(
&a.data.borrow_mut(),
Key::ReservationListV2,
MAX_RESERVATION_LIST_SIZE,
)?;
Ok(res)
}
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct Reservation {
pub address: Pubkey,
pub spots_remaining: u64,
pub total_spots: u64,
}
// Legacy Reservation List with u8s
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct ReservationListV1 {
pub key: Key,
/// Present for reverse lookups
pub master_edition: Pubkey,
/// What supply counter was on master_edition when this reservation was created.
pub supply_snapshot: Option<u64>,
pub reservations: Vec<ReservationV1>,
}
impl ReservationList for ReservationListV1 {
fn master_edition(&self) -> Pubkey {
self.master_edition
}
fn supply_snapshot(&self) -> Option<u64> {
self.supply_snapshot
}
fn reservations(&self) -> Vec<Reservation> {
self.reservations
.iter()
.map(|r| Reservation {
address: r.address,
spots_remaining: r.spots_remaining as u64,
total_spots: r.total_spots as u64,
})
.collect()
}
fn set_master_edition(&mut self, key: Pubkey) {
self.master_edition = key
}
fn set_supply_snapshot(&mut self, supply: Option<u64>) {
self.supply_snapshot = supply;
}
fn set_reservations(&mut self, reservations: Vec<Reservation>) {
self.reservations = reservations
.iter()
.map(|r| ReservationV1 {
address: r.address,
spots_remaining: r.spots_remaining as u8,
total_spots: r.total_spots as u8,
})
.collect();
}
fn save(&self, account: &AccountInfo) -> ProgramResult {
self.serialize(&mut *account.data.borrow_mut())?;
Ok(())
}
}
impl ReservationListV1 {
pub fn from_account_info(a: &AccountInfo) -> Result<ReservationListV1, ProgramError> {
let res: ReservationListV1 = try_from_slice_checked(
&a.data.borrow_mut(),
Key::ReservationListV1,
MAX_RESERVATION_LIST_V1_SIZE,
)?;
Ok(res)
}
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct ReservationV1 {
pub address: Pubkey,
pub spots_remaining: u8,
pub total_spots: u8,
}

View File

@ -0,0 +1,615 @@
use {
crate::{
error::MetadataError,
processor::process_create_metadata_accounts,
state::{
get_reservation_list, Data, Edition, Key, MasterEdition, Metadata, EDITION,
MAX_CREATOR_LIMIT, MAX_EDITION_LEN, MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH,
PREFIX,
},
},
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::AccountInfo,
borsh::try_from_slice_unchecked,
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
},
spl_token::{
instruction::{set_authority, AuthorityType},
state::Mint,
},
std::convert::TryInto,
};
pub fn assert_data_valid(
data: &Data,
update_authority: &Pubkey,
existing_metadata: &Metadata,
allow_direct_creator_writes: bool,
) -> ProgramResult {
if data.name.len() > MAX_NAME_LENGTH {
return Err(MetadataError::NameTooLong.into());
}
if data.symbol.len() > MAX_SYMBOL_LENGTH {
return Err(MetadataError::SymbolTooLong.into());
}
if data.uri.len() > MAX_URI_LENGTH {
return Err(MetadataError::UriTooLong.into());
}
if data.seller_fee_basis_points > 10000 {
return Err(MetadataError::InvalidBasisPoints.into());
}
if data.creators.is_some() {
if let Some(creators) = &data.creators {
if creators.len() > MAX_CREATOR_LIMIT {
return Err(MetadataError::CreatorsTooLong.into());
}
if creators.is_empty() {
return Err(MetadataError::CreatorsMustBeAtleastOne.into());
} else {
let mut found = false;
let mut total: u8 = 0;
for i in 0..creators.len() {
let creator = &creators[i];
for j in (i + 1)..creators.len() {
if creators[j].address == creator.address {
return Err(MetadataError::DuplicateCreatorAddress.into());
}
}
total = total
.checked_add(creator.share)
.ok_or(MetadataError::NumericalOverflowError)?;
if creator.address == *update_authority {
found = true;
}
// Dont allow metadata owner to unilaterally say a creator verified...
// cross check with array, only let them say verified=true here if
// it already was true and in the array.
// Conversely, dont let a verified creator be wiped.
if creator.address != *update_authority && !allow_direct_creator_writes {
if let Some(existing_creators) = &existing_metadata.data.creators {
match existing_creators
.iter()
.find(|c| c.address == creator.address)
{
Some(existing_creator) => {
if creator.verified && !existing_creator.verified {
return Err(
MetadataError::CannotVerifyAnotherCreator.into()
);
} else if !creator.verified && existing_creator.verified {
return Err(
MetadataError::CannotUnverifyAnotherCreator.into()
);
}
}
None => {
if creator.verified {
return Err(
MetadataError::CannotVerifyAnotherCreator.into()
);
}
}
}
} else {
if creator.verified {
return Err(MetadataError::CannotVerifyAnotherCreator.into());
}
}
}
}
if !found && !allow_direct_creator_writes {
return Err(MetadataError::MustBeOneOfCreators.into());
}
if total != 100 {
return Err(MetadataError::ShareTotalMustBe100.into());
}
}
}
}
Ok(())
}
/// assert initialized account
pub fn assert_initialized<T: Pack + IsInitialized>(
account_info: &AccountInfo,
) -> Result<T, ProgramError> {
let account: T = T::unpack_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(MetadataError::Uninitialized.into())
} else {
Ok(account)
}
}
/// Create account almost from scratch, lifted from
/// https://github.com/solana-labs/solana-program-library/tree/master/associated-token-account/program/src/processor.rs#L51-L98
#[inline(always)]
pub fn create_or_allocate_account_raw<'a>(
program_id: Pubkey,
new_account_info: &AccountInfo<'a>,
rent_sysvar_info: &AccountInfo<'a>,
system_program_info: &AccountInfo<'a>,
payer_info: &AccountInfo<'a>,
size: usize,
signer_seeds: &[&[u8]],
) -> ProgramResult {
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let required_lamports = rent
.minimum_balance(size)
.max(1)
.saturating_sub(new_account_info.lamports());
if required_lamports > 0 {
msg!("Transfer {} lamports to the new account", required_lamports);
invoke(
&system_instruction::transfer(&payer_info.key, new_account_info.key, required_lamports),
&[
payer_info.clone(),
new_account_info.clone(),
system_program_info.clone(),
],
)?;
}
msg!("Allocate space for the account");
invoke_signed(
&system_instruction::allocate(new_account_info.key, size.try_into().unwrap()),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
msg!("Assign the account to the owning program");
invoke_signed(
&system_instruction::assign(new_account_info.key, &program_id),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
Ok(())
}
pub fn assert_update_authority_is_correct(
metadata: &Metadata,
update_authority_info: &AccountInfo,
) -> ProgramResult {
if metadata.update_authority != *update_authority_info.key {
return Err(MetadataError::UpdateAuthorityIncorrect.into());
}
if !update_authority_info.is_signer {
return Err(MetadataError::UpdateAuthorityIsNotSigner.into());
}
Ok(())
}
pub fn assert_mint_authority_matches_mint(
mint: &Mint,
mint_authority_info: &AccountInfo,
) -> ProgramResult {
match mint.mint_authority {
solana_program::program_option::COption::None => {
return Err(MetadataError::InvalidMintAuthority.into());
}
solana_program::program_option::COption::Some(key) => {
if *mint_authority_info.key != key {
return Err(MetadataError::InvalidMintAuthority.into());
}
}
}
if !mint_authority_info.is_signer {
return Err(MetadataError::NotMintAuthority.into());
}
Ok(())
}
pub fn assert_supply_invariance(
master_edition: &MasterEdition,
printing_mint: &Mint,
new_supply: u64,
) -> ProgramResult {
// The supply of printed tokens and the supply of the master edition should, when added, never exceed max supply.
// Every time a printed token is burned, master edition.supply goes up by 1.
if let Some(max_supply) = master_edition.max_supply {
let current_supply = printing_mint
.supply
.checked_add(master_edition.supply)
.ok_or(MetadataError::NumericalOverflowError)?;
let new_proposed_supply = current_supply
.checked_add(new_supply)
.ok_or(MetadataError::NumericalOverflowError)?;
if new_proposed_supply > max_supply {
return Err(MetadataError::PrintingWouldBreachMaximumSupply.into());
}
}
Ok(())
}
pub fn transfer_mint_authority<'a>(
edition_key: &Pubkey,
edition_account_info: &AccountInfo<'a>,
mint_info: &AccountInfo<'a>,
mint_authority_info: &AccountInfo<'a>,
token_program_info: &AccountInfo<'a>,
) -> ProgramResult {
msg!("Setting mint authority");
invoke_signed(
&set_authority(
token_program_info.key,
mint_info.key,
Some(edition_key),
AuthorityType::MintTokens,
mint_authority_info.key,
&[&mint_authority_info.key],
)
.unwrap(),
&[
mint_authority_info.clone(),
mint_info.clone(),
token_program_info.clone(),
edition_account_info.clone(),
],
&[],
)?;
msg!("Setting freeze authority");
invoke_signed(
&set_authority(
token_program_info.key,
mint_info.key,
Some(&edition_key),
AuthorityType::FreezeAccount,
mint_authority_info.key,
&[&mint_authority_info.key],
)
.unwrap(),
&[
mint_authority_info.clone(),
mint_info.clone(),
token_program_info.clone(),
edition_account_info.clone(),
],
&[],
)?;
Ok(())
}
pub fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult {
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
Err(MetadataError::NotRentExempt.into())
} else {
Ok(())
}
}
// Todo deprecate this for assert derivation
pub fn assert_edition_valid(
program_id: &Pubkey,
mint: &Pubkey,
edition_account_info: &AccountInfo,
) -> ProgramResult {
let edition_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
&mint.as_ref(),
EDITION.as_bytes(),
];
let (edition_key, _) = Pubkey::find_program_address(edition_seeds, program_id);
if edition_key != *edition_account_info.key {
return Err(MetadataError::InvalidEditionKey.into());
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn mint_limited_edition<'a>(
program_id: &Pubkey,
new_metadata_account_info: &AccountInfo<'a>,
new_edition_account_info: &AccountInfo<'a>,
master_edition_account_info: &AccountInfo<'a>,
mint_info: &AccountInfo<'a>,
mint_authority_info: &AccountInfo<'a>,
payer_account_info: &AccountInfo<'a>,
update_authority_info: &AccountInfo<'a>,
master_metadata_account_info: &AccountInfo<'a>,
token_program_account_info: &AccountInfo<'a>,
system_account_info: &AccountInfo<'a>,
rent_info: &AccountInfo<'a>,
reservation_list_info: Option<&AccountInfo<'a>>,
) -> ProgramResult {
let master_metadata = Metadata::from_account_info(master_metadata_account_info)?;
let mut master_edition = MasterEdition::from_account_info(master_edition_account_info)?;
let mint: Mint = assert_initialized(mint_info)?;
assert_mint_authority_matches_mint(&mint, mint_authority_info)?;
assert_edition_valid(
program_id,
&master_metadata.mint,
master_edition_account_info,
)?;
let edition_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
&mint_info.key.as_ref(),
EDITION.as_bytes(),
];
let (edition_key, bump_seed) = Pubkey::find_program_address(edition_seeds, program_id);
if edition_key != *new_edition_account_info.key {
return Err(MetadataError::InvalidEditionKey.into());
}
if reservation_list_info.is_none() {
if let Some(max) = master_edition.max_supply {
if master_edition.supply >= max {
return Err(MetadataError::MaxEditionsMintedAlready.into());
}
}
master_edition.supply += 1;
master_edition.serialize(&mut *master_edition_account_info.data.borrow_mut())?;
}
if mint.supply != 1 {
return Err(MetadataError::EditionsMustHaveExactlyOneToken.into());
}
// create the metadata the normal way...
process_create_metadata_accounts(
program_id,
&[
new_metadata_account_info.clone(),
mint_info.clone(),
mint_authority_info.clone(),
payer_account_info.clone(),
update_authority_info.clone(),
system_account_info.clone(),
rent_info.clone(),
],
master_metadata.data,
true,
false,
)?;
let edition_authority_seeds = &[
PREFIX.as_bytes(),
program_id.as_ref(),
&mint_info.key.as_ref(),
EDITION.as_bytes(),
&[bump_seed],
];
create_or_allocate_account_raw(
*program_id,
new_edition_account_info,
rent_info,
system_account_info,
payer_account_info,
MAX_EDITION_LEN,
edition_authority_seeds,
)?;
let mut new_edition = Edition::from_account_info(new_edition_account_info)?;
new_edition.key = Key::EditionV1;
new_edition.parent = *master_edition_account_info.key;
new_edition.edition = match reservation_list_info {
Some(account) => {
let mut reservation_list = get_reservation_list(account)?;
if let Some(supply_snapshot) = reservation_list.supply_snapshot() {
let mut prev_total_offsets: u64 = 0;
let mut offset: Option<u64> = None;
let mut reservations = reservation_list.reservations();
for i in 0..reservations.len() {
let mut reservation = &mut reservations[i];
if reservation.address == *mint_authority_info.key {
offset = Some(
prev_total_offsets
.checked_add(reservation.spots_remaining)
.ok_or(MetadataError::NumericalOverflowError)?,
);
// You get your editions in reverse order but who cares, saves a byte
reservation.spots_remaining = reservation
.spots_remaining
.checked_sub(1)
.ok_or(MetadataError::NumericalOverflowError)?;
reservation_list.set_reservations(reservations);
reservation_list.save(account)?;
break;
}
prev_total_offsets = prev_total_offsets
.checked_add(reservation.total_spots)
.ok_or(MetadataError::NumericalOverflowError)?;
}
match offset {
Some(val) => supply_snapshot
.checked_add(val)
.ok_or(MetadataError::NumericalOverflowError)?,
None => {
return Err(MetadataError::AddressNotInReservation.into());
}
}
} else {
return Err(MetadataError::ReservationNotSet.into());
}
}
None => master_edition.supply,
};
new_edition.serialize(&mut *new_edition_account_info.data.borrow_mut())?;
// Now make sure this mint can never be used by anybody else.
transfer_mint_authority(
&edition_key,
new_edition_account_info,
mint_info,
mint_authority_info,
token_program_account_info,
)?;
Ok(())
}
pub fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult {
let TokenBurnParams {
mint,
source,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let mut seeds: Vec<&[&[u8]]> = vec![];
if let Some(seed) = authority_signer_seeds {
seeds.push(seed);
}
let result = invoke_signed(
&spl_token::instruction::burn(
token_program.key,
source.key,
mint.key,
authority.key,
&[],
amount,
)?,
&[source, mint, authority, token_program],
seeds.as_slice(),
);
result.map_err(|_| MetadataError::TokenBurnFailed.into())
}
/// TokenBurnParams
pub struct TokenBurnParams<'a: 'b, 'b> {
/// mint
pub mint: AccountInfo<'a>,
/// source
pub source: AccountInfo<'a>,
/// amount
pub amount: u64,
/// authority
pub authority: AccountInfo<'a>,
/// authority_signer_seeds
pub authority_signer_seeds: Option<&'b [&'b [u8]]>,
/// token_program
pub token_program: AccountInfo<'a>,
}
pub fn spl_token_mint_to(params: TokenMintToParams<'_, '_>) -> ProgramResult {
let TokenMintToParams {
mint,
destination,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let mut seeds: Vec<&[&[u8]]> = vec![];
if let Some(seed) = authority_signer_seeds {
seeds.push(seed);
}
let result = invoke_signed(
&spl_token::instruction::mint_to(
token_program.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[mint, destination, authority, token_program],
seeds.as_slice(),
);
result.map_err(|_| MetadataError::TokenMintToFailed.into())
}
/// TokenMintToParams
pub struct TokenMintToParams<'a: 'b, 'b> {
/// mint
pub mint: AccountInfo<'a>,
/// destination
pub destination: AccountInfo<'a>,
/// amount
pub amount: u64,
/// authority
pub authority: AccountInfo<'a>,
/// authority_signer_seeds
pub authority_signer_seeds: Option<&'b [&'b [u8]]>,
/// token_program
pub token_program: AccountInfo<'a>,
}
pub fn assert_derivation(
program_id: &Pubkey,
account: &AccountInfo,
path: &[&[u8]],
) -> Result<u8, ProgramError> {
let (key, bump) = Pubkey::find_program_address(&path, program_id);
if key != *account.key {
return Err(MetadataError::DerivedKeyInvalid.into());
}
Ok(bump)
}
pub fn assert_signer(account_info: &AccountInfo) -> ProgramResult {
if !account_info.is_signer {
Err(ProgramError::MissingRequiredSignature)
} else {
Ok(())
}
}
pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> ProgramResult {
if account.owner != owner {
Err(MetadataError::IncorrectOwner.into())
} else {
Ok(())
}
}
pub fn assert_token_program_matches_package(token_program_info: &AccountInfo) -> ProgramResult {
if *token_program_info.key != spl_token::id() {
return Err(MetadataError::InvalidTokenProgram.into());
}
Ok(())
}
pub fn try_from_slice_checked<T: BorshDeserialize>(
data: &[u8],
data_type: Key,
data_size: usize,
) -> Result<T, ProgramError> {
if (data[0] != data_type as u8 && data[0] != Key::Uninitialized as u8)
|| data.len() != data_size
{
return Err(MetadataError::DataTypeMismatch.into());
}
let result: T = try_from_slice_unchecked(data)?;
Ok(result)
}

View File

@ -0,0 +1,21 @@
[package]
name = "spl-token-metadata-test-client"
version = "0.1.0"
description = "Metaplex Library Metadata Integration Test Client"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
publish = false
[dependencies]
solana-client = "1.6.10"
solana-program = "1.6.10"
solana-sdk = "1.6.10"
bincode = "1.3.2"
borsh = "0.8.2"
clap = "2.33.3"
solana-clap-utils = "1.6"
solana-cli-config = "1.6"
spl-token-metadata = { path = "../program", features = [ "no-entrypoint" ] }
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }

View File

@ -0,0 +1,851 @@
use {
clap::{crate_description, crate_name, crate_version, App, Arg, ArgMatches, SubCommand},
solana_clap_utils::{
input_parsers::pubkey_of,
input_validators::{is_url, is_valid_pubkey, is_valid_signer},
},
solana_client::rpc_client::RpcClient,
solana_program::{borsh::try_from_slice_unchecked, program_pack::Pack},
solana_sdk::{
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
system_instruction::create_account,
transaction::Transaction,
},
spl_token::{
instruction::{approve, initialize_account, initialize_mint, mint_to},
state::{Account, Mint},
},
spl_token_metadata::{
instruction::{
create_master_edition, create_metadata_accounts,
mint_new_edition_from_master_edition_via_token, mint_printing_tokens,
update_metadata_accounts,
},
state::{Data, Edition, Key, MasterEdition, Metadata, EDITION, PREFIX},
},
std::str::FromStr,
};
const TOKEN_PROGRAM_PUBKEY: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
fn mint_coins(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let amount = match app_matches.value_of("amount") {
Some(val) => Some(val.parse::<u64>().unwrap()),
None => None,
}
.unwrap();
let mint_key = pubkey_of(app_matches, "mint").unwrap();
let mut instructions = vec![];
let mut signers = vec![&payer];
let destination_key: Pubkey;
let destination = Keypair::new();
if app_matches.is_present("destination") {
destination_key = pubkey_of(app_matches, "destination").unwrap();
} else {
destination_key = destination.pubkey();
signers.push(&destination);
instructions.push(create_account(
&payer.pubkey(),
&destination_key,
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(&token_key, &destination_key, &mint_key, &payer.pubkey()).unwrap(),
);
}
instructions.push(
mint_to(
&token_key,
&mint_key,
&destination_key,
&payer.pubkey(),
&[&payer.pubkey()],
amount,
)
.unwrap(),
);
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
println!("Minted {:?} tokens to {:?}.", amount, destination_key);
}
fn show(app_matches: &ArgMatches, _payer: Keypair, client: RpcClient) {
let program_key = spl_token_metadata::id();
let printing_mint_key = pubkey_of(app_matches, "mint").unwrap();
let master_metadata_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
printing_mint_key.as_ref(),
];
let (master_metadata_key, _) =
Pubkey::find_program_address(master_metadata_seeds, &program_key);
let master_metadata_account = client.get_account(&master_metadata_key).unwrap();
let master_metadata: Metadata =
try_from_slice_unchecked(&master_metadata_account.data).unwrap();
let update_authority = master_metadata.update_authority;
let master_edition_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
&master_metadata.mint.as_ref(),
EDITION.as_bytes(),
];
let (master_edition_key, _) = Pubkey::find_program_address(master_edition_seeds, &program_key);
let master_edition_account = client.get_account(&master_edition_key).unwrap();
println!("Metadata key: {:?}", master_metadata_key);
println!("Metadata: {:#?}", master_metadata);
println!("Update authority: {:?}", update_authority);
if master_edition_account.data[0] == Key::MasterEditionV1 as u8 {
let master_edition: MasterEdition =
try_from_slice_unchecked(&master_edition_account.data).unwrap();
println!("Master edition {:#?}", master_edition);
} else {
let edition: Edition = try_from_slice_unchecked(&master_edition_account.data).unwrap();
println!("Limited edition {:#?}", edition);
}
}
fn mint_edition_via_token_call(
app_matches: &ArgMatches,
payer: Keypair,
client: RpcClient,
) -> (Edition, Pubkey) {
let account_authority = read_keypair_file(
app_matches
.value_of("account_authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let program_key = spl_token_metadata::id();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let new_mint_key = Keypair::new();
let added_token_account = Keypair::new();
let burn_authority = Keypair::new();
let new_mint_pub = new_mint_key.pubkey();
let metadata_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
&new_mint_pub.as_ref(),
];
let (metadata_key, _) = Pubkey::find_program_address(metadata_seeds, &program_key);
let edition_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
&new_mint_pub.as_ref(),
EDITION.as_bytes(),
];
let (edition_key, _) = Pubkey::find_program_address(edition_seeds, &program_key);
let printing_mint_key = pubkey_of(app_matches, "mint").unwrap();
let master_metadata_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
printing_mint_key.as_ref(),
];
let (master_metadata_key, _) =
Pubkey::find_program_address(master_metadata_seeds, &program_key);
let master_metadata_account = client.get_account(&master_metadata_key).unwrap();
let master_metadata: Metadata =
try_from_slice_unchecked(&master_metadata_account.data).unwrap();
let update_authority = master_metadata.update_authority;
let master_edition_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
&master_metadata.mint.as_ref(),
EDITION.as_bytes(),
];
let (master_edition_key, _) = Pubkey::find_program_address(master_edition_seeds, &program_key);
let master_edition_account = client.get_account(&master_edition_key).unwrap();
let master_edition: MasterEdition =
try_from_slice_unchecked(&master_edition_account.data).unwrap();
let mut signers = vec![
&account_authority,
&new_mint_key,
&burn_authority,
&added_token_account,
];
let mut instructions = vec![
create_account(
&payer.pubkey(),
&new_mint_key.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
),
initialize_mint(
&token_key,
&new_mint_key.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
create_account(
&payer.pubkey(),
&added_token_account.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
),
initialize_account(
&token_key,
&added_token_account.pubkey(),
&new_mint_key.pubkey(),
&payer.pubkey(),
)
.unwrap(),
mint_to(
&token_key,
&new_mint_key.pubkey(),
&added_token_account.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
1,
)
.unwrap(),
];
let new_master_key: Pubkey;
let new_master_account = Keypair::new();
if app_matches.is_present("account") {
new_master_key = pubkey_of(app_matches, "account").unwrap();
} else {
signers.push(&new_master_account);
new_master_key = new_master_account.pubkey();
instructions.push(create_account(
&payer.pubkey(),
&new_master_account.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(
&token_key,
&new_master_account.pubkey(),
&master_edition.printing_mint,
&payer.pubkey(),
)
.unwrap(),
);
instructions.push(mint_printing_tokens(
token_key,
new_master_account.pubkey(),
master_edition.printing_mint,
update_authority,
master_metadata_key,
master_edition_key,
1,
));
}
instructions.push(
approve(
&token_key,
&new_master_key,
&burn_authority.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
1,
)
.unwrap(),
);
instructions.push(mint_new_edition_from_master_edition_via_token(
program_key,
metadata_key,
edition_key,
master_edition_key,
new_mint_key.pubkey(),
payer.pubkey(),
master_edition.printing_mint,
new_master_key,
burn_authority.pubkey(),
payer.pubkey(),
update_authority,
master_metadata_key,
None,
));
println!("Instructions, {:?},", instructions);
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let account = client.get_account(&edition_key).unwrap();
let edition: Edition = try_from_slice_unchecked(&account.data).unwrap();
(edition, edition_key)
}
fn master_edition_call(
app_matches: &ArgMatches,
payer: Keypair,
client: RpcClient,
) -> (MasterEdition, Pubkey) {
let update_authority = read_keypair_file(
app_matches
.value_of("update_authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let mint_authority = read_keypair_file(
app_matches
.value_of("mint_authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let printing_mint = Keypair::new();
let one_time_printing_authorization_mint = Keypair::new();
let program_key = spl_token_metadata::id();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let mint_key = pubkey_of(app_matches, "mint").unwrap();
let metadata_seeds = &[PREFIX.as_bytes(), &program_key.as_ref(), mint_key.as_ref()];
let (metadata_key, _) = Pubkey::find_program_address(metadata_seeds, &program_key);
let metadata_account = client.get_account(&metadata_key).unwrap();
let metadata: Metadata = try_from_slice_unchecked(&metadata_account.data).unwrap();
let master_edition_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
&metadata.mint.as_ref(),
EDITION.as_bytes(),
];
let (master_edition_key, _) = Pubkey::find_program_address(master_edition_seeds, &program_key);
let max_supply = match app_matches.value_of("max_supply") {
Some(val) => Some(val.parse::<u64>().unwrap()),
None => None,
};
let added_token_account = Keypair::new();
let needs_a_token = app_matches.is_present("add_one_token");
let mut signers = vec![
&update_authority,
&printing_mint,
&one_time_printing_authorization_mint,
];
let mut instructions = vec![];
if needs_a_token {
instructions.push(create_account(
&payer.pubkey(),
&added_token_account.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Account::LEN)
.unwrap(),
Account::LEN as u64,
&token_key,
));
instructions.push(
initialize_account(
&token_key,
&added_token_account.pubkey(),
&metadata.mint,
&payer.pubkey(),
)
.unwrap(),
);
instructions.push(
mint_to(
&token_key,
&metadata.mint,
&added_token_account.pubkey(),
&payer.pubkey(),
&[&payer.pubkey()],
1,
)
.unwrap(),
)
}
instructions.push(create_account(
&payer.pubkey(),
&printing_mint.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
));
instructions.push(
initialize_mint(
&token_key,
&printing_mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
);
instructions.push(create_account(
&payer.pubkey(),
&one_time_printing_authorization_mint.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
));
instructions.push(
initialize_mint(
&token_key,
&one_time_printing_authorization_mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
);
let printing_mint_authority = payer.pubkey();
let mut one_time_printing_authorization_mint_authority = None;
if max_supply.is_some() {
one_time_printing_authorization_mint_authority = Some(payer.pubkey());
}
instructions.push(create_master_edition(
program_key,
master_edition_key,
mint_key,
printing_mint.pubkey(),
one_time_printing_authorization_mint.pubkey(),
update_authority.pubkey(),
printing_mint_authority,
mint_authority.pubkey(),
metadata_key,
payer.pubkey(),
max_supply,
one_time_printing_authorization_mint_authority,
));
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
if needs_a_token {
signers.push(&added_token_account);
}
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let account = client.get_account(&master_edition_key).unwrap();
let master_edition: MasterEdition = try_from_slice_unchecked(&account.data).unwrap();
(master_edition, master_edition_key)
}
fn update_metadata_account_call(
app_matches: &ArgMatches,
payer: Keypair,
client: RpcClient,
) -> (Metadata, Pubkey) {
let update_authority = read_keypair_file(
app_matches
.value_of("update_authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let program_key = spl_token_metadata::id();
let mint_key = pubkey_of(app_matches, "mint").unwrap();
let metadata_seeds = &[PREFIX.as_bytes(), &program_key.as_ref(), mint_key.as_ref()];
let (metadata_key, _) = Pubkey::find_program_address(metadata_seeds, &program_key);
let uri = match app_matches.value_of("uri") {
Some(val) => Some(val.to_owned()),
None => None,
};
let name = match app_matches.value_of("name") {
Some(val) => Some(val.to_owned()),
None => None,
};
let new_update_authority = pubkey_of(app_matches, "new_update_authority");
let metadata_account = client.get_account(&metadata_key).unwrap();
let metadata: Metadata = try_from_slice_unchecked(&metadata_account.data).unwrap();
let new_data = Data {
name: name.unwrap_or(metadata.data.name),
symbol: metadata.data.symbol,
uri: uri.unwrap_or(metadata.data.uri),
seller_fee_basis_points: 0,
creators: metadata.data.creators,
};
let instructions = [update_metadata_accounts(
program_key,
metadata_key,
update_authority.pubkey(),
new_update_authority,
Some(new_data),
None,
)];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
let signers = vec![&update_authority];
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let metadata_account = client.get_account(&metadata_key).unwrap();
let metadata: Metadata = try_from_slice_unchecked(&metadata_account.data).unwrap();
(metadata, metadata_key)
}
fn create_metadata_account_call(
app_matches: &ArgMatches,
payer: Keypair,
client: RpcClient,
) -> (Metadata, Pubkey) {
let update_authority = read_keypair_file(
app_matches
.value_of("update_authority")
.unwrap_or_else(|| app_matches.value_of("keypair").unwrap()),
)
.unwrap();
let program_key = spl_token_metadata::id();
let token_key = Pubkey::from_str(TOKEN_PROGRAM_PUBKEY).unwrap();
let new_mint = Keypair::new();
let name = app_matches.value_of("name").unwrap().to_owned();
let symbol = app_matches.value_of("symbol").unwrap().to_owned();
let uri = app_matches.value_of("uri").unwrap().to_owned();
let new_mint_key = new_mint.pubkey();
let metadata_seeds = &[
PREFIX.as_bytes(),
&program_key.as_ref(),
new_mint_key.as_ref(),
];
let (metadata_key, _) = Pubkey::find_program_address(metadata_seeds, &program_key);
let instructions = [
create_account(
&payer.pubkey(),
&new_mint.pubkey(),
client
.get_minimum_balance_for_rent_exemption(Mint::LEN)
.unwrap(),
Mint::LEN as u64,
&token_key,
),
initialize_mint(
&token_key,
&new_mint.pubkey(),
&payer.pubkey(),
Some(&payer.pubkey()),
0,
)
.unwrap(),
create_metadata_accounts(
program_key,
metadata_key,
new_mint.pubkey(),
payer.pubkey(),
payer.pubkey(),
update_authority.pubkey(),
name,
symbol,
uri,
None,
0,
update_authority.pubkey() != payer.pubkey(),
false,
),
];
let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey()));
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
let mut signers = vec![&payer, &new_mint];
if update_authority.pubkey() != payer.pubkey() {
signers.push(&update_authority)
}
transaction.sign(&signers, recent_blockhash);
client.send_and_confirm_transaction(&transaction).unwrap();
let account = client.get_account(&metadata_key).unwrap();
let metadata: Metadata = try_from_slice_unchecked(&account.data).unwrap();
(metadata, metadata_key)
}
fn main() {
let app_matches = App::new(crate_name!())
.about(crate_description!())
.version(crate_version!())
.arg(
Arg::with_name("keypair")
.long("keypair")
.value_name("KEYPAIR")
.validator(is_valid_signer)
.takes_value(true)
.global(true)
.help("Filepath or URL to a keypair"),
)
.arg(
Arg::with_name("json_rpc_url")
.long("url")
.value_name("URL")
.takes_value(true)
.global(true)
.validator(is_url)
.help("JSON RPC URL for the cluster [default: devnet]"),
)
.arg(
Arg::with_name("update_authority")
.long("update_authority")
.value_name("UPDATE_AUTHORITY")
.takes_value(true)
.global(true)
.help("Update authority filepath or url to keypair besides yourself, defaults to normal keypair"),
)
.subcommand(
SubCommand::with_name("create_metadata_accounts")
.about("Create Metadata Accounts")
.arg(
Arg::with_name("name")
.long("name")
.global(true)
.value_name("NAME")
.takes_value(true)
.help("name for the Mint"),
)
.arg(
Arg::with_name("symbol")
.long("symbol")
.value_name("SYMBOL")
.takes_value(true)
.global(true)
.help("symbol for the Mint"),
)
.arg(
Arg::with_name("uri")
.long("uri")
.value_name("URI")
.takes_value(true)
.required(true)
.help("URI for the Mint"),
)
).subcommand(
SubCommand::with_name("mint_coins")
.about("Mint coins to your mint to an account")
.arg(
Arg::with_name("mint")
.long("mint")
.value_name("MINT")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Mint of the Metadata"),
).arg(
Arg::with_name("destination")
.long("destination")
.value_name("DESTINATION")
.required(false)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Destination account. If one isnt given, one is made."),
).arg(
Arg::with_name("amount")
.long("amount")
.value_name("AMOUNT")
.required(true)
.takes_value(true)
.help("How many"),
)
)
.subcommand(
SubCommand::with_name("update_metadata_accounts")
.about("Update Metadata Accounts")
.arg(
Arg::with_name("mint")
.long("mint")
.value_name("MINT")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Mint of the Metadata"),
)
.arg(
Arg::with_name("uri")
.long("uri")
.value_name("URI")
.takes_value(true)
.required(false)
.help("new URI for the Metadata"),
)
.arg(
Arg::with_name("name")
.long("name")
.value_name("NAME")
.takes_value(true)
.required(false)
.help("new NAME for the Metadata"),
)
.arg(
Arg::with_name("new_update_authority")
.long("new_update_authority")
.value_name("NEW_UPDATE_AUTHORITY")
.required(false)
.validator(is_valid_pubkey)
.takes_value(true)
.help("New update authority"))
).subcommand(
SubCommand::with_name("show")
.about("Show")
.arg(
Arg::with_name("mint")
.long("mint")
.value_name("MINT")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Metadata mint"),
)
)
.subcommand(
SubCommand::with_name("create_master_edition")
.about("Create Master Edition out of Metadata")
.arg(
Arg::with_name("add_one_token")
.long("add_one_token")
.value_name("ADD_ONE_TOKEN")
.required(false)
.takes_value(false)
.help("Add a token to this mint before calling (useful if your mint has zero tokens, this action requires one to be present)"),
).arg(
Arg::with_name("max_supply")
.long("max_supply")
.value_name("MAX_SUPPLY")
.required(false)
.takes_value(true)
.help("Set a maximum supply that can be minted."),
).arg(
Arg::with_name("mint")
.long("mint")
.value_name("MINT")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Metadata mint to from which to create a master edition."),
).arg(
Arg::with_name("mint_authority")
.long("mint_authority")
.value_name("MINT_AUTHORITY")
.validator(is_valid_signer)
.takes_value(true)
.required(false)
.help("Filepath or URL to a keypair representing mint authority, defaults to you"),
)
).subcommand(
SubCommand::with_name("mint_new_edition_from_master_edition_via_token")
.about("Mint new edition from master edition via a token - this will just also mint the token for you and submit it.")
.arg(
Arg::with_name("mint")
.long("mint")
.value_name("MINT")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Printing mint from which to mint this new edition"),
).arg(
Arg::with_name("account")
.long("account")
.value_name("ACCOUNT")
.required(false)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Account which contains authorization token. If not provided, one will be made."),
).arg(
Arg::with_name("account_authority")
.long("account_authority")
.value_name("ACCOUNT_AUTHORITY")
.required(false)
.validator(is_valid_signer)
.takes_value(true)
.help("Account's authority, defaults to you"),
)
).get_matches();
let client = RpcClient::new(
app_matches
.value_of("json_rpc_url")
.unwrap_or(&"https://devnet.solana.com".to_owned())
.to_owned(),
);
let payer = read_keypair_file(app_matches.value_of("keypair").unwrap()).unwrap();
let (sub_command, sub_matches) = app_matches.subcommand();
match (sub_command, sub_matches) {
("create_metadata_accounts", Some(arg_matches)) => {
let (metadata, metadata_key) = create_metadata_account_call(arg_matches, payer, client);
println!(
"Create metadata account with mint {:?} and key {:?} and name of {:?} and symbol of {:?}",
metadata.mint, metadata_key, metadata.data.name, metadata.data.symbol
);
}
("update_metadata_accounts", Some(arg_matches)) => {
let (metadata, metadata_key) = update_metadata_account_call(arg_matches, payer, client);
println!(
"Update metadata account with mint {:?} and key {:?} which now has URI of {:?}",
metadata.mint, metadata_key, metadata.data.uri
);
}
("create_master_edition", Some(arg_matches)) => {
let (master_edition, master_edition_key) =
master_edition_call(arg_matches, payer, client);
println!(
"Created master edition {:?} with key {:?}",
master_edition, master_edition_key
);
}
("mint_new_edition_from_master_edition_via_token", Some(arg_matches)) => {
let (edition, edition_key) = mint_edition_via_token_call(arg_matches, payer, client);
println!(
"Created new edition {:?} from parent edition {:?} with edition number {:?}",
edition_key, edition.parent, edition.edition
);
}
("show", Some(arg_matches)) => {
show(arg_matches, payer, client);
}
("mint_coins", Some(arg_matches)) => {
mint_coins(arg_matches, payer, client);
}
_ => unreachable!(),
}
}

View File

@ -0,0 +1,24 @@
[package]
name = "spl-token-vault"
version = "0.0.1"
description = "Metaplex Token Vault"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
exclude = ["js/**"]
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
num-derive = "0.3"
num-traits = "0.2"
solana-program = "1.6.10"
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }
thiserror = "1.0"
borsh = "0.8.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,30 @@
---
title: Token Vault Program
---
## Background
Solana's programming model and the definitions of the Solana terms used in this
document are available at:
- https://docs.solana.com/apps
- https://docs.solana.com/terminology
## Source
The Vault Program's source is available on
[github](https://github.com/metaplex-foundation/metaplex)
There is also an example Rust client located at
[github](https://github.com/metaplex-foundation/metaplex/tree/master/token_vault/test/src/main.rs)
that can be perused for learning and built if desired with `cargo build`. It allows testing out a variety of scenarios.
## Interface
The on-chain Token Fraction program is written in Rust and available on crates.io as
[spl-vault](https://crates.io/crates/spl-token-vault) and
[docs.rs](https://docs.rs/spl-token-vault).
## Operational overview
TODO

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,25 @@
//! Program entrypoint definitions
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
use {
crate::{error::VaultError, processor},
solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
program_error::PrintProgramError, pubkey::Pubkey,
},
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) {
// catch the error so we can print it
error.print::<VaultError>();
return Err(error);
}
Ok(())
}

View File

@ -0,0 +1,249 @@
//! Error types
use {
num_derive::FromPrimitive,
solana_program::{
decode_error::DecodeError,
msg,
program_error::{PrintProgramError, ProgramError},
},
thiserror::Error,
};
/// Errors that may be returned by the Vault program.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum VaultError {
/// Invalid instruction data passed in.
#[error("Failed to unpack instruction data")]
InstructionUnpackError,
/// Lamport balance below rent-exempt threshold.
#[error("Lamport balance below rent-exempt threshold")]
NotRentExempt,
/// Already initialized
#[error("Already initialized")]
AlreadyInitialized,
/// Uninitialized
#[error("Uninitialized")]
Uninitialized,
/// Account does not have correct owner
#[error("Account does not have correct owner")]
IncorrectOwner,
/// NumericalOverflowError
#[error("NumericalOverflowError")]
NumericalOverflowError,
/// Provided token account contains no tokens
#[error("Provided token account contains no tokens")]
TokenAccountContainsNoTokens,
/// Provided token account cannot provide amount specified
#[error("Provided token account cannot provide amount specified")]
TokenAccountAmountLessThanAmountSpecified,
/// Provided vault account contains is not empty
#[error("Provided vault account contains is not empty")]
VaultAccountIsNotEmpty,
/// Provided vault account is not owned by program
#[error("Provided vault account is not owned by program derived address with seed of prefix and program id")]
VaultAccountIsNotOwnedByProgram,
/// The provided safety deposit account address does not match the expected program derived address
#[error(
"The provided safety deposit account address does not match the expected program derived address"
)]
SafetyDepositAddressInvalid,
/// Token transfer failed
#[error("Token transfer failed")]
TokenTransferFailed,
/// Token mint to failed
#[error("Token mint to failed")]
TokenMintToFailed,
/// Token burn failed
#[error("Token burn failed")]
TokenBurnFailed,
/// Vault mint not empty on int
#[error("Vault mint not empty on init")]
VaultMintNotEmpty,
/// Vault mint's authority not set to program
#[error("Vault mint's authority not set to program PDA with seed of prefix and program id")]
VaultAuthorityNotProgram,
/// Vault treasury not empty on init
#[error("Vault treasury not empty on init")]
TreasuryNotEmpty,
/// Vault treasury's owner not set to program
#[error("Vault treasury's owner not set to program pda with seed of prefix and program id")]
TreasuryOwnerNotProgram,
/// Vault should be inactive
#[error("Vault should be inactive")]
VaultShouldBeInactive,
/// Vault should be active
#[error("Vault should be active")]
VaultShouldBeActive,
/// Vault should be combined
#[error("Vault should be combined")]
VaultShouldBeCombined,
/// Vault treasury needs to match fraction mint
#[error("Vault treasury needs to match fraction mint")]
VaultTreasuryMintDoesNotMatchVaultMint,
/// Redeem Treasury cannot be same mint as fraction
#[error("Redeem Treasury cannot be same mint as fraction")]
RedeemTreasuryCantShareSameMintAsFraction,
/// Invalid program authority provided
#[error("Invalid program authority provided")]
InvalidAuthority,
/// Redeem treasury mint must match lookup mint
#[error("Redeem treasury mint must match lookup mint")]
RedeemTreasuryMintMustMatchLookupMint,
/// You must pay with the same mint as the external pricing oracle
#[error("You must pay with the same mint as the external pricing oracle")]
PaymentMintShouldMatchPricingMint,
/// Your share account should match the mint of the fractional mint
#[error("Your share account should match the mint of the fractional mint")]
ShareMintShouldMatchFractionalMint,
/// Vault mint provided does not match that on the token vault
#[error("Vault mint provided does not match that on the token vault")]
VaultMintNeedsToMatchVault,
/// Redeem treasury provided does not match that on the token vault
#[error("Redeem treasury provided does not match that on the token vault")]
RedeemTreasuryNeedsToMatchVault,
/// Fraction treasury provided does not match that on the token vault
#[error("Fraction treasury provided does not match that on the token vault")]
FractionTreasuryNeedsToMatchVault,
/// Not allowed to combine at this time
#[error("Not allowed to combine at this time")]
NotAllowedToCombine,
/// You cannot afford to combine this pool
#[error("You cannot afford to combine this vault")]
CannotAffordToCombineThisVault,
/// You have no shares to redeem
#[error("You have no shares to redeem")]
NoShares,
/// Your outstanding share account is the incorrect mint
#[error("Your outstanding share account is the incorrect mint")]
OutstandingShareAccountNeedsToMatchFractionalMint,
/// Your destination account is the incorrect mint
#[error("Your destination account is the incorrect mint")]
DestinationAccountNeedsToMatchRedeemMint,
/// Fractional mint is empty
#[error("Fractional mint is empty")]
FractionSupplyEmpty,
/// Token Program Provided Needs To Match Vault
#[error("Token Program Provided Needs To Match Vault")]
TokenProgramProvidedDoesNotMatchVault,
/// Authority of vault needs to be signer for this action
#[error("Authority of vault needs to be signer for this action")]
AuthorityIsNotSigner,
/// Authority of vault does not match authority provided
#[error("Authority of vault does not match authority provided")]
AuthorityDoesNotMatch,
/// This safety deposit box does not belong to this vault!
#[error("This safety deposit box does not belong to this vault!")]
SafetyDepositBoxVaultMismatch,
/// The store provided does not match the store key on the safety deposit box!
#[error("The store provided does not match the store key on the safety deposit box!")]
StoreDoesNotMatchSafetyDepositBox,
/// This safety deposit box is empty!
#[error("This safety deposit box is empty!")]
StoreEmpty,
/// The destination account to receive your token needs to be the same mint as the token's mint
#[error("The destination account to receive your token needs to be the same mint as the token's mint")]
DestinationAccountNeedsToMatchTokenMint,
/// The destination account to receive your shares needs to be the same mint as the vault's fraction mint
#[error("The destination account to receive your shares needs to be the same mint as the vault's fraction mint")]
DestinationAccountNeedsToMatchFractionMint,
/// The source account to send your shares from needs to be the same mint as the vault's fraction mint
#[error("The source account to send your shares from needs to be the same mint as the vault's fraction mint")]
SourceAccountNeedsToMatchFractionMint,
/// This vault does not allow the minting of new shares!
#[error("This vault does not allow the minting of new shares!")]
VaultDoesNotAllowNewShareMinting,
/// There are not enough shares
#[error("There are not enough shares")]
NotEnoughShares,
/// External price account must be signer
#[error("External price account must be signer")]
ExternalPriceAccountMustBeSigner,
///Very bad, someone changed external account's price mint after vault creation!
#[error("Very bad, someone changed external account's price mint after vault creation!")]
RedeemTreasuryMintShouldMatchPricingMint,
/// Store has less than amount desired
#[error("Store has less than amount desired")]
StoreLessThanAmount,
/// Invalid token program
#[error("Invalid token program")]
InvalidTokenProgram,
/// Data type mismatch
#[error("Data type mismatch")]
DataTypeMismatch,
/// Accept payment delegate should be none
#[error("Accept payment delegate should be none")]
DelegateShouldBeNone,
/// Accept payment close authority should be none
#[error("Accept payment close authority should be none")]
CloseAuthorityShouldBeNone,
}
impl PrintProgramError for VaultError {
fn print<E>(&self) {
msg!(&self.to_string());
}
}
impl From<VaultError> for ProgramError {
fn from(e: VaultError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for VaultError {
fn type_of() -> &'static str {
"Vault Error"
}
}

View File

@ -0,0 +1,434 @@
use {
crate::state::{ExternalPriceAccount, Key},
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
sysvar,
},
};
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct InitVaultArgs {
pub allow_further_share_creation: bool,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct AmountArgs {
pub amount: u64,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub struct NumberOfShareArgs {
pub number_of_shares: u64,
}
/// Instructions supported by the Fraction program.
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub enum VaultInstruction {
/// Initialize a token vault, starts inactivate. Add tokens in subsequent instructions, then activate.
/// 0. `[writable]` Initialized fractional share mint with 0 tokens in supply, authority on mint must be pda of program with seed [prefix, programid]
/// 1. `[writable]` Initialized redeem treasury token account with 0 tokens in supply, owner of account must be pda of program like above
/// 2. `[writable]` Initialized fraction treasury token account with 0 tokens in supply, owner of account must be pda of program like above
/// 3. `[writable]` Uninitialized vault account
/// 4. `[]` Authority on the vault
/// 5. `[]` Pricing Lookup Address
/// 6. `[]` Token program
/// 7. `[]` Rent sysvar
InitVault(InitVaultArgs),
/// Add a token to a inactive token vault
/// 0. `[writable]` Uninitialized safety deposit box account address (will be created and allocated by this endpoint)
/// Address should be pda with seed of [PREFIX, vault_address, token_mint_address]
/// 1. `[writable]` Initialized Token account
/// 2. `[writable]` Initialized Token store account with authority of this program, this will get set on the safety deposit box
/// 3. `[writable]` Initialized inactive fractionalized token vault
/// 4. `[signer]` Authority on the vault
/// 5. `[signer]` Payer
/// 6. `[signer]` Transfer Authority to move desired token amount from token account to safety deposit
/// 7. `[]` Token program
/// 8. `[]` Rent sysvar
/// 9. `[]` System account sysvar
AddTokenToInactiveVault(AmountArgs),
/// Activates the vault, distributing initial shares into the fraction treasury.
/// Tokens can no longer be removed in this state until Combination.
/// 0. `[writable]` Initialized inactivated fractionalized token vault
/// 1. `[writable]` Fraction mint
/// 2. `[writable]` Fraction treasury
/// 3. `[]` Fraction mint authority for the program - seed of [PREFIX, program_id]
/// 4. `[signer]` Authority on the vault
/// 5. `[]` Token program
ActivateVault(NumberOfShareArgs),
/// This act checks the external pricing oracle for permission to combine and the price of the circulating market cap to do so.
/// If you can afford it, this amount is charged and placed into the redeem treasury for shareholders to redeem at a later time.
/// The treasury then unlocks into Combine state and you can remove the tokens.
/// 0. `[writable]` Initialized activated token vault
/// 1. `[writable]` Token account containing your portion of the outstanding fraction shares
/// 2. `[writable]` Token account of the redeem_treasury mint type that you will pay with
/// 3. `[writable]` Fraction mint
/// 4. `[writable]` Fraction treasury account
/// 5. `[writable]` Redeem treasury account
/// 6. `[]` New authority on the vault going forward - can be same authority if you want
/// 7. `[signer]` Authority on the vault
/// 8. `[signer]` Transfer authority for the token account and outstanding fractional shares account you're transferring from
/// 9. `[]` PDA-based Burn authority for the fraction treasury account containing the uncirculated shares seed [PREFIX, program_id]
/// 10. `[]` External pricing lookup address
/// 11. `[]` Token program
CombineVault,
/// If in the combine state, shareholders can hit this endpoint to burn shares in exchange for monies from the treasury.
/// Once fractional supply is zero and all tokens have been removed this action will take vault to Deactivated
/// 0. `[writable]` Initialized Token account containing your fractional shares
/// 1. `[writable]` Initialized Destination token account where you wish your proceeds to arrive
/// 2. `[writable]` Fraction mint
/// 3. `[writable]` Redeem treasury account
/// 4. `[]` PDA-based Transfer authority for the transfer of proceeds from redeem treasury to destination seed [PREFIX, program_id]
/// 5. `[signer]` Burn authority for the burning of your shares
/// 6. `[]` Combined token vault
/// 7. `[]` Token program
/// 8. `[]` Rent sysvar
RedeemShares,
/// If in combine state, authority on vault can hit this to withdrawal some of a token type from a safety deposit box.
/// Once fractional supply is zero and all tokens have been removed this action will take vault to Deactivated
/// 0. `[writable]` Initialized Destination account for the tokens being withdrawn
/// 1. `[writable]` The safety deposit box account key for the tokens
/// 2. `[writable]` The store key on the safety deposit box account
/// 3. `[writable]` The initialized combined token vault
/// 4. `[]` Fraction mint
/// 5. `[signer]` Authority of vault
/// 6. `[]` PDA-based Transfer authority to move the tokens from the store to the destination seed [PREFIX, program_id]
/// 7. `[]` Token program
/// 8. `[]` Rent sysvar
WithdrawTokenFromSafetyDepositBox(AmountArgs),
/// Self explanatory - mint more fractional shares if the vault is configured to allow such.
/// 0. `[writable]` Fraction treasury
/// 1. `[writable]` Fraction mint
/// 2. `[]` The initialized active token vault
/// 3. `[]` PDA-based Mint authority to mint tokens to treasury[PREFIX, program_id]
/// 4. `[signer]` Authority of vault
/// 5. `[]` Token program
MintFractionalShares(NumberOfShareArgs),
/// Withdraws shares from the treasury to a desired account.
/// 0. `[writable]` Initialized Destination account for the shares being withdrawn
/// 1. `[writable]` Fraction treasury
/// 2. `[]` The initialized active token vault
/// 3. `[]` PDA-based Transfer authority to move tokens from treasury to your destination[PREFIX, program_id]
/// 3. `[signer]` Authority of vault
/// 4. `[]` Token program
/// 5. `[]` Rent sysvar
WithdrawSharesFromTreasury(NumberOfShareArgs),
/// Returns shares to the vault if you wish to remove them from circulation.
/// 0. `[writable]` Initialized account from which shares will be withdrawn
/// 1. `[writable]` Fraction treasury
/// 2. `[]` The initialized active token vault
/// 3. `[signer]` Transfer authority to move tokens from your account to treasury
/// 3. `[signer]` Authority of vault
/// 4. `[]` Token program
AddSharesToTreasury(NumberOfShareArgs),
/// Helpful method that isn't necessary to use for main users of the app, but allows one to create/update
/// existing external price account fields if they are signers of this account.
/// Useful for testing purposes, and the CLI makes use of it as well so that you can verify logic.
/// 0. `[writable]` External price account
UpdateExternalPriceAccount(ExternalPriceAccount),
}
/// Creates an InitVault instruction
#[allow(clippy::too_many_arguments)]
pub fn create_init_vault_instruction(
program_id: Pubkey,
fraction_mint: Pubkey,
redeem_treasury: Pubkey,
fraction_treasury: Pubkey,
vault: Pubkey,
vault_authority: Pubkey,
external_price_account: Pubkey,
allow_further_share_creation: bool,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(fraction_mint, false),
AccountMeta::new(redeem_treasury, false),
AccountMeta::new(fraction_treasury, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(vault_authority, false),
AccountMeta::new_readonly(external_price_account, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: VaultInstruction::InitVault(InitVaultArgs {
allow_further_share_creation,
})
.try_to_vec()
.unwrap(),
}
}
/// Creates an UpdateExternalPriceAccount instruction
#[allow(clippy::too_many_arguments)]
pub fn create_update_external_price_account_instruction(
program_id: Pubkey,
external_price_account: Pubkey,
price_per_share: u64,
price_mint: Pubkey,
allowed_to_combine: bool,
) -> Instruction {
Instruction {
program_id,
accounts: vec![AccountMeta::new(external_price_account, true)],
data: VaultInstruction::UpdateExternalPriceAccount(ExternalPriceAccount {
key: Key::ExternalAccountKeyV1,
price_per_share,
price_mint,
allowed_to_combine,
})
.try_to_vec()
.unwrap(),
}
}
/// Creates an AddTokenToInactiveVault instruction
#[allow(clippy::too_many_arguments)]
pub fn create_add_token_to_inactive_vault_instruction(
program_id: Pubkey,
safety_deposit_box: Pubkey,
token_account: Pubkey,
store: Pubkey,
vault: Pubkey,
vault_authority: Pubkey,
payer: Pubkey,
transfer_authority: Pubkey,
amount: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(safety_deposit_box, false),
AccountMeta::new(token_account, false),
AccountMeta::new(store, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(vault_authority, true),
AccountMeta::new_readonly(payer, true),
AccountMeta::new_readonly(transfer_authority, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
],
data: VaultInstruction::AddTokenToInactiveVault(AmountArgs { amount })
.try_to_vec()
.unwrap(),
}
}
/// Creates an ActivateVault instruction
#[allow(clippy::too_many_arguments)]
pub fn create_activate_vault_instruction(
program_id: Pubkey,
vault: Pubkey,
fraction_mint: Pubkey,
fraction_treasury: Pubkey,
fraction_mint_authority: Pubkey,
vault_authority: Pubkey,
number_of_shares: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(vault, false),
AccountMeta::new(fraction_mint, false),
AccountMeta::new(fraction_treasury, false),
AccountMeta::new_readonly(fraction_mint_authority, false),
AccountMeta::new_readonly(vault_authority, true),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: VaultInstruction::ActivateVault(NumberOfShareArgs { number_of_shares })
.try_to_vec()
.unwrap(),
}
}
/// Creates an CombineVault instruction
#[allow(clippy::too_many_arguments)]
pub fn create_combine_vault_instruction(
program_id: Pubkey,
vault: Pubkey,
outstanding_share_token_account: Pubkey,
paying_token_account: Pubkey,
fraction_mint: Pubkey,
fraction_treasury: Pubkey,
redeem_treasury: Pubkey,
new_authority: Pubkey,
vault_authority: Pubkey,
paying_transfer_authority: Pubkey,
uncirculated_burn_authority: Pubkey,
external_pricing_account: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(vault, false),
AccountMeta::new(outstanding_share_token_account, false),
AccountMeta::new(paying_token_account, false),
AccountMeta::new(fraction_mint, false),
AccountMeta::new(fraction_treasury, false),
AccountMeta::new(redeem_treasury, false),
AccountMeta::new(new_authority, false),
AccountMeta::new_readonly(vault_authority, true),
AccountMeta::new_readonly(paying_transfer_authority, true),
AccountMeta::new_readonly(uncirculated_burn_authority, false),
AccountMeta::new_readonly(external_pricing_account, false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: VaultInstruction::CombineVault.try_to_vec().unwrap(),
}
}
/// Creates an RedeemShares instruction
#[allow(clippy::too_many_arguments)]
pub fn create_redeem_shares_instruction(
program_id: Pubkey,
outstanding_shares_account: Pubkey,
proceeds_account: Pubkey,
fraction_mint: Pubkey,
redeem_treasury: Pubkey,
transfer_authority: Pubkey,
burn_authority: Pubkey,
vault: Pubkey,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(outstanding_shares_account, false),
AccountMeta::new(proceeds_account, false),
AccountMeta::new(fraction_mint, false),
AccountMeta::new(redeem_treasury, false),
AccountMeta::new_readonly(transfer_authority, false),
AccountMeta::new_readonly(burn_authority, true),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: VaultInstruction::RedeemShares.try_to_vec().unwrap(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_withdraw_tokens_instruction(
program_id: Pubkey,
destination: Pubkey,
safety_deposit_box: Pubkey,
store: Pubkey,
vault: Pubkey,
fraction_mint: Pubkey,
vault_authority: Pubkey,
transfer_authority: Pubkey,
amount: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(destination, false),
AccountMeta::new(safety_deposit_box, false),
AccountMeta::new(store, false),
AccountMeta::new(vault, false),
AccountMeta::new_readonly(fraction_mint, false),
AccountMeta::new_readonly(vault_authority, true),
AccountMeta::new_readonly(transfer_authority, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: VaultInstruction::WithdrawTokenFromSafetyDepositBox(AmountArgs { amount })
.try_to_vec()
.unwrap(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_mint_shares_instruction(
program_id: Pubkey,
fraction_treasury: Pubkey,
fraction_mint: Pubkey,
vault: Pubkey,
fraction_mint_authority: Pubkey,
vault_authority: Pubkey,
number_of_shares: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(fraction_treasury, false),
AccountMeta::new(fraction_mint, false),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(fraction_mint_authority, false),
AccountMeta::new_readonly(vault_authority, true),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: VaultInstruction::MintFractionalShares(NumberOfShareArgs { number_of_shares })
.try_to_vec()
.unwrap(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_withdraw_shares_instruction(
program_id: Pubkey,
destination: Pubkey,
fraction_treasury: Pubkey,
vault: Pubkey,
transfer_authority: Pubkey,
vault_authority: Pubkey,
number_of_shares: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(destination, false),
AccountMeta::new(fraction_treasury, false),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(transfer_authority, false),
AccountMeta::new_readonly(vault_authority, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: VaultInstruction::WithdrawSharesFromTreasury(NumberOfShareArgs { number_of_shares })
.try_to_vec()
.unwrap(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn create_add_shares_instruction(
program_id: Pubkey,
source: Pubkey,
fraction_treasury: Pubkey,
vault: Pubkey,
transfer_authority: Pubkey,
vault_authority: Pubkey,
number_of_shares: u64,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(source, false),
AccountMeta::new(fraction_treasury, false),
AccountMeta::new_readonly(vault, false),
AccountMeta::new_readonly(transfer_authority, true),
AccountMeta::new_readonly(vault_authority, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: VaultInstruction::AddSharesToTreasury(NumberOfShareArgs { number_of_shares })
.try_to_vec()
.unwrap(),
}
}

View File

@ -0,0 +1,12 @@
//! A Token Fraction program for the Solana blockchain.
pub mod entrypoint;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
pub mod utils;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
solana_program::declare_id!("vau1zxA2LbssAUEF7Gpw91zMM1LvXrvpzJtmZ58rPsn");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,122 @@
use {
crate::utils::try_from_slice_checked,
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey},
};
/// prefix used for PDAs to avoid certain collision attacks (https://en.wikipedia.org/wiki/Collision_attack#Chosen-prefix_collision_attack)
pub const PREFIX: &str = "vault";
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub enum Key {
Uninitialized,
SafetyDepositBoxV1,
ExternalAccountKeyV1,
VaultV1,
}
pub const MAX_SAFETY_DEPOSIT_SIZE: usize = 1 + 32 + 32 + 32 + 1;
pub const MAX_VAULT_SIZE: usize = 1 + 32 + 32 + 32 + 32 + 1 + 32 + 1 + 32 + 1 + 1 + 8;
pub const MAX_EXTERNAL_ACCOUNT_SIZE: usize = 1 + 8 + 32 + 1;
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
pub enum VaultState {
Inactive,
Active,
Combined,
Deactivated,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize)]
pub struct Vault {
pub key: Key,
/// Store token program used
pub token_program: Pubkey,
/// Mint that produces the fractional shares
pub fraction_mint: Pubkey,
/// Authority who can make changes to the vault
pub authority: Pubkey,
/// treasury where fractional shares are held for redemption by authority
pub fraction_treasury: Pubkey,
/// treasury where monies are held for fractional share holders to redeem(burn) shares once buyout is made
pub redeem_treasury: Pubkey,
/// Can authority mint more shares from fraction_mint after activation
pub allow_further_share_creation: bool,
/// Must point at an ExternalPriceAccount, which gives permission and price for buyout.
pub pricing_lookup_address: Pubkey,
/// In inactive state, we use this to set the order key on Safety Deposit Boxes being added and
/// then we increment it and save so the next safety deposit box gets the next number.
/// In the Combined state during token redemption by authority, we use it as a decrementing counter each time
/// The authority of the vault withdrawals a Safety Deposit contents to count down how many
/// are left to be opened and closed down. Once this hits zero, and the fraction mint has zero shares,
/// then we can deactivate the vault.
pub token_type_count: u8,
pub state: VaultState,
/// Once combination happens, we copy price per share to vault so that if something nefarious happens
/// to external price account, like price change, we still have the math 'saved' for use in our calcs
pub locked_price_per_share: u64,
}
impl Vault {
pub fn from_account_info(a: &AccountInfo) -> Result<Vault, ProgramError> {
let vt: Vault = try_from_slice_checked(&a.data.borrow_mut(), Key::VaultV1, MAX_VAULT_SIZE)?;
Ok(vt)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize)]
pub struct SafetyDepositBox {
// Please note if you change this struct, be careful as we read directly off it
// in Metaplex to avoid serialization costs...
/// Each token type in a vault has it's own box that contains it's mint and a look-back
pub key: Key,
/// Key pointing to the parent vault
pub vault: Pubkey,
/// This particular token's mint
pub token_mint: Pubkey,
/// Account that stores the tokens under management
pub store: Pubkey,
/// the order in the array of registries
pub order: u8,
}
impl SafetyDepositBox {
pub fn from_account_info(a: &AccountInfo) -> Result<SafetyDepositBox, ProgramError> {
let sd: SafetyDepositBox = try_from_slice_checked(
&a.data.borrow_mut(),
Key::SafetyDepositBoxV1,
MAX_SAFETY_DEPOSIT_SIZE,
)?;
Ok(sd)
}
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize)]
pub struct ExternalPriceAccount {
pub key: Key,
pub price_per_share: u64,
/// Mint of the currency we are pricing the shares against, should be same as redeem_treasury.
/// Most likely will be USDC mint most of the time.
pub price_mint: Pubkey,
/// Whether or not combination has been allowed for this vault.
pub allowed_to_combine: bool,
}
impl ExternalPriceAccount {
pub fn from_account_info(a: &AccountInfo) -> Result<ExternalPriceAccount, ProgramError> {
let sd: ExternalPriceAccount = try_from_slice_checked(
&a.data.borrow_mut(),
Key::ExternalAccountKeyV1,
MAX_EXTERNAL_ACCOUNT_SIZE,
)?;
Ok(sd)
}
}

View File

@ -0,0 +1,264 @@
use {
crate::{
error::VaultError,
state::{Key, Vault},
},
borsh::BorshDeserialize,
solana_program::{
account_info::AccountInfo,
borsh::try_from_slice_unchecked,
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
},
std::convert::TryInto,
};
/// assert initialized account
pub fn assert_initialized<T: Pack + IsInitialized>(
account_info: &AccountInfo,
) -> Result<T, ProgramError> {
let account: T = T::unpack_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(VaultError::Uninitialized.into())
} else {
Ok(account)
}
}
pub fn assert_rent_exempt(rent: &Rent, account_info: &AccountInfo) -> ProgramResult {
if !rent.is_exempt(account_info.lamports(), account_info.data_len()) {
Err(VaultError::NotRentExempt.into())
} else {
Ok(())
}
}
pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> ProgramResult {
if account.owner != owner {
Err(VaultError::IncorrectOwner.into())
} else {
Ok(())
}
}
pub fn assert_token_matching(vault: &Vault, token: &AccountInfo) -> ProgramResult {
if vault.token_program != *token.key {
return Err(VaultError::TokenProgramProvidedDoesNotMatchVault.into());
}
Ok(())
}
pub fn assert_vault_authority_correct(
vault: &Vault,
vault_authority_info: &AccountInfo,
) -> ProgramResult {
if !vault_authority_info.is_signer {
return Err(VaultError::AuthorityIsNotSigner.into());
}
if *vault_authority_info.key != vault.authority {
return Err(VaultError::AuthorityDoesNotMatch.into());
}
Ok(())
}
pub fn assert_token_program_matches_package(token_program_info: &AccountInfo) -> ProgramResult {
if *token_program_info.key != spl_token::id() {
return Err(VaultError::InvalidTokenProgram.into());
}
Ok(())
}
/// Create account almost from scratch, lifted from
/// https://github.com/solana-labs/solana-program-library/blob/7d4873c61721aca25464d42cc5ef651a7923ca79/associated-token-account/program/src/processor.rs#L51-L98
#[inline(always)]
pub fn create_or_allocate_account_raw<'a>(
program_id: Pubkey,
new_account_info: &AccountInfo<'a>,
rent_sysvar_info: &AccountInfo<'a>,
system_program_info: &AccountInfo<'a>,
payer_info: &AccountInfo<'a>,
size: usize,
signer_seeds: &[&[u8]],
) -> Result<(), ProgramError> {
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let required_lamports = rent
.minimum_balance(size)
.max(1)
.saturating_sub(new_account_info.lamports());
if required_lamports > 0 {
msg!("Transfer {} lamports to the new account", required_lamports);
invoke(
&system_instruction::transfer(&payer_info.key, new_account_info.key, required_lamports),
&[
payer_info.clone(),
new_account_info.clone(),
system_program_info.clone(),
],
)?;
}
msg!("Allocate space for the account");
invoke_signed(
&system_instruction::allocate(new_account_info.key, size.try_into().unwrap()),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
msg!("Assign the account to the owning program");
invoke_signed(
&system_instruction::assign(new_account_info.key, &program_id),
&[new_account_info.clone(), system_program_info.clone()],
&[&signer_seeds],
)?;
msg!("Completed assignation!");
Ok(())
}
/// Issue a spl_token `Transfer` instruction.
#[inline(always)]
pub fn spl_token_transfer(params: TokenTransferParams<'_, '_>) -> ProgramResult {
let TokenTransferParams {
source,
destination,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let result = invoke_signed(
&spl_token::instruction::transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[source, destination, authority, token_program],
&[authority_signer_seeds],
);
result.map_err(|_| VaultError::TokenTransferFailed.into())
}
/// Issue a spl_token `MintTo` instruction.
pub fn spl_token_mint_to(params: TokenMintToParams<'_, '_>) -> ProgramResult {
let TokenMintToParams {
mint,
destination,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let result = invoke_signed(
&spl_token::instruction::mint_to(
token_program.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[mint, destination, authority, token_program],
&[authority_signer_seeds],
);
result.map_err(|_| VaultError::TokenMintToFailed.into())
}
/// Issue a spl_token `Burn` instruction.
#[inline(always)]
pub fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult {
let TokenBurnParams {
mint,
source,
authority,
token_program,
amount,
authority_signer_seeds,
} = params;
let result = invoke_signed(
&spl_token::instruction::burn(
token_program.key,
source.key,
mint.key,
authority.key,
&[],
amount,
)?,
&[source, mint, authority, token_program],
&[authority_signer_seeds],
);
result.map_err(|_| VaultError::TokenBurnFailed.into())
}
///TokenTransferParams
pub struct TokenTransferParams<'a: 'b, 'b> {
/// source
pub source: AccountInfo<'a>,
/// destination
pub destination: AccountInfo<'a>,
/// amount
pub amount: u64,
/// authority
pub authority: AccountInfo<'a>,
/// authority_signer_seeds
pub authority_signer_seeds: &'b [&'b [u8]],
/// token_program
pub token_program: AccountInfo<'a>,
}
/// TokenMintToParams
pub struct TokenMintToParams<'a: 'b, 'b> {
/// mint
pub mint: AccountInfo<'a>,
/// destination
pub destination: AccountInfo<'a>,
/// amount
pub amount: u64,
/// authority
pub authority: AccountInfo<'a>,
/// authority_signer_seeds
pub authority_signer_seeds: &'b [&'b [u8]],
/// token_program
pub token_program: AccountInfo<'a>,
}
/// TokenBurnParams
pub struct TokenBurnParams<'a: 'b, 'b> {
/// mint
pub mint: AccountInfo<'a>,
/// source
pub source: AccountInfo<'a>,
/// amount
pub amount: u64,
/// authority
pub authority: AccountInfo<'a>,
/// authority_signer_seeds
pub authority_signer_seeds: &'b [&'b [u8]],
/// token_program
pub token_program: AccountInfo<'a>,
}
pub fn try_from_slice_checked<T: BorshDeserialize>(
data: &[u8],
data_type: Key,
data_size: usize,
) -> Result<T, ProgramError> {
if (data[0] != data_type as u8 && data[0] != Key::Uninitialized as u8)
|| data.len() != data_size
{
return Err(VaultError::DataTypeMismatch.into());
}
let result: T = try_from_slice_unchecked(data)?;
Ok(result)
}

View File

@ -0,0 +1,21 @@
[package]
name = "spl-token-vault-test-client"
version = "0.1.0"
description = "Metaplex Library Fraction Test Client"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
publish = false
[dependencies]
solana-client = "1.6.10"
solana-program = "1.6.10"
solana-sdk = "1.6.10"
bincode = "1.3.2"
borsh = "0.8.2"
clap = "2.33.3"
solana-clap-utils = "1.6"
solana-cli-config = "1.6"
spl-token-vault = { path = "../program", features = [ "no-entrypoint" ] }
spl-token = { version="3.1.1", features = [ "no-entrypoint" ] }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
#
# Updates the solana version in all the SPL crates
#
solana_ver=$1
if [[ -z $solana_ver ]]; then
echo "Usage: $0 <new-solana-version>"
exit 1
fi
cd "$(dirname "$0")"
declare tomls=()
while IFS='' read -r line; do tomls+=("$line"); done < <(find . -name Cargo.toml)
crates=(
solana-account-decoder
solana-banks-client
solana-banks-server
solana-bpf-loader-program
solana-clap-utils
solana-cli-config
solana-cli-output
solana-client
solana-core
solana-logger
solana-notifier
solana-program
solana-program-test
solana-remote-wallet
solana-runtime
solana-sdk
solana-stake-program
solana-transaction-status
solana-vote-program
)
set -x
for crate in "${crates[@]}"; do
sed -i -e "s#\(${crate} = \"\)\(=\?\).*\(\"\)#\1\2$solana_ver\3#g" "${tomls[@]}"
done