From 822a048a5a7f33781e5a8791ce298c975612fe26 Mon Sep 17 00:00:00 2001 From: RooTheRu Date: Thu, 18 Aug 2022 09:22:38 +0800 Subject: [PATCH] Add Edition Drop Redeem implementation --- core/cli/src/cli.ts | 65 +++++++------ core/sdk/src/CandyShop.ts | 92 ++++++++++++------- core/sdk/src/CandyShopDrop.ts | 23 ++++- core/sdk/src/CandyShopModel.ts | 6 ++ core/sdk/src/factory/program/model.ts | 4 + .../factory/program/v2/editionDrop/index.ts | 1 + .../program/v2/editionDrop/redeemNft.ts | 32 +++++++ core/sdk/src/idl/edition_drop.json | 83 +++++++++++++---- core/sdk/src/vendor/error.ts | 2 + core/sdk/src/vendor/utils/validationUtils.ts | 15 +++ 10 files changed, 248 insertions(+), 75 deletions(-) create mode 100644 core/sdk/src/factory/program/v2/editionDrop/redeemNft.ts diff --git a/core/cli/src/cli.ts b/core/cli/src/cli.ts index 713dc2d1..61d6ca80 100644 --- a/core/cli/src/cli.ts +++ b/core/cli/src/cli.ts @@ -36,8 +36,6 @@ programCommand('sellMany') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .requiredOption('-p, --price ', 'price in token decimals') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMintList, treasuryMint, price, shopCreator, rpcUrl, version, isEnterpriseArg } = cmd.opts(); @@ -82,8 +80,6 @@ programCommand('cancelMany') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .requiredOption('-p, --price ', 'price in token decimals') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMintList, treasuryMint, price, shopCreator, rpcUrl, version, isEnterpriseArg } = cmd.opts(); @@ -128,8 +124,6 @@ programCommand('sell') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .requiredOption('-p, --price ', 'price in token decimals') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMint, treasuryMint, price, shopCreator, rpcUrl, version, isEnterpriseArg } = cmd.opts(); @@ -170,8 +164,6 @@ programCommand('cancel') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .requiredOption('-p, --price ', 'price in token decimals') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMint, treasuryMint, price, shopCreator, rpcUrl, version, isEnterpriseArg } = cmd.opts(); @@ -214,8 +206,6 @@ programCommand('buy') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .requiredOption('-p, --price ', 'price in token decimals') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, @@ -268,8 +258,6 @@ programCommand('createAuction') .requiredOption('-ts, --tick-size ', 'tick size') .option('-bnp, --buy-now-price ', 'Buy now price, in the unit of treasury mint, nullable') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, @@ -326,8 +314,6 @@ programCommand('cancelAuction') .requiredOption('-tm, --treasury-mint ', 'Candy Shop treasury mint') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMint, treasuryMint, rpcUrl, shopCreator, version, isEnterpriseArg } = cmd.opts(); const wallet = loadKey(keypair); @@ -366,8 +352,6 @@ programCommand('makeBid') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .requiredOption('-p, --price ', 'price in token decimals') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMint, treasuryMint, rpcUrl, shopCreator, price, version, isEnterpriseArg } = cmd.opts(); @@ -407,8 +391,6 @@ programCommand('withdrawBid') .requiredOption('-tm, --treasury-mint ', 'Candy Shop treasury mint') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMint, treasuryMint, rpcUrl, shopCreator, version, isEnterpriseArg } = cmd.opts(); const wallet = loadKey(keypair); @@ -445,8 +427,6 @@ programCommand('buyNow') .requiredOption('-tm, --treasury-mint ', 'Candy Shop treasury mint') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMint, treasuryMint, rpcUrl, shopCreator, version, isEnterpriseArg } = cmd.opts(); const wallet = loadKey(keypair); @@ -484,8 +464,6 @@ programCommand('settleAndDistribute') .requiredOption('-tm, --treasury-mint ', 'Candy Shop treasury mint') .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, tokenAccountMint, treasuryMint, rpcUrl, shopCreator, version, isEnterpriseArg } = cmd.opts(); const wallet = loadKey(keypair); @@ -529,8 +507,6 @@ programCommand('commitEditionDropNft') .option('-wtt, --whitelist-time ', 'whitelist time, unix timestamp') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, @@ -591,8 +567,6 @@ programCommand('mintPrint') .option('-wtm, --whitelist-mint ', 'whitelist mint') .action(async (name, cmd) => { - console.log(name); - let { keypair, env, @@ -636,4 +610,43 @@ programCommand('mintPrint') console.log('txHash', txHash); }); +programCommand('redeemDrop') + .description('mint an edition-ed NFT from the master edition') + .requiredOption('-ota, --nft-owner-token-account ', 'NFT token account address') + .requiredOption('-tm, --treasury-mint ', 'Candy Shop treasury mint') + .requiredOption('-sc, --shop-creator ', 'Candy Shop creator address') + .action(async (name, cmd) => { + let { keypair, env, nftOwnerTokenAccount, treasuryMint, rpcUrl, shopCreator, version, isEnterpriseArg } = + cmd.opts(); + const wallet = loadKey(keypair); + + if (version !== 'v2') { + throw new CandyShopError(CandyShopErrorType.IncorrectProgramId); + } + + // default to v2 + const candyShopProgramId = CANDY_SHOP_V2_PROGRAM_ID; + + const candyShop = new CandyShop({ + candyShopCreatorAddress: new anchor.web3.PublicKey(shopCreator), + treasuryMint: new anchor.web3.PublicKey(treasuryMint), + candyShopProgramId, + env, + settings: { + mainnetConnectionUrl: rpcUrl + }, + isEnterprise: isEnterprise(isEnterpriseArg) + }); + + const tokenAccountInfo = await getAccount(candyShop.connection(), new PublicKey(nftOwnerTokenAccount), 'finalized'); + + const txHash = await candyShop.redeemDrop({ + nftOwner: wallet, + nftOwnerTokenAccount: new PublicKey(nftOwnerTokenAccount), + masterMint: tokenAccountInfo.mint + }); + + console.log('txHash', txHash); + }); + CMD.parse(process.argv); diff --git a/core/sdk/src/CandyShop.ts b/core/sdk/src/CandyShop.ts index d2f7998f..065b5f05 100644 --- a/core/sdk/src/CandyShop.ts +++ b/core/sdk/src/CandyShop.ts @@ -10,23 +10,10 @@ import { TradeQuery, WhitelistNft } from '@liqnft/candy-shop-types'; -import { BN, Idl, Program, Provider, web3 } from '@project-serum/anchor'; +import { Idl, Program, Provider, web3 } from '@project-serum/anchor'; import { AnchorWallet } from '@solana/wallet-adapter-react'; -import { - getAuction, - getAuctionHouse, - getAuctionHouseAuthority, - getAuctionHouseFeeAcct, - getAuctionHouseTreasuryAcct, - getCandyShopSync, - getMetadataAccount, - getProgram, - CandyShopError, - CandyShopErrorType, - getCandyShopVersion -} from './vendor'; -import { supply } from './vendor/shipping'; -import { CANDY_SHOP_PROGRAM_ID, CANDY_SHOP_V2_PROGRAM_ID } from './factory/constants'; +import { CandyShopCommitNftParams, CandyShopMintPrintParams } from '.'; +import { CandyShopDrop } from './CandyShopDrop'; import { fetchNFTByMintAddress, fetchOrderByShopAndMintAddress, @@ -39,46 +26,60 @@ import { } from './CandyShopInfoAPI'; import { CandyShopBidAuctionParams, + CandyShopBuyNowParams, CandyShopBuyParams, CandyShopCancelAuctionParams, CandyShopCancelParams, + CandyShopConstructorParams, CandyShopCreateAuctionParams, + CandyShopRedeemParams, CandyShopSellParams, CandyShopSettings, - CandyShopWithdrawAuctionBidParams, CandyShopSettleAndDistributeParams, - CandyShopBuyNowParams, CandyShopUpdateParams, - CandyShopConstructorParams, - CandyShopVersion + CandyShopVersion, + CandyShopWithdrawAuctionBidParams } from './CandyShopModel'; import { CandyShopTrade } from './CandyShopTrade'; -import { configBaseUrl } from './vendor/config'; +import { CANDY_SHOP_PROGRAM_ID, CANDY_SHOP_V2_PROGRAM_ID } from './factory/constants'; import { + bidAuction, BidAuctionParams, bidAuctionV1, - bidAuction, + buyNowAuction, BuyNowAuctionParams, buyNowAuctionV1, - buyNowAuction, + cancelAuction, CancelAuctionParams, cancelAuctionV1, - cancelAuction, + createAuction, CreateAuctionParams, createAuctionV1, - createAuction, SettleAndDistributeProceedParams, - settleAndDistributeProceedsV1, settleAndDistributeProceeds, + settleAndDistributeProceedsV1, + updateCandyShop, UpdateCandyShopParams, updateCandyShopV1, - updateCandyShop, + withdrawBid, WithdrawBidParams, - withdrawBidV1, - withdrawBid + withdrawBidV1 } from './factory/program'; -import { CandyShopCommitNftParams, CandyShopMintPrintParams } from '.'; -import { CandyShopDrop } from './CandyShopDrop'; +import { + CandyShopError, + CandyShopErrorType, + getAuction, + getAuctionHouse, + getAuctionHouseAuthority, + getAuctionHouseFeeAcct, + getAuctionHouseTreasuryAcct, + getCandyShopSync, + getCandyShopVersion, + getMetadataAccount, + getProgram +} from './vendor'; +import { configBaseUrl } from './vendor/config'; +import { supply } from './vendor/shipping'; const Logger = 'CandyShop'; @@ -755,6 +756,35 @@ export class CandyShop { return txHash; } + /** + * Executes Edition Drop __RedeemNft__ action + * + * @param {CandyShopRedeemParams} params required parameters for mint print action + */ + public async redeemDrop(params: CandyShopRedeemParams) { + const { nftOwnerTokenAccount, masterMint, nftOwner } = params; + + if (this._version !== CandyShopVersion.V2) { + throw new CandyShopError(CandyShopErrorType.IncorrectProgramId); + } + + console.log(`${Logger}: performing redeem drop `, { + masterNft: masterMint.toString() + }); + + const txHash = await CandyShopDrop.redeemDrop({ + nftOwner, + candyShop: this._candyShopAddress, + nftOwnerTokenAccount, + masterMint, + isEnterprise: this._isEnterprise, + connection: this.connection(), + candyShopProgram: this.getStaticProgram(nftOwner) + }); + + return txHash; + } + /** * Fetch stats associated with this Candy Shop */ diff --git a/core/sdk/src/CandyShopDrop.ts b/core/sdk/src/CandyShopDrop.ts index fac0048f..cf72a8d7 100644 --- a/core/sdk/src/CandyShopDrop.ts +++ b/core/sdk/src/CandyShopDrop.ts @@ -8,7 +8,7 @@ import { } from '@solana/spl-token'; import { AnchorWallet } from '@solana/wallet-adapter-react'; import { Connection, Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { EditionDropCommitNftParams, EditionDropMintPrintParams } from '.'; +import { EditionDropCommitNftParams, EditionDropMintPrintParams, EditionDropRedeemParams } from '.'; import { EDITION_DROP_PROGRAM_ID } from './factory/constants'; import { commitNft, @@ -16,7 +16,9 @@ import { enterpriseCommitNft, enterpriseMintPrint, mintPrint, - MintPrintParams + MintPrintParams, + redeemNft, + RedeemNftParams } from './factory/program'; import editionDropIdl from './idl/edition_drop.json'; import { @@ -133,6 +135,23 @@ export abstract class CandyShopDrop { return mintPrint(instructions, mintPrintParams); } + + static async redeemDrop(params: EditionDropRedeemParams): Promise { + const { nftOwner, candyShop, nftOwnerTokenAccount, masterMint, connection } = params; + const [vaultAccount] = await getEditionVaultAccount(candyShop, nftOwnerTokenAccount); + const program = this.getProgram(connection, nftOwner); + + const redeemParams: RedeemNftParams = { + nftOwner, + candyShop, + vaultAccount, + nftOwnerTokenAccount, + masterMint, + program + }; + + return redeemNft(redeemParams); + } } interface NewToken { diff --git a/core/sdk/src/CandyShopModel.ts b/core/sdk/src/CandyShopModel.ts index 1d7b4c5c..aec4b490 100644 --- a/core/sdk/src/CandyShopModel.ts +++ b/core/sdk/src/CandyShopModel.ts @@ -226,6 +226,10 @@ export interface CandyShopMintPrintParams extends CandyShopEditionDropParams { editionBuyer: AnchorWallet | web3.Keypair; } +export interface CandyShopRedeemParams extends CandyShopEditionDropParams { + nftOwner: AnchorWallet | web3.Keypair; +} + interface EditionDropParams extends CandyShopEditionDropParams { isEnterprise: boolean; candyShop: web3.PublicKey; @@ -238,3 +242,5 @@ export interface EditionDropCommitNftParams extends EditionDropParams, CandyShop export interface EditionDropMintPrintParams extends EditionDropParams, CandyShopMintPrintParams { auctionHouse: web3.PublicKey; } + +export interface EditionDropRedeemParams extends EditionDropParams, CandyShopRedeemParams {} diff --git a/core/sdk/src/factory/program/model.ts b/core/sdk/src/factory/program/model.ts index 5d731865..de45a001 100644 --- a/core/sdk/src/factory/program/model.ts +++ b/core/sdk/src/factory/program/model.ts @@ -128,3 +128,7 @@ export interface MintPrintParams extends EditionDropParams { auctionHouse: web3.PublicKey; editionNumber: BN; } + +export interface RedeemNftParams extends EditionDropParams { + nftOwner: AnchorWallet | web3.Keypair; +} diff --git a/core/sdk/src/factory/program/v2/editionDrop/index.ts b/core/sdk/src/factory/program/v2/editionDrop/index.ts index bad83392..b644d929 100644 --- a/core/sdk/src/factory/program/v2/editionDrop/index.ts +++ b/core/sdk/src/factory/program/v2/editionDrop/index.ts @@ -1,4 +1,5 @@ export * from './commitNft'; export * from './mintPrint'; +export * from './redeemNft'; export { commitNft as enterpriseCommitNft } from './enterpriseCommitNft'; export { mintPrint as enterpriseMintPrint } from './enterpriseMintPrint'; diff --git a/core/sdk/src/factory/program/v2/editionDrop/redeemNft.ts b/core/sdk/src/factory/program/v2/editionDrop/redeemNft.ts new file mode 100644 index 00000000..33e7e825 --- /dev/null +++ b/core/sdk/src/factory/program/v2/editionDrop/redeemNft.ts @@ -0,0 +1,32 @@ +import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { SystemProgram, SYSVAR_RENT_PUBKEY, Transaction } from '@solana/web3.js'; +import { checkRedeemable, getAtaForMint, sendTx } from '../../../../vendor'; +import { RedeemNftParams } from '../../model'; + +export const redeemNft = async (params: RedeemNftParams) => { + const { nftOwner, vaultAccount, nftOwnerTokenAccount, masterMint, program } = params; + + await checkRedeemable(vaultAccount, program); + + const transaction = new Transaction(); + + const [vaultTokenAccount] = await getAtaForMint(masterMint, vaultAccount); + + const ix = await program.methods + .redeemNft() + .accounts({ + nftOwner: nftOwner.publicKey, + nftOwnerTokenAccount, + vaultAccount, + vaultTokenAccount, + nftMint: masterMint, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY + }) + .instruction(); + + transaction.add(ix); + + return await sendTx(nftOwner, transaction, program); +}; diff --git a/core/sdk/src/idl/edition_drop.json b/core/sdk/src/idl/edition_drop.json index a1f6200c..3dedb270 100644 --- a/core/sdk/src/idl/edition_drop.json +++ b/core/sdk/src/idl/edition_drop.json @@ -393,6 +393,52 @@ "type": "u64" } ] + }, + { + "name": "redeemNft", + "accounts": [ + { + "name": "nftOwner", + "isMut": true, + "isSigner": true + }, + { + "name": "nftOwnerTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "vaultTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "nftMint", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] } ], "accounts": [ @@ -680,86 +726,91 @@ }, { "code": 6026, + "name": "ExceededMaxAllowedSupply", + "msg": "Exceeded max allowed supply" + }, + { + "code": 6027, "name": "AlreadyInitialized", "msg": "Already initialized" }, { - "code": 6027, + "code": 6028, "name": "NumericalOverflow", "msg": "Invalid numerical inputs" }, { - "code": 6028, + "code": 6029, "name": "RedeemConditionNotMet", "msg": "Redeem condition not met" }, { - "code": 6029, + "code": 6030, "name": "WhitelistTimeNotSet", "msg": "whitelist time not set" }, { - "code": 6030, + "code": 6031, "name": "WhitelistAccountsWronglySupplied", "msg": "missing whitelist accounts" }, { - "code": 6031, + "code": 6032, "name": "InvalidWhitelistMint", "msg": "invalid whitelist mint" }, { - "code": 6032, + "code": 6033, "name": "InvalidWhitelistTokenAccount", "msg": "invalid whitelist token account" }, { - "code": 6033, + "code": 6034, "name": "WhitelistMintNotPresent", "msg": "whitelist mint not present in vault" }, { - "code": 6034, + "code": 6035, "name": "InsufficientWhitelistToken", "msg": "insufficient whitelist token" }, { - "code": 6035, + "code": 6036, "name": "VaultWhitelistAtaNotInitialized", "msg": "vault whitelist ata not initialized" }, { - "code": 6036, + "code": 6037, "name": "InvalidWhitelistMintDecimal", "msg": "invalid whitelist mint decimals" }, { - "code": 6037, + "code": 6038, "name": "InvalidWhitelistTime", "msg": "invalid whitelist time" }, { - "code": 6038, + "code": 6039, "name": "InvalidEditionIndex", "msg": "invalid edition number" }, { - "code": 6039, + "code": 6040, "name": "EditionTaken", "msg": "edition already taken" }, { - "code": 6040, + "code": 6041, "name": "InvalidShopKey", "msg": "key of provided candy shop does not match expected value" }, { - "code": 6041, + "code": 6042, "name": "MintNonNative", "msg": "Candy Shop treasury mint must be native" }, { - "code": 6042, + "code": 6043, "name": "InvalidNftOwner", "msg": "Invalid nft owner provided in remaining accounts" } diff --git a/core/sdk/src/vendor/error.ts b/core/sdk/src/vendor/error.ts index e1fd373c..22e5ab02 100644 --- a/core/sdk/src/vendor/error.ts +++ b/core/sdk/src/vendor/error.ts @@ -29,6 +29,7 @@ export enum CandyShopErrorType { IncorrectCandyShopType = 'IncorrectCandyShopType', VaultDoesNotExist = 'VaultDoesNotExist', NotWithinSalesPeriod = 'NotWithinSalesPeriod', + DropNotRedeemable = 'DropNotRedeemable', NotReachable = 'NotReachable' } @@ -67,6 +68,7 @@ export const CandyShopErrorMsgMap = { [CandyShopErrorType.IncorrectCandyShopType]: 'Candy Shop is of the incorrect type (enterprise or regular).', [CandyShopErrorType.VaultDoesNotExist]: 'Vault account does not exist.', [CandyShopErrorType.NotWithinSalesPeriod]: 'Attempted to mint print outside of sales period.', + [CandyShopErrorType.DropNotRedeemable]: 'Drop not redeemable', [CandyShopErrorType.NotReachable]: 'Unknown error. Please contact CandyShop team for further info.' }; diff --git a/core/sdk/src/vendor/utils/validationUtils.ts b/core/sdk/src/vendor/utils/validationUtils.ts index 4d0ed8f4..cf7c3382 100644 --- a/core/sdk/src/vendor/utils/validationUtils.ts +++ b/core/sdk/src/vendor/utils/validationUtils.ts @@ -268,3 +268,18 @@ export const checkEditionMintPeriod = async (vaultAccount: PublicKey, program: P } return vaultData; }; + +export const checkRedeemable = async (vaultAccount: PublicKey, program: Program) => { + const vaultData = await getEditionVaultData(vaultAccount, program); + + const currentTime: BN = new BN(Date.now() / 1000); + const salesEndTime: BN = vaultData.startingTime.add(vaultData.salesPeriod); + + if ( + vaultData.currentSupply.gt(new BN(0)) || + (vaultData.whitelistTime !== null && currentTime.gte(vaultData.whitelistTime) && currentTime.lt(salesEndTime)) || + (vaultData.startingTime !== null && currentTime.gte(vaultData.startingTime && currentTime.lt(salesEndTime))) + ) { + throw new CandyShopError(CandyShopErrorType.DropNotRedeemable); + } +};