Participation nfts in fl (#537)

* fix some boolean checks and add check for 0 sol

* Fixes for eligible count, not having enough sol, and couple other things like rug verbiage

* Make punching faster in cli and make rug screen better

* Added try catches for errors around txns

* Work in progress on NFTs

* Still wip

* continuation

* still working

* WIP on stuff

* Assert ownership of ATAs

* Continue adding participation nfts

* WIP on participation

* Fix to lottery call

* Looks like participation is getting close, just need to test the negative.

* Participation complete
This commit is contained in:
Jordan Prince 2021-10-01 19:45:49 -05:00 committed by GitHub
parent 9ca7b7bec9
commit 446470ae27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1015 additions and 49 deletions

View File

@ -21,6 +21,10 @@ import {
getFairLaunchTicketSeqLookup,
getFairLaunchLotteryBitmap,
getMetadata,
getParticipationMint,
getParticipationToken,
getMasterEdition,
getEditionMarkPda,
} from './helpers/accounts';
import { chunks, getMultipleAccounts, sleep } from './helpers/various';
import { createAssociatedTokenAccountInstruction } from './helpers/instructions';
@ -829,6 +833,214 @@ async function adjustTicket({
);
}
program
.command('update_participation_nft')
.option(
'-e, --env <string>',
'Solana cluster env name',
'devnet', //mainnet-beta, testnet, devnet
)
.option(
'-k, --keypair <path>',
`Solana wallet location`,
'--keypair not provided',
)
.option('-f, --fair-launch <string>', 'fair launch id')
.option('-n, --name <string>', 'name')
.option('-s, --symbol <string>', 'symbol')
.option('-u, --uri <string>', 'uri')
.option(
'-sfbp, --seller-fee-basis-points <string>',
'seller fee basis points',
)
.option(
'-m, --participation-modulo <string>',
'1 if everybody gets it, 4 if only 1 in 4 get it, etc',
)
.option(
'-c, --creators <string>',
'comma separated creator wallets like wallet1,73,true,wallet2,27,false where its wallet, then share, then verified true/false',
)
.option('-nm, --is_not_mutable', 'is not mutable')
.action(async (_, cmd) => {
const {
env,
keypair,
fairLaunch,
name,
symbol,
uri,
sellerFeeBasisPoints,
creators,
isNotMutable,
participationModulo,
} = cmd.opts();
const sellerFeeBasisPointsNumber = parseInt(sellerFeeBasisPoints);
const participationModuloNumber = parseInt(participationModulo);
const creatorsListPre = creators ? creators.split(',') : [];
const creatorsList = [];
for (let i = 0; i < creatorsListPre.length; i += 3) {
creatorsList.push({
address: new anchor.web3.PublicKey(creatorsListPre[i]),
share: parseInt(creatorsListPre[i + 1]),
verified: creatorsListPre[i + 2] == 'true' ? true : false,
});
}
const isMutableBool = isNotMutable ? false : true;
const walletKeyPair = loadWalletKey(keypair);
const anchorProgram = await loadFairLaunchProgram(walletKeyPair, env);
const fairLaunchKey = new anchor.web3.PublicKey(fairLaunch);
const fairLaunchObj = await anchorProgram.account.fairLaunch.fetch(
fairLaunchKey,
);
const participationMint = (
await getParticipationMint(
//@ts-ignore
fairLaunchObj.authority,
//@ts-ignore
fairLaunchObj.data.uuid,
)
)[0];
await anchorProgram.rpc.updateParticipationNft(
participationModuloNumber,
{
name,
symbol,
uri,
sellerFeeBasisPoints: sellerFeeBasisPointsNumber,
creators: creatorsList,
isMutable: isMutableBool,
},
{
accounts: {
fairLaunch: fairLaunchKey,
authority: walletKeyPair.publicKey,
//@ts-ignore
metadata: await getMetadata(participationMint),
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
},
},
);
console.log('Update participation metadata.');
});
program
.command('set_participation_nft')
.option(
'-e, --env <string>',
'Solana cluster env name',
'devnet', //mainnet-beta, testnet, devnet
)
.option(
'-k, --keypair <path>',
`Solana wallet location`,
'--keypair not provided',
)
.option('-f, --fair-launch <string>', 'fair launch id')
.option('-n, --name <string>', 'name')
.option('-s, --symbol <string>', 'symbol')
.option('-u, --uri <string>', 'uri')
.option(
'-sfbp, --seller-fee-basis-points <string>',
'seller fee basis points',
)
.option(
'-m, --participation-modulo <string>',
'1 if everybody gets it, 4 if only 1 in 4 get it, etc',
)
.option(
'-c, --creators <string>',
'comma separated creator wallets like wallet1,73,true,wallet2,27,false where its wallet, then share, then verified true/false',
)
.option('-nm, --is_not_mutable', 'is not mutable')
.action(async (_, cmd) => {
const {
env,
keypair,
fairLaunch,
name,
symbol,
uri,
sellerFeeBasisPoints,
creators,
isNotMutable,
participationModulo,
} = cmd.opts();
const sellerFeeBasisPointsNumber = parseInt(sellerFeeBasisPoints);
const participationModuloNumber = parseInt(participationModulo);
const creatorsListPre = creators ? creators.split(',') : [];
const creatorsList = [];
for (let i = 0; i < creatorsListPre.length; i += 3) {
creatorsList.push({
address: new anchor.web3.PublicKey(creatorsListPre[i]),
share: parseInt(creatorsListPre[i + 1]),
verified: creatorsListPre[i + 2] == 'true' ? true : false,
});
}
const isMutableBool = isNotMutable ? false : true;
const walletKeyPair = loadWalletKey(keypair);
const anchorProgram = await loadFairLaunchProgram(walletKeyPair, env);
const fairLaunchKey = new anchor.web3.PublicKey(fairLaunch);
const fairLaunchObj = await anchorProgram.account.fairLaunch.fetch(
fairLaunchKey,
);
const [participationMint, mintBump] = await getParticipationMint(
//@ts-ignore
fairLaunchObj.authority,
//@ts-ignore
fairLaunchObj.data.uuid,
);
const [participationTokenAccount, tokenBump] = await getParticipationToken(
//@ts-ignore
fairLaunchObj.authority,
//@ts-ignore
fairLaunchObj.data.uuid,
);
await anchorProgram.rpc.setParticipationNft(
mintBump,
tokenBump,
participationModuloNumber,
{
name,
symbol,
uri,
sellerFeeBasisPoints: sellerFeeBasisPointsNumber,
creators: creatorsList,
isMutable: isMutableBool,
},
{
accounts: {
fairLaunch: fairLaunchKey,
authority: walletKeyPair.publicKey,
payer: walletKeyPair.publicKey,
participationMint,
participationTokenAccount,
//@ts-ignore
metadata: await getMetadata(participationMint),
//@ts-ignore
masterEdition: await getMasterEdition(participationMint),
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
},
},
);
console.log('Set participation metadata.');
});
program
.command('set_token_metadata')
.option(
@ -1116,6 +1328,46 @@ program
async allIndexesInSlice => {
for (let i = 0; i < allIndexesInSlice.length; i++) {
const ticket = ticketDataFlat[allIndexesInSlice[i]];
if (!ticket.model.gottenParticipation) {
let tries = 0;
let done = false;
while (tries < 3 && !done) {
try {
const nft = await getParticipationNft({
payer: walletKeyPair,
buyer: ticket.model.buyer,
anchorProgram,
fairLaunchTicket: ticket.key,
fairLaunch,
fairLaunchObj,
fairLaunchTicketObj: ticket.model,
});
done = true;
if (nft) {
console.log(
`Got participation nft and placed token in new account ${nft.toBase58()}.`,
);
}
} catch (e) {
if (tries > 3) {
throw e;
} else {
tries++;
}
console.log(e);
console.log(
'Ticket failed to get participation nft, trying one more time',
);
await sleep(1000);
}
}
} else {
console.log(
'Ticket',
ticket.model.buyer.toBase58(),
'already received participation',
);
}
if (ticket.model.state.unpunched) {
if (
ticket.model.amount.toNumber() <
@ -1187,29 +1439,35 @@ program
}
}
let tries = 0;
try {
const buyerTokenAccount = await punchTicket({
payer: walletKeyPair,
puncher: ticket.model.buyer,
anchorProgram,
fairLaunchTicket: ticket.key,
fairLaunch,
fairLaunchLotteryBitmap,
fairLaunchObj,
});
console.log(
`Punched ticket and placed token in new account ${buyerTokenAccount.toBase58()} for buyer `,
allIndexesInSlice[i],
);
} catch (e) {
if (tries > 3) {
throw e;
} else {
tries++;
let done = false;
while (tries < 3 && !done) {
try {
const buyerTokenAccount = await punchTicket({
payer: walletKeyPair,
puncher: ticket.model.buyer,
anchorProgram,
fairLaunchTicket: ticket.key,
fairLaunch,
fairLaunchLotteryBitmap,
fairLaunchObj,
fairLaunchTicketObj: ticket.model,
});
done = true;
console.log(
`Punched ticket and placed token in new account ${buyerTokenAccount.toBase58()}.`,
);
} catch (e) {
if (tries > 3) {
throw e;
} else {
tries++;
}
console.log(e);
console.log(
'Ticket failed to punch, trying one more time',
);
await sleep(1000);
}
console.log('Ticket failed to punch, trying one more time');
await sleep(1000);
}
} else {
console.log(
@ -1254,6 +1512,104 @@ program
);
});
async function getParticipationNft({
buyer,
payer,
anchorProgram,
fairLaunchTicket,
fairLaunch,
fairLaunchObj,
fairLaunchTicketObj,
}: {
buyer: anchor.web3.PublicKey;
anchorProgram: anchor.Program;
payer: anchor.web3.Keypair;
fairLaunchTicket: anchor.web3.PublicKey;
fairLaunch: anchor.web3.PublicKey;
fairLaunchObj: any;
fairLaunchTicketObj: any;
}): Promise<anchor.web3.PublicKey | null> {
if (
fairLaunchObj.participationMint &&
fairLaunchTicketObj.seq.toNumber() % fairLaunchObj.participationModulo == 0
) {
console.log(buyer.toBase58(), 'gets participation token.');
const mint = anchor.web3.Keypair.generate();
let signers = [mint];
const tokenAccount = (
await getParticipationToken(
fairLaunchObj.authority,
fairLaunchObj.data.uuid,
)
)[0];
const buyerTokenNft = (await getAtaForMint(mint.publicKey, buyer))[0];
let instructions = [
anchor.web3.SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint.publicKey,
space: MintLayout.span,
lamports:
await anchorProgram.provider.connection.getMinimumBalanceForRentExemption(
MintLayout.span,
),
programId: TOKEN_PROGRAM_ID,
}),
Token.createInitMintInstruction(
TOKEN_PROGRAM_ID,
mint.publicKey,
0,
payer.publicKey,
payer.publicKey,
),
createAssociatedTokenAccountInstruction(
buyerTokenNft,
payer.publicKey,
buyer,
mint.publicKey,
),
Token.createMintToInstruction(
TOKEN_PROGRAM_ID,
mint.publicKey,
buyerTokenNft,
payer.publicKey,
[],
1,
),
];
await anchorProgram.rpc.mintParticipationNft({
accounts: {
fairLaunch,
fairLaunchTicket,
payer: payer.publicKey,
participationMint: fairLaunchObj.participationMint,
participationTokenAccount: tokenAccount,
buyer,
buyerNftTokenAccount: buyerTokenNft,
newMetadata: await getMetadata(mint.publicKey),
newEdition: await getMasterEdition(mint.publicKey),
newMint: mint.publicKey,
newMintAuthority: payer.publicKey,
metadata: await getMetadata(fairLaunchObj.participationMint),
masterEdition: await getMasterEdition(fairLaunchObj.participationMint),
editionMarkPda: await getEditionMarkPda(
fairLaunchObj.participationMint,
fairLaunchTicketObj.seq.toNumber(),
),
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
instructions,
signers,
});
return buyerTokenNft;
} else {
console.log(buyer.toBase58(), 'doesnt get participation token.');
return null;
}
}
async function punchTicket({
puncher,
payer,
@ -1262,6 +1618,7 @@ async function punchTicket({
fairLaunch,
fairLaunchLotteryBitmap,
fairLaunchObj,
fairLaunchTicketObj,
}: {
puncher: anchor.web3.PublicKey;
anchorProgram: anchor.Program;
@ -1270,6 +1627,7 @@ async function punchTicket({
fairLaunch: anchor.web3.PublicKey;
fairLaunchLotteryBitmap: anchor.web3.PublicKey;
fairLaunchObj: any;
fairLaunchTicketObj: any;
}): Promise<anchor.web3.PublicKey> {
const buyerTokenAccount = (
await getAtaForMint(
@ -1369,28 +1727,68 @@ program
}
let tries = 0;
try {
const buyerTokenAccount = await punchTicket({
puncher: walletKeyPair.publicKey,
payer: walletKeyPair,
anchorProgram,
fairLaunchTicket,
fairLaunch,
fairLaunchLotteryBitmap,
fairLaunchObj,
});
let done = false;
//@ts-ignore
if (!ticket.gottenParticipation) {
while (tries < 3 && !done) {
try {
const nft = await getParticipationNft({
buyer: walletKeyPair.publicKey,
payer: walletKeyPair,
anchorProgram,
fairLaunchTicket,
fairLaunch,
fairLaunchObj,
fairLaunchTicketObj: ticket,
});
done = true;
console.log(
`Punched ticket and placed token in new account ${buyerTokenAccount.toBase58()}.`,
);
} catch (e) {
if (tries > 3) {
throw e;
} else {
tries++;
if (nft) {
console.log(
`Punched participation NFT and placed token in new account ${nft.toBase58()}.`,
);
}
} catch (e) {
if (tries > 3) {
throw e;
} else {
tries++;
}
console.log('Ticket failed to punch, trying one more time');
await sleep(1000);
}
}
} else {
console.log('Already got participation');
}
tries = 0;
done = false;
while (tries < 3 && !done) {
try {
const buyerTokenAccount = await punchTicket({
puncher: walletKeyPair.publicKey,
payer: walletKeyPair,
anchorProgram,
fairLaunchTicket,
fairLaunch,
fairLaunchLotteryBitmap,
fairLaunchObj,
fairLaunchTicketObj: ticket,
});
done = true;
console.log(
`Punched ticket and placed token in new account ${buyerTokenAccount.toBase58()}.`,
);
} catch (e) {
if (tries > 3) {
throw e;
} else {
tries++;
}
console.log('Ticket failed to punch, trying one more time');
await sleep(1000);
}
console.log('Ticket failed to punch, trying one more time');
await sleep(1000);
}
});
@ -2040,6 +2438,12 @@ program
//@ts-ignore
console.log('Treasury Mint', fairLaunchObj.treasuryMint?.toBase58());
//@ts-ignore
console.log(
'Participation Mint',
//@ts-ignore
fairLaunchObj.participationMint?.toBase58(),
);
//@ts-ignore
console.log('Authority', fairLaunchObj.authority.toBase58());
//@ts-ignore
console.log('Bump', fairLaunchObj.bump);
@ -2048,6 +2452,8 @@ program
//@ts-ignore
console.log('Token Mint Bump', fairLaunchObj.tokenMintBump);
//@ts-ignore
console.log('Participation Modulo', fairLaunchObj.participationModulo);
//@ts-ignore
if (fairLaunchObj.data.antiRugSetting) {
console.log('Anti-Rug Settings:');
//@ts-ignore

View File

@ -163,6 +163,39 @@ export const getAtaForMint = async (
);
};
export const getParticipationMint = async (
authority: anchor.web3.PublicKey,
uuid: string,
): Promise<[anchor.web3.PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from('fair_launch'),
authority.toBuffer(),
Buffer.from('mint'),
Buffer.from(uuid),
Buffer.from('participation'),
],
FAIR_LAUNCH_PROGRAM_ID,
);
};
export const getParticipationToken = async (
authority: anchor.web3.PublicKey,
uuid: string,
): Promise<[anchor.web3.PublicKey, number]> => {
return await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from('fair_launch'),
authority.toBuffer(),
Buffer.from('mint'),
Buffer.from(uuid),
Buffer.from('participation'),
Buffer.from('account'),
],
FAIR_LAUNCH_PROGRAM_ID,
);
};
export const getTreasury = async (
tokenMint: anchor.web3.PublicKey,
): Promise<[anchor.web3.PublicKey, number]> => {
@ -203,6 +236,25 @@ export const getMasterEdition = async (
)[0];
};
export const getEditionMarkPda = async (
mint: anchor.web3.PublicKey,
edition: number,
): Promise<anchor.web3.PublicKey> => {
const editionNumber = Math.floor(edition / 248);
return (
await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from('metadata'),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
Buffer.from('edition'),
Buffer.from(editionNumber.toString()),
],
TOKEN_METADATA_PROGRAM_ID,
)
)[0];
};
export function loadWalletKey(keypair): Keypair {
if (!keypair || keypair == '') {
throw new Error('Keypair is required!');

View File

@ -20,7 +20,7 @@ export const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
);
export const FAIR_LAUNCH_PROGRAM_ID = new PublicKey(
'faircnAB9k59Y4TXmLabBULeuTLgV7TkGMGNkjnA15j',
'7HmfyvWK7LDohUL9TDAuGv9VFZHUce1SgYMkwti1xWwF',
);
export const CONFIG_ARRAY_START =
32 + // authority

View File

@ -18,14 +18,22 @@ use {
AnchorDeserialize, AnchorSerialize,
},
anchor_spl::token::Mint,
spl_token::{instruction::initialize_account2, state::Account},
spl_token_metadata::instruction::{create_metadata_accounts, update_metadata_accounts},
spl_token::{
instruction::{initialize_account2, mint_to},
state::Account,
},
spl_token_metadata::instruction::{
create_master_edition, create_metadata_accounts,
mint_new_edition_from_master_edition_via_token, update_metadata_accounts,
},
};
pub const PREFIX: &str = "fair_launch";
pub const TREASURY: &str = "treasury";
pub const MINT: &str = "mint";
pub const LOTTERY: &str = "lottery";
pub const PARTICIPATION: &str = "participation";
pub const ACCOUNT: &str = "account";
pub const MAX_GRANULARITY: u64 = 100;
#[program]
@ -75,6 +83,10 @@ pub mod fair_launch {
fair_launch.treasury_mint = Some(*treasury_mint_info.key);
if treasury_info.data_len() > 0 {
return Err(ErrorCode::TreasuryAlreadyExists.into());
}
// make the treasury token account
create_or_allocate_account_raw(
@ -1110,6 +1122,397 @@ pub mod fair_launch {
Ok(())
}
pub fn set_participation_nft<'info>(
ctx: Context<'_, '_, '_, 'info, SetParticipationNFT<'info>>,
participation_mint_bump: u8,
participation_token_bump: u8,
participation_modulo: u8,
data: TokenMetadata,
) -> ProgramResult {
let fair_launch = &mut ctx.accounts.fair_launch;
let participation_mint = &ctx.accounts.participation_mint;
let participation_token_info = &ctx.accounts.participation_token_account;
let participation_mint_info = participation_mint.to_account_info();
let token_program = &ctx.accounts.token_program;
if token_program.key != &spl_token::id() {
return Err(ErrorCode::InvalidTokenProgram.into());
}
fair_launch.participation_mint_bump = participation_mint_bump;
fair_launch.participation_token_bump = participation_token_bump;
fair_launch.participation_mint = Some(participation_mint.key());
fair_launch.participation_modulo = participation_modulo;
if participation_modulo == 0 {
return Err(ErrorCode::InvalidParticipationModulo.into());
}
// make the token account
let authority_seeds = [
PREFIX.as_bytes(),
fair_launch.token_mint.as_ref(),
&[fair_launch.bump],
];
let mut creators: Vec<spl_token_metadata::state::Creator> =
vec![spl_token_metadata::state::Creator {
address: fair_launch.key(),
verified: true,
share: 0,
}];
if let Some(cre) = &data.creators {
for c in cre {
creators.push(spl_token_metadata::state::Creator {
address: c.address,
verified: c.verified,
share: c.share,
});
}
}
assert_owned_by(&participation_mint_info, &spl_token::id())?;
assert_derivation(
&ctx.program_id,
participation_token_info,
&[
PREFIX.as_bytes(),
fair_launch.authority.as_ref(),
MINT.as_bytes(),
fair_launch.data.uuid.as_bytes(),
PARTICIPATION.as_bytes(),
ACCOUNT.as_bytes(),
],
)?;
let signer_seeds = &[
PREFIX.as_bytes(),
fair_launch.authority.as_ref(),
MINT.as_bytes(),
fair_launch.data.uuid.as_bytes(),
PARTICIPATION.as_bytes(),
ACCOUNT.as_bytes(),
&[participation_token_bump],
];
if participation_token_info.data_len() > 0 {
return Err(ErrorCode::ParticipationTokenAccountAlreadyExists.into());
};
msg!("Allocating token account");
create_or_allocate_account_raw(
*ctx.accounts.token_program.key,
participation_token_info,
&ctx.accounts.rent.to_account_info(),
&ctx.accounts.system_program,
&ctx.accounts.payer,
Account::LEN,
signer_seeds,
)?;
invoke_signed(
&initialize_account2(
&ctx.accounts.token_program.key,
participation_token_info.key,
participation_mint_info.key,
&fair_launch.key(),
)
.unwrap(),
&[
ctx.accounts.token_program.clone(),
participation_token_info.clone(),
fair_launch.to_account_info(),
participation_mint_info.clone(),
ctx.accounts.rent.to_account_info(),
],
&[signer_seeds],
)?;
msg!("Minting token");
invoke_signed(
&mint_to(
&ctx.accounts.token_program.key,
participation_mint_info.key,
participation_token_info.key,
&fair_launch.key(),
&[],
1,
)
.unwrap(),
&[
ctx.accounts.token_program.clone(),
participation_token_info.clone(),
fair_launch.to_account_info(),
participation_mint_info.clone(),
ctx.accounts.rent.to_account_info(),
],
&[&authority_seeds],
)?;
msg!("Creating metadata");
let metadata_infos = vec![
ctx.accounts.metadata.clone(),
participation_mint_info.clone(),
ctx.accounts.payer.clone(),
ctx.accounts.token_metadata_program.clone(),
ctx.accounts.token_program.clone(),
ctx.accounts.system_program.clone(),
ctx.accounts.rent.to_account_info().clone(),
fair_launch.to_account_info(),
];
let master_edition_infos = vec![
ctx.accounts.master_edition.clone(),
participation_mint_info.clone(),
fair_launch.to_account_info(),
ctx.accounts.payer.clone(),
ctx.accounts.metadata.clone(),
ctx.accounts.token_metadata_program.clone(),
ctx.accounts.token_program.clone(),
ctx.accounts.system_program.clone(),
ctx.accounts.rent.to_account_info().clone(),
];
invoke_signed(
&create_metadata_accounts(
*ctx.accounts.token_metadata_program.key,
*ctx.accounts.metadata.key,
*participation_mint_info.key,
fair_launch.key(),
*ctx.accounts.payer.key,
fair_launch.key(),
data.name,
data.symbol.clone(),
data.uri,
Some(creators),
data.seller_fee_basis_points,
false,
data.is_mutable,
),
metadata_infos.as_slice(),
&[&authority_seeds],
)?;
msg!("Creating master edition");
invoke_signed(
&create_master_edition(
*ctx.accounts.token_metadata_program.key,
*ctx.accounts.master_edition.key,
*participation_mint_info.key,
fair_launch.key(),
fair_launch.key(),
*ctx.accounts.metadata.key,
*ctx.accounts.payer.key,
None,
),
master_edition_infos.as_slice(),
&[&authority_seeds],
)?;
Ok(())
}
pub fn update_participation_nft<'info>(
ctx: Context<'_, '_, '_, 'info, UpdateParticipationNFT<'info>>,
participation_modulo: u8,
data: TokenMetadata,
) -> ProgramResult {
let fair_launch = &mut ctx.accounts.fair_launch;
fair_launch.participation_modulo = participation_modulo;
let token_program = &ctx.accounts.token_program;
if token_program.key != &spl_token::id() {
return Err(ErrorCode::InvalidTokenProgram.into());
}
if participation_modulo == 0 {
return Err(ErrorCode::InvalidParticipationModulo.into());
}
let authority_seeds = [
PREFIX.as_bytes(),
fair_launch.token_mint.as_ref(),
&[fair_launch.bump],
];
let mut creators: Vec<spl_token_metadata::state::Creator> =
vec![spl_token_metadata::state::Creator {
address: fair_launch.key(),
verified: true,
share: 0,
}];
if let Some(cre) = &data.creators {
for c in cre {
creators.push(spl_token_metadata::state::Creator {
address: c.address,
verified: c.verified,
share: c.share,
});
}
}
let update_infos = vec![
ctx.accounts.token_metadata_program.clone(),
ctx.accounts.token_program.clone(),
ctx.accounts.metadata.clone(),
fair_launch.to_account_info().clone(),
];
msg!("Updating metadata");
invoke_signed(
&update_metadata_accounts(
*ctx.accounts.token_metadata_program.key,
*ctx.accounts.metadata.key,
fair_launch.key(),
None,
Some(spl_token_metadata::state::Data {
name: data.name,
symbol: data.symbol,
uri: data.uri,
creators: Some(creators),
seller_fee_basis_points: data.seller_fee_basis_points,
}),
None,
),
update_infos.as_slice(),
&[&authority_seeds],
)?;
Ok(())
}
pub fn mint_participation_nft<'info>(
ctx: Context<'_, '_, '_, 'info, MintParticipationNFT<'info>>,
) -> ProgramResult {
let fair_launch = &ctx.accounts.fair_launch;
let fair_launch_ticket = &mut ctx.accounts.fair_launch_ticket;
let buyer = &ctx.accounts.buyer;
let buyer_nft_token_account_info = &ctx.accounts.buyer_nft_token_account;
let token_program = &ctx.accounts.token_program;
if token_program.key != &spl_token::id() {
return Err(ErrorCode::InvalidTokenProgram.into());
}
if fair_launch.participation_modulo == 0 {
return Err(ErrorCode::InvalidParticipationModulo.into());
}
if fair_launch_ticket.gotten_participation {
return Err(ErrorCode::AlreadyMintedParticipation.into());
}
if let Some(val) = fair_launch_ticket
.seq
.checked_rem(fair_launch.participation_modulo as u64)
{
msg!("Val is {}", val);
if val != 0 {
return Err(ErrorCode::NotEligibleForParticipation.into());
}
} else {
return Err(ErrorCode::NotEligibleForParticipation.into());
}
fair_launch_ticket.gotten_participation = true;
let authority_seeds = [
PREFIX.as_bytes(),
fair_launch.token_mint.as_ref(),
&[fair_launch.bump],
];
let buyer_nft_token_account: Account = assert_initialized(&buyer_nft_token_account_info)?;
if buyer_nft_token_account.mint != *ctx.accounts.new_mint.key {
return Err(ErrorCode::ParticipationMintMismatch.into());
}
if buyer_nft_token_account.delegate.is_some() {
return Err(ErrorCode::AccountShouldHaveNoDelegates.into());
}
if buyer_nft_token_account.owner != *buyer.key {
return Err(ErrorCode::AccountOwnerShouldBeBuyer.into());
}
assert_owned_by(buyer_nft_token_account_info, &token_program.key)?;
// assert is an ATA
assert_derivation(
&spl_associated_token_account::id(),
buyer_nft_token_account_info,
&[
buyer.key.as_ref(),
token_program.key.as_ref(),
&ctx.accounts.new_mint.key.as_ref(),
],
)?;
let edition_infos = vec![
ctx.accounts.metadata.clone(),
ctx.accounts.new_metadata.clone(),
ctx.accounts.new_edition.clone(),
ctx.accounts.master_edition.clone(),
ctx.accounts.new_mint.clone(),
ctx.accounts.participation_token_account.clone(),
ctx.accounts.participation_mint.to_account_info(),
ctx.accounts.payer.clone(),
ctx.accounts.token_metadata_program.clone(),
ctx.accounts.token_program.clone(),
ctx.accounts.system_program.clone(),
ctx.accounts.edition_mark_pda.clone(),
ctx.accounts.rent.to_account_info(),
fair_launch.to_account_info(),
];
invoke_signed(
&mint_new_edition_from_master_edition_via_token(
*ctx.accounts.token_metadata_program.key,
*ctx.accounts.new_metadata.key,
*ctx.accounts.new_edition.key,
*ctx.accounts.master_edition.key,
*ctx.accounts.new_mint.key,
*ctx.accounts.payer.key,
*ctx.accounts.payer.key,
fair_launch.key(),
*ctx.accounts.participation_token_account.key,
fair_launch.key(),
*ctx.accounts.metadata.key,
ctx.accounts.participation_mint.key(),
fair_launch_ticket.seq,
),
edition_infos.as_slice(),
&[&authority_seeds],
)?;
invoke_signed(
&update_metadata_accounts(
*ctx.accounts.token_metadata_program.key,
*ctx.accounts.new_metadata.key,
fair_launch.key(),
None,
None,
Some(true),
),
&[
ctx.accounts.token_metadata_program.clone(),
ctx.accounts.new_metadata.clone(),
fair_launch.to_account_info(),
],
&[&authority_seeds],
)?;
Ok(())
}
}
#[derive(Accounts)]
#[instruction(bump: u8, treasury_bump: u8, token_mint_bump: u8, data: FairLaunchData)]
@ -1342,6 +1745,92 @@ pub struct SetTokenMetadata<'info> {
clock: Sysvar<'info, Clock>,
}
#[derive(Accounts)]
#[instruction(participation_mint_bump: u8, participation_token_bump: u8)]
pub struct SetParticipationNFT<'info> {
#[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.token_mint.as_ref()], bump=fair_launch.bump, has_one=authority)]
fair_launch: ProgramAccount<'info, FairLaunch>,
#[account(signer)]
authority: AccountInfo<'info>,
#[account(mut, signer)]
payer: AccountInfo<'info>,
#[account(init, seeds=[PREFIX.as_bytes(), authority.key.as_ref(), MINT.as_bytes(), fair_launch.data.uuid.as_bytes(), PARTICIPATION.as_bytes()], mint::authority=fair_launch, mint::decimals=0, payer=payer, bump=participation_mint_bump)]
participation_mint: CpiAccount<'info, Mint>,
#[account(mut, seeds=[PREFIX.as_bytes(), authority.key.as_ref(), MINT.as_bytes(), fair_launch.data.uuid.as_bytes(), PARTICIPATION.as_bytes(), ACCOUNT.as_bytes()], bump=participation_token_bump)]
participation_token_account: AccountInfo<'info>,
// With the following accounts we aren't using anchor macros because they are CPI'd
// through to token-metadata which will do all the validations we need on them.
#[account(mut)]
metadata: AccountInfo<'info>,
#[account(mut)]
master_edition: AccountInfo<'info>,
#[account(address = spl_token_metadata::id())]
token_metadata_program: AccountInfo<'info>,
#[account(address = spl_token::id())]
token_program: AccountInfo<'info>,
#[account(address = system_program::ID)]
system_program: AccountInfo<'info>,
rent: Sysvar<'info, Rent>,
clock: Sysvar<'info, Clock>,
}
#[derive(Accounts)]
pub struct UpdateParticipationNFT<'info> {
#[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.token_mint.as_ref()], bump=fair_launch.bump, has_one=authority)]
fair_launch: ProgramAccount<'info, FairLaunch>,
#[account(signer)]
authority: AccountInfo<'info>,
// With the following accounts we aren't using anchor macros because they are CPI'd
// through to token-metadata which will do all the validations we need on them.
#[account(mut)]
metadata: AccountInfo<'info>,
#[account(address = spl_token_metadata::id())]
token_metadata_program: AccountInfo<'info>,
#[account(address = spl_token::id())]
token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct MintParticipationNFT<'info> {
#[account(seeds=[PREFIX.as_bytes(), fair_launch.token_mint.as_ref()], bump=fair_launch.bump)]
fair_launch: ProgramAccount<'info, FairLaunch>,
#[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.token_mint.as_ref(), fair_launch_ticket.buyer.as_ref()], bump=fair_launch_ticket.bump, has_one=fair_launch, has_one=buyer)]
fair_launch_ticket: ProgramAccount<'info, FairLaunchTicket>,
#[account(mut, signer)]
payer: AccountInfo<'info>,
#[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.authority.as_ref(), MINT.as_bytes(), fair_launch.data.uuid.as_bytes(), PARTICIPATION.as_bytes()], bump=fair_launch.participation_mint_bump)]
participation_mint: CpiAccount<'info, Mint>,
#[account(mut, seeds=[PREFIX.as_bytes(), fair_launch.authority.as_ref(), MINT.as_bytes(), fair_launch.data.uuid.as_bytes(), PARTICIPATION.as_bytes(), ACCOUNT.as_bytes()], bump=fair_launch.participation_token_bump)]
participation_token_account: AccountInfo<'info>,
buyer: AccountInfo<'info>,
buyer_nft_token_account: AccountInfo<'info>,
// With the following accounts we aren't using anchor macros because they are CPI'd
// through to token-metadata which will do all the validations we need on them.
// This will fail if there is more than one token in existence and we force you to provide
// an ata that must belong to buyer and we check that it has the token from this new_mint.
#[account(mut)]
new_metadata: AccountInfo<'info>,
#[account(mut)]
new_edition: AccountInfo<'info>,
#[account(mut)]
new_mint: AccountInfo<'info>,
#[account(signer)]
new_mint_authority: AccountInfo<'info>,
#[account(mut)]
metadata: AccountInfo<'info>,
#[account(mut)]
master_edition: AccountInfo<'info>,
#[account(mut)]
edition_mark_pda: AccountInfo<'info>,
#[account(address = spl_token_metadata::id())]
token_metadata_program: AccountInfo<'info>,
#[account(address = spl_token::id())]
token_program: AccountInfo<'info>,
#[account(address = system_program::ID)]
system_program: AccountInfo<'info>,
rent: Sysvar<'info, Rent>,
}
pub const FAIR_LAUNCH_LOTTERY_SIZE: usize = 8 + // discriminator
32 + // fair launch
1 + // bump
@ -1379,7 +1868,11 @@ pub const FAIR_LAUNCH_SPACE_VEC_START: usize = 8 + // discriminator
8 + // current_eligible_holders
8 + // current median,
4 + // u32 representing number of amounts in vec so far
100; // padding
1 + // participation modulo (added later)
1 + // participation_mint_bump (added later)
1 + // participation_token_bump (added later)
33 + // participation_mint (added later)
65; // padding
pub const FAIR_LAUNCH_TICKET_SIZE: usize = 8 + // discriminator
32 + // fair launch reverse lookup
@ -1388,7 +1881,8 @@ pub const FAIR_LAUNCH_TICKET_SIZE: usize = 8 + // discriminator
1 + // state
1 + // bump
8 + // seq
50; //padding
1 + // gotten participation
49; //padding
pub const FAIR_LAUNCH_TICKET_SEQ_SIZE: usize = 8 + //discriminator
32 + // fair launch ticket reverse lookup
@ -1477,7 +1971,10 @@ pub struct FairLaunch {
pub current_eligible_holders: u64,
pub current_median: u64,
pub counts_at_each_tick: Vec<u64>,
// Todo add participation fields in the future for participation NFTs
pub participation_modulo: u8,
pub participation_mint_bump: u8,
pub participation_token_bump: u8,
pub participation_mint: Option<Pubkey>,
}
#[account]
@ -1504,6 +2001,7 @@ pub struct FairLaunchTicket {
pub state: FairLaunchTicketState,
pub bump: u8,
pub seq: u64,
pub gotten_participation: bool,
}
#[account]
@ -1624,6 +2122,16 @@ pub enum ErrorCode {
LotteryDurationHasntEndedYet,
#[msg("Fair launch ticket and fair launch key mismatch")]
FairLaunchMismatch,
#[msg("Participation Token Account already exists")]
ParticipationTokenAccountAlreadyExists,
#[msg("Invalid participation modulo")]
InvalidParticipationModulo,
#[msg("Already got participation")]
AlreadyMintedParticipation,
#[msg("Not eligible for participation")]
NotEligibleForParticipation,
#[msg("The mint on this account does not match the participation nft mint")]
ParticipationMintMismatch,
#[msg("Account owner should be buyer")]
AccountOwnerShouldBeBuyer,
#[msg("Account owner should be fair launch authority")]

File diff suppressed because one or more lines are too long