Merge pull request #32 from metaplex-foundation/various-patches
History doesn't repeat itself, but it does rhyme.
This commit is contained in:
commit
b57a335552
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,9 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"auction/program",
|
||||
"metaplex/program",
|
||||
"token-vault/program",
|
||||
"token-metadata/program",
|
||||
]
|
||||
exclude = [
|
||||
]
|
|
@ -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" ] }
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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"]
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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(())
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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");
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -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"]
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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(())
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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");
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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" ] }
|
|
@ -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)
|
||||
}
|
|
@ -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!(),
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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.");
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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"
|
|
@ -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"]
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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(())
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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");
|
|
@ -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(())
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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" ] }
|
|
@ -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!(),
|
||||
}
|
||||
}
|
|
@ -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"]
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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(())
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue