From fd4a3b376ec94d8626ac84c54f4f4b3d45c37a11 Mon Sep 17 00:00:00 2001 From: Paul <5280742+kurpav@users.noreply.github.com> Date: Mon, 6 Dec 2021 15:16:17 +0200 Subject: [PATCH] feat: add redeem printing v2 action and update instant sale (#91) * feat: add instruction for redeem printing v2 bid * feat: update redeem printing v2 bid params * feat: minor refactoring * feat: add redeem printing v2 action add redeem printing v2 action and update instant sale to support it * feat: update readme * test: add test for transaction * refactor: fix formatting * refactor: fix code review comments --- README.md | 6 +- src/Account.ts | 3 +- src/actions/index.ts | 3 +- src/actions/instantSale.ts | 118 ++++----- ...mBid.ts => redeemFullRightsTransferBid.ts} | 6 +- src/actions/redeemPrintingV2Bid.ts | 240 ++++++++++++++++++ src/actions/shared/index.ts | 18 +- src/errors.ts | 9 + src/programs/auction/accounts/Auction.ts | 7 +- src/programs/auction/accounts/BidderPot.ts | 3 + .../transactions/RedeemPrintingV2Bid.ts | 225 ++++++++++++++++ .../__snapshots__/metaplex.test.ts.snap | 2 + test/transactions/metaplex.test.ts | 45 ++++ test/utils.ts | 15 ++ 14 files changed, 620 insertions(+), 80 deletions(-) rename src/actions/{redeemBid.ts => redeemFullRightsTransferBid.ts} (96%) create mode 100644 src/actions/redeemPrintingV2Bid.ts create mode 100644 src/programs/metaplex/transactions/RedeemPrintingV2Bid.ts diff --git a/README.md b/README.md index 089d981..04b0379 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,8 @@ const rates = await new Coingecko().getRate([Currency.AR, Currency.SOL], Currenc - [ ] Original Authority Lookup - [ ] Instructions - [x] RedeemBid - - [ ] RedeemFullRightsTransferBid + - [x] RedeemFullRightsTransferBid + - [x] RedeemPrintingV2Bid - [x] StartAuction - [ ] EndAuction - [x] ClaimBid @@ -127,7 +128,8 @@ const rates = await new Coingecko().getRate([Currency.AR, Currency.SOL], Currenc - [ ] Actions (no standalone actions) - [x] Cancel Bid - [x] Place Bid - - [x] Redeem Bid + - [x] Redeem Full Rights Transfer Bid + - [x] Redeem Printing V2 Bid - [x] Instant Sale - [ ] Vault - [ ] Accounts diff --git a/src/Account.ts b/src/Account.ts index b8534be..59aab18 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -1,6 +1,7 @@ import { AccountInfo, Commitment, PublicKey, Connection } from '@solana/web3.js'; import { AnyPublicKey } from '@metaplex/types'; import { Buffer } from 'buffer'; +import { ERROR_ACCOUNT_NOT_FOUND } from './errors'; export type AccountConstructor = { new (pubkey: AnyPublicKey, info: AccountInfo): T; @@ -38,7 +39,7 @@ export class Account { static async getInfo(connection: Connection, pubkey: AnyPublicKey) { const info = await connection.getAccountInfo(new PublicKey(pubkey)); if (!info) { - throw new Error(`Unable to find account: ${pubkey}`); + throw ERROR_ACCOUNT_NOT_FOUND(pubkey); } return { ...info, data: Buffer.from(info?.data) }; diff --git a/src/actions/index.ts b/src/actions/index.ts index e012374..8a5142d 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -12,7 +12,8 @@ export * from './signMetadata'; export * from './updateMetadata'; export * from './cancelBid'; export * from './placeBid'; -export * from './redeemBid'; +export * from './redeemFullRightsTransferBid'; +export * from './redeemPrintingV2Bid'; export * from './claimBid'; export * from './instantSale'; export * from './burnToken'; diff --git a/src/actions/instantSale.ts b/src/actions/instantSale.ts index 646447e..f0f5bdf 100644 --- a/src/actions/instantSale.ts +++ b/src/actions/instantSale.ts @@ -1,18 +1,15 @@ import { PublicKey } from '@solana/web3.js'; -import { AccountLayout } from '@solana/spl-token'; import retry from 'async-retry'; import { Wallet } from '../wallet'; import { Connection } from '../Connection'; -import { sendTransaction } from './transactions'; -import { Auction, AuctionExtended, BidderPot } from '../programs/auction'; -import { AuctionManager, SafetyDepositConfig } from '../programs/metaplex'; +import { AuctionExtended } from '../programs/auction'; +import { AuctionManager, SafetyDepositConfig, WinningConfigType } from '../programs/metaplex'; import { placeBid } from './placeBid'; -import { getClaimBidTransactions } from './claimBid'; -import { getRedeemBidTransactions } from './redeemBid'; +import { claimBid } from './claimBid'; import { Vault } from '../programs/vault/accounts/Vault'; -import { Metadata } from '../programs/metadata'; -import { getBidRedemptionPDA } from './redeemBid'; +import { redeemFullRightsTransferBid } from './redeemFullRightsTransferBid'; import { Account } from '../Account'; +import { redeemPrintingV2Bid } from './redeemPrintingV2Bid'; interface IInstantSaleParams { connection: Connection; @@ -22,7 +19,7 @@ interface IInstantSaleParams { } interface IInstantSaleResponse { - txId: string; + txIds: string[]; } export const instantSale = async ({ @@ -31,89 +28,74 @@ export const instantSale = async ({ store, auction, }: IInstantSaleParams): Promise => { + const txIds = []; // get data for transactions - const bidder = wallet.publicKey; - const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span); - const auctionManager = await AuctionManager.getPDA(auction); - const manager = await AuctionManager.load(connection, auctionManager); + const auctionManagerPDA = await AuctionManager.getPDA(auction); + const manager = await AuctionManager.load(connection, auctionManagerPDA); const vault = await Vault.load(connection, manager.data.vault); - const { - data: { tokenMint }, - } = await Auction.load(connection, auction); - const auctionExtended = await AuctionExtended.getPDA(vault.pubkey); - const acceptPayment = new PublicKey(manager.data.acceptPayment); + const auctionExtendedPDA = await AuctionExtended.getPDA(vault.pubkey); const { data: { instantSalePrice }, - } = await AuctionExtended.load(connection, auctionExtended); - const auctionTokenMint = new PublicKey(tokenMint); - const bidderPot = await BidderPot.getPDA(auction, bidder); - const fractionMint = new PublicKey(vault.data.fractionMint); - // assuming we have 1 item + } = await AuctionExtended.load(connection, auctionExtendedPDA); const [safetyDepositBox] = await vault.getSafetyDepositBoxes(connection); - const metadataTokenMint = new PublicKey(safetyDepositBox.data.tokenMint); - const safetyDepositTokenStore = new PublicKey(safetyDepositBox.data.store); - const safetyDepositConfig = await SafetyDepositConfig.getPDA( - auctionManager, + const safetyDepositConfigPDA = await SafetyDepositConfig.getPDA( + auctionManagerPDA, safetyDepositBox.pubkey, ); - const transferAuthority = await Vault.getPDA(vault.pubkey); - const metadata = await Metadata.getPDA(metadataTokenMint); + const { + data: { winningConfigType }, + } = await SafetyDepositConfig.load(connection, safetyDepositConfigPDA); //// - const { bidderPotToken, bidderMeta } = await placeBid({ + const { + txId: placeBidTxId, + bidderPotToken, + bidderMeta, + } = await placeBid({ connection, wallet, amount: instantSalePrice, auction, }); + txIds.push(placeBidTxId); // workaround to wait for bidderMeta to be created await retry(async (bail) => { await Account.getInfo(connection, bidderMeta); }); - const bidRedemption = await getBidRedemptionPDA(auction, bidderMeta); - const redeemBatch = await getRedeemBidTransactions({ - accountRentExempt, - tokenMint: metadataTokenMint, - bidder, - bidderMeta, - store, - vault: vault.pubkey, - auction, - auctionExtended, - auctionManager, - fractionMint, - safetyDepositTokenStore, - safetyDeposit: safetyDepositBox.pubkey, - bidRedemption, - safetyDepositConfig, - transferAuthority, - metadata, - }); + // NOTE: it's divided into 3 transactions since transaction size is restricted + switch (winningConfigType) { + case WinningConfigType.FullRightsTransfer: + const { txId: redeemFullRightsTransferBidTxId } = await redeemFullRightsTransferBid({ + connection, + wallet, + store, + auction, + }); + txIds.push(redeemFullRightsTransferBidTxId); + break; + case WinningConfigType.PrintingV2: + const { txId: redeemPrintingV2BidTxId } = await redeemPrintingV2Bid({ + connection, + wallet, + store, + auction, + }); + txIds.push(redeemPrintingV2BidTxId); + break; + default: + throw new Error(`${winningConfigType} winning type isn't supported yet`); + } - const claimBatch = await getClaimBidTransactions({ - auctionTokenMint, - bidder, + const { txId: claimBidTxId } = await claimBid({ + connection, + wallet, store, - vault: vault.pubkey, auction, - auctionExtended, - auctionManager, - acceptPayment, - bidderPot, bidderPotToken, }); + txIds.push(claimBidTxId); - const txs = [...redeemBatch.toTransactions(), ...claimBatch.toTransactions()]; - const signers = [...redeemBatch.signers, ...claimBatch.signers]; - - const txId = await sendTransaction({ - connection, - wallet, - txs, - signers, - }); - - return { txId }; + return { txIds: [placeBidTxId] }; }; diff --git a/src/actions/redeemBid.ts b/src/actions/redeemFullRightsTransferBid.ts similarity index 96% rename from src/actions/redeemBid.ts rename to src/actions/redeemFullRightsTransferBid.ts index dcc7d4f..4068af6 100644 --- a/src/actions/redeemBid.ts +++ b/src/actions/redeemFullRightsTransferBid.ts @@ -26,7 +26,7 @@ interface IRedeemBidResponse { txId: string; } -export const redeemBid = async ({ +export const redeemFullRightsTransferBid = async ({ connection, wallet, store, @@ -54,7 +54,7 @@ export const redeemBid = async ({ const metadata = await Metadata.getPDA(tokenMint); //// - const txBatch = await getRedeemBidTransactions({ + const txBatch = await getRedeemFRTBidTransactions({ accountRentExempt, tokenMint, bidder, @@ -103,7 +103,7 @@ interface IRedeemBidTransactionsParams { metadata: PublicKey; } -export const getRedeemBidTransactions = async ({ +export const getRedeemFRTBidTransactions = async ({ accountRentExempt, bidder, tokenMint, diff --git a/src/actions/redeemPrintingV2Bid.ts b/src/actions/redeemPrintingV2Bid.ts new file mode 100644 index 0000000..b934156 --- /dev/null +++ b/src/actions/redeemPrintingV2Bid.ts @@ -0,0 +1,240 @@ +import BN from 'bn.js'; +import { PublicKey } from '@solana/web3.js'; +import { Wallet } from '../wallet'; +import { Connection } from '../Connection'; +import { sendTransaction } from './transactions'; +import { Auction, AuctionExtended, BidderMetadata } from '../programs/auction'; +import { TransactionsBatch } from '../utils/transactions-batch'; +import { AuctionManager, PrizeTrackingTicket, SafetyDepositConfig } from '../programs/metaplex'; +import { Vault } from '../programs/vault'; +import { + Edition, + EditionMarker, + MasterEdition, + Metadata, + UpdatePrimarySaleHappenedViaToken, +} from '../programs/metadata'; +import { prepareTokenAccountAndMintTx } from './shared'; +import { RedeemPrintingV2Bid } from '../programs/metaplex/transactions/RedeemPrintingV2Bid'; +import { getBidRedemptionPDA } from './redeemFullRightsTransferBid'; + +interface IRedeemBidParams { + connection: Connection; + wallet: Wallet; + auction: PublicKey; + store: PublicKey; +} + +interface IRedeemBidResponse { + txId: string; +} + +interface IRedeemBidTransactionsParams { + bidder: PublicKey; + bidderPotToken?: PublicKey; + bidderMeta: PublicKey; + auction: PublicKey; + auctionExtended: PublicKey; + destination: PublicKey; + vault: PublicKey; + store: PublicKey; + auctionManager: PublicKey; + bidRedemption: PublicKey; + safetyDepositTokenStore: PublicKey; + safetyDeposit: PublicKey; + safetyDepositConfig: PublicKey; + metadata: PublicKey; + newMint: PublicKey; + newMetadata: PublicKey; + newEdition: PublicKey; + masterEdition: PublicKey; + editionMarker: PublicKey; + prizeTrackingTicket: PublicKey; + winIndex: BN; + editionOffset: BN; +} + +export const redeemPrintingV2Bid = async ({ + connection, + wallet, + store, + auction, +}: IRedeemBidParams): Promise => { + // get data for transactions + const bidder = wallet.publicKey; + const { + data: { bidState }, + } = await Auction.load(connection, auction); + const auctionManagerPDA = await AuctionManager.getPDA(auction); + const manager = await AuctionManager.load(connection, auctionManagerPDA); + const vault = await Vault.load(connection, manager.data.vault); + const auctionExtendedPDA = await AuctionExtended.getPDA(vault.pubkey); + const [safetyDepositBox] = await vault.getSafetyDepositBoxes(connection); + const originalMint = new PublicKey(safetyDepositBox.data.tokenMint); + + const safetyDepositTokenStore = new PublicKey(safetyDepositBox.data.store); + const bidderMetaPDA = await BidderMetadata.getPDA(auction, bidder); + const bidRedemptionPDA = await getBidRedemptionPDA(auction, bidderMetaPDA); + const safetyDepositConfigPDA = await SafetyDepositConfig.getPDA( + auctionManagerPDA, + safetyDepositBox.pubkey, + ); + //// + + const { mint, createMintTx, createAssociatedTokenAccountTx, mintToTx, recipient } = + await prepareTokenAccountAndMintTx(connection, wallet.publicKey); + + const newMint = mint.publicKey; + const newMetadataPDA = await Metadata.getPDA(newMint); + const newEditionPDA = await Edition.getPDA(newMint); + + const metadataPDA = await Metadata.getPDA(originalMint); + const masterEditionPDA = await MasterEdition.getPDA(originalMint); + const masterEdition = await MasterEdition.load(connection, masterEditionPDA); + + const prizeTrackingTicketPDA = await PrizeTrackingTicket.getPDA(auctionManagerPDA, originalMint); + + let prizeTrackingTicket: PrizeTrackingTicket; + // this account doesn't exist when we do redeem for the first time + try { + prizeTrackingTicket = await PrizeTrackingTicket.load(connection, prizeTrackingTicketPDA); + } catch (e) { + prizeTrackingTicket = null; + } + + const winIndex = bidState.getWinnerIndex(bidder.toBase58()) || 0; + + const editionOffset = getEditionOffset(winIndex); + const editionBase = prizeTrackingTicket?.data.supplySnapshot || masterEdition.data.supply; + const desiredEdition = editionBase.add(editionOffset); + const editionMarkerPDA = await EditionMarker.getPDA(originalMint, desiredEdition); + + // checking if edition marker is taken + try { + const editionMarker = await EditionMarker.load(connection, editionMarkerPDA); + const isEditionTaken = editionMarker.data.editionTaken(desiredEdition.toNumber()); + if (isEditionTaken) { + throw new Error('The edition is already taken'); + } + } catch (e) { + // it's not. continue + } + + const txBatch = await getRedeemPrintingV2BidTransactions({ + bidder, + bidderMeta: bidderMetaPDA, + store, + vault: vault.pubkey, + destination: recipient, + auction, + auctionExtended: auctionExtendedPDA, + auctionManager: auctionManagerPDA, + safetyDepositTokenStore, + safetyDeposit: safetyDepositBox.pubkey, + bidRedemption: bidRedemptionPDA, + safetyDepositConfig: safetyDepositConfigPDA, + + metadata: metadataPDA, + newMint, + newMetadata: newMetadataPDA, + newEdition: newEditionPDA, + masterEdition: masterEditionPDA, + editionMarker: editionMarkerPDA, + prizeTrackingTicket: prizeTrackingTicketPDA, + editionOffset, + winIndex: new BN(winIndex), + }); + + txBatch.addSigner(mint); + txBatch.addBeforeTransaction(createMintTx); + txBatch.addBeforeTransaction(createAssociatedTokenAccountTx); + txBatch.addBeforeTransaction(mintToTx); + + const txId = await sendTransaction({ + connection, + wallet, + txs: txBatch.toTransactions(), + signers: txBatch.signers, + }); + + return { txId }; +}; + +export const getRedeemPrintingV2BidTransactions = async ({ + bidder, + destination, + store, + vault, + auction, + auctionManager, + auctionExtended, + bidRedemption, + bidderMeta: bidMetadata, + safetyDepositTokenStore, + safetyDeposit, + safetyDepositConfig, + + metadata, + newMint, + newMetadata, + newEdition, + masterEdition, + editionMarker: editionMark, + prizeTrackingTicket, + + winIndex, + editionOffset, +}: IRedeemBidTransactionsParams) => { + const txBatch = new TransactionsBatch({ transactions: [] }); + + // create redeem bid + const redeemPrintingV2BidTx = new RedeemPrintingV2Bid( + { feePayer: bidder }, + { + store, + vault, + auction, + auctionManager, + bidRedemption, + bidMetadata, + safetyDepositTokenStore, + destination, + safetyDeposit, + bidder, + safetyDepositConfig, + auctionExtended, + + newMint, + newEdition, + newMetadata, + metadata, + masterEdition, + editionMark, + prizeTrackingTicket, + winIndex, + editionOffset, + }, + ); + txBatch.addTransaction(redeemPrintingV2BidTx); + //// + + // update primary sale happened via token + const updatePrimarySaleHappenedViaTokenTx = new UpdatePrimarySaleHappenedViaToken( + { feePayer: bidder }, + { + metadata: newMetadata, + owner: bidder, + tokenAccount: destination, + }, + ); + txBatch.addTransaction(updatePrimarySaleHappenedViaTokenTx); + //// + + return txBatch; +}; + +export function getEditionOffset(winIndex: number) { + const offset = new BN(1); + // NOTE: not sure if this the right way to calculate it + return offset.add(new BN(winIndex)); +} diff --git a/src/actions/shared/index.ts b/src/actions/shared/index.ts index 185b663..f9e3503 100644 --- a/src/actions/shared/index.ts +++ b/src/actions/shared/index.ts @@ -5,9 +5,21 @@ import { Token, TOKEN_PROGRAM_ID, } from '@solana/spl-token'; -import { CreateAssociatedTokenAccount, CreateMint, MintTo } from '../../programs'; +import { CreateAssociatedTokenAccount, CreateMint, MintTo, Transaction } from '../../programs'; -export async function prepareTokenAccountAndMintTx(connection: Connection, owner: PublicKey) { +interface MintTxs { + mint: Keypair; + // recipient ATA + recipient: PublicKey; + createMintTx: Transaction; + createAssociatedTokenAccountTx: Transaction; + mintToTx: Transaction; +} + +export async function prepareTokenAccountAndMintTx( + connection: Connection, + owner: PublicKey, +): Promise { const mint = Keypair.generate(); const mintRent = await connection.getMinimumBalanceForRentExemption(MintLayout.span); const createMintTx = new CreateMint( @@ -42,5 +54,5 @@ export async function prepareTokenAccountAndMintTx(connection: Connection, owner }, ); - return { mint, createMintTx, createAssociatedTokenAccountTx, mintToTx }; + return { mint, createMintTx, createAssociatedTokenAccountTx, mintToTx, recipient }; } diff --git a/src/errors.ts b/src/errors.ts index 97d469b..4d1d6a4 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,7 +1,10 @@ +import { AnyPublicKey } from './types'; + export enum ErrorCode { ERROR_INVALID_OWNER, ERROR_INVALID_ACCOUNT_DATA, ERROR_DEPRECATED_ACCOUNT_DATA, + ERROR_ACCOUNT_NOT_FOUND, } export class MetaplexError extends Error { @@ -24,3 +27,9 @@ export const ERROR_INVALID_ACCOUNT_DATA: () => MetaplexError = () => { export const ERROR_DEPRECATED_ACCOUNT_DATA: () => MetaplexError = () => { return new MetaplexError(ErrorCode.ERROR_DEPRECATED_ACCOUNT_DATA, 'Account data is deprecated'); }; + +export const ERROR_ACCOUNT_NOT_FOUND: (pubkey: AnyPublicKey) => MetaplexError = ( + pubkey: string, +) => { + return new MetaplexError(ErrorCode.ERROR_ACCOUNT_NOT_FOUND, `Unable to find account: ${pubkey}`); +}; diff --git a/src/programs/auction/accounts/Auction.ts b/src/programs/auction/accounts/Auction.ts index d9464a2..bee822b 100644 --- a/src/programs/auction/accounts/Auction.ts +++ b/src/programs/auction/accounts/Auction.ts @@ -73,14 +73,17 @@ export class BidState extends Borsh.Data { } getWinnerIndex(bidder: StringPublicKey): number | null { - if (!this.bids) return null; + if (!this.bids) { + return null; + } const index = this.bids.findIndex((b) => b.key === bidder); // auction stores data in reverse order if (index !== -1) { const zeroBased = this.bids.length - index - 1; return zeroBased < this.max.toNumber() ? zeroBased : null; - } else return null; + } + return null; } } diff --git a/src/programs/auction/accounts/BidderPot.ts b/src/programs/auction/accounts/BidderPot.ts index a145a85..d1a4914 100644 --- a/src/programs/auction/accounts/BidderPot.ts +++ b/src/programs/auction/accounts/BidderPot.ts @@ -22,8 +22,11 @@ export class BidderPotData extends Borsh.Data { /// Points at actual pot that is a token account bidderPot: StringPublicKey; + /// Originating bidder account bidderAct: StringPublicKey; + /// Auction account auctionAct: StringPublicKey; + /// emptied or not emptied: boolean; } diff --git a/src/programs/metaplex/transactions/RedeemPrintingV2Bid.ts b/src/programs/metaplex/transactions/RedeemPrintingV2Bid.ts new file mode 100644 index 0000000..b96d1fc --- /dev/null +++ b/src/programs/metaplex/transactions/RedeemPrintingV2Bid.ts @@ -0,0 +1,225 @@ +import { ParamsWithStore } from '@metaplex/types'; +import { Borsh } from '@metaplex/utils'; +import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + PublicKey, + SystemProgram, + SYSVAR_RENT_PUBKEY, + TransactionCtorFields, + TransactionInstruction, +} from '@solana/web3.js'; +import BN from 'bn.js'; +import { Transaction } from '../../../Transaction'; +import { MetadataProgram } from '../../metadata'; +import { VaultProgram } from '../../vault'; +import { MetaplexProgram } from '../MetaplexProgram'; + +export class RedeemPrintingV2BidArgs extends Borsh.Data<{ editionOffset: BN; winIndex: BN }> { + static readonly SCHEMA = this.struct([ + ['instruction', 'u8'], + ['editionOffset', 'u64'], + ['winIndex', 'u64'], + ]); + + instruction = 14; + editionOffset: BN; + winIndex: BN; +} + +type RedeemPrintingV2BidParams = { + vault: PublicKey; + auction: PublicKey; + auctionManager: PublicKey; + bidRedemption: PublicKey; + bidMetadata: PublicKey; + safetyDepositTokenStore: PublicKey; + destination: PublicKey; + safetyDeposit: PublicKey; + bidder: PublicKey; + safetyDepositConfig: PublicKey; + auctionExtended: PublicKey; + metadata: PublicKey; + prizeTrackingTicket: PublicKey; + newMetadata: PublicKey; + newEdition: PublicKey; + masterEdition: PublicKey; + newMint: PublicKey; + editionMark: PublicKey; + winIndex: BN; + editionOffset: BN; +}; + +export class RedeemPrintingV2Bid extends Transaction { + constructor(options: TransactionCtorFields, params: ParamsWithStore) { + super(options); + const { feePayer } = options; + const { + store, + vault, + auction, + auctionExtended, + auctionManager, + bidRedemption, + bidMetadata, + safetyDepositTokenStore, + destination, + safetyDeposit, + bidder, + safetyDepositConfig, + metadata, + prizeTrackingTicket, + newMetadata, + newEdition, + masterEdition, + newMint, + editionMark, + winIndex, + editionOffset, + } = params; + + const data = RedeemPrintingV2BidArgs.serialize({ winIndex, editionOffset }); + + this.add( + new TransactionInstruction({ + keys: [ + { + pubkey: auctionManager, + isSigner: false, + isWritable: true, + }, + { + pubkey: safetyDepositTokenStore, + isSigner: false, + isWritable: true, + }, + { + pubkey: destination, + isSigner: false, + isWritable: true, + }, + { + pubkey: bidRedemption, + isSigner: false, + isWritable: true, + }, + { + pubkey: safetyDeposit, + isSigner: false, + isWritable: true, + }, + { + pubkey: vault, + isSigner: false, + isWritable: true, + }, + { + pubkey: safetyDepositConfig, + isSigner: false, + isWritable: false, + }, + { + pubkey: auction, + isSigner: false, + isWritable: false, + }, + { + pubkey: bidMetadata, + isSigner: false, + isWritable: false, + }, + { + pubkey: bidder, + isSigner: false, + isWritable: false, + }, + { + pubkey: feePayer, + isSigner: true, + isWritable: true, + }, + { + pubkey: TOKEN_PROGRAM_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: VaultProgram.PUBKEY, + isSigner: false, + isWritable: false, + }, + { + pubkey: MetadataProgram.PUBKEY, + isSigner: false, + isWritable: false, + }, + { + pubkey: store, + isSigner: false, + isWritable: false, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: SYSVAR_RENT_PUBKEY, + isSigner: false, + isWritable: false, + }, + { + pubkey: prizeTrackingTicket, + isSigner: false, + isWritable: true, + }, + { + pubkey: newMetadata, + isSigner: false, + isWritable: true, + }, + { + pubkey: newEdition, + isSigner: false, + isWritable: true, + }, + { + pubkey: masterEdition, + isSigner: false, + isWritable: true, + }, + { + pubkey: newMint, + isSigner: false, + isWritable: true, + }, + { + pubkey: editionMark, + isSigner: false, + isWritable: true, + }, + { + // Mint authority (this) is going to be the payer since the bidder + // may not be signer hre - we may be redeeming for someone else (permissionless) + // and during the txn, mint authority is removed from us and given to master edition. + // The ATA account is already owned by bidder by default. No signing needed + pubkey: feePayer, + isSigner: true, + isWritable: false, + }, + { + pubkey: metadata, + isSigner: false, + isWritable: false, + }, + { + pubkey: auctionExtended, + isSigner: false, + isWritable: false, + }, + ], + programId: MetaplexProgram.PUBKEY, + data, + }), + ); + } +} diff --git a/test/transactions/__snapshots__/metaplex.test.ts.snap b/test/transactions/__snapshots__/metaplex.test.ts.snap index 6786266..d4de57c 100644 --- a/test/transactions/__snapshots__/metaplex.test.ts.snap +++ b/test/transactions/__snapshots__/metaplex.test.ts.snap @@ -1,3 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Metaplex transactions EndAuction(reveal = null) 1`] = `"{\\"type\\":\\"Buffer\\",\\"data\\":[2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,5,9,93,135,29,120,250,13,109,171,35,122,77,213,58,178,208,153,32,156,150,192,153,14,177,253,172,197,7,247,216,61,254,175,207,140,94,235,253,88,52,215,223,250,161,132,68,41,246,210,18,149,185,76,29,184,100,232,184,10,144,98,156,162,27,90,233,202,105,2,153,20,72,22,139,175,230,106,27,70,0,106,112,120,169,110,223,61,168,80,141,241,227,242,31,218,180,236,155,76,202,43,34,141,168,115,82,176,65,170,75,45,110,185,97,234,236,43,234,0,144,234,95,255,33,107,30,38,145,153,130,131,135,32,113,60,44,78,85,97,231,146,60,57,129,72,71,229,99,140,205,42,175,85,79,60,2,195,134,224,132,55,183,200,146,174,179,202,255,59,237,71,198,14,93,80,130,36,243,239,156,123,105,168,4,229,71,114,217,106,120,80,194,62,8,175,169,191,10,132,153,239,245,177,12,199,221,36,59,224,20,103,70,219,249,150,85,157,145,130,193,93,188,24,246,87,6,167,213,23,24,199,116,201,40,86,99,152,105,29,94,182,139,94,184,163,155,75,109,92,115,85,91,33,0,0,0,0,12,11,9,188,163,72,31,129,1,245,97,110,85,233,233,212,126,91,245,121,77,102,76,9,137,66,49,240,175,59,106,23,131,79,219,30,88,36,153,69,200,48,40,136,227,246,197,102,213,141,122,4,186,57,98,62,139,154,145,152,59,13,211,149,1,8,7,2,3,4,1,5,6,7,2,20,0]}"`; + +exports[`Metaplex transactions RedeemPrintingV2Bid 1`] = `"{\\"type\\":\\"Buffer\\",\\"data\\":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,12,25,93,135,29,120,250,13,109,171,35,122,77,213,58,178,208,153,32,156,150,192,153,14,177,253,172,197,7,247,216,61,254,175,155,76,202,43,34,141,168,115,82,176,65,170,75,45,110,185,97,234,236,43,234,0,144,234,95,255,33,107,30,38,145,153,47,149,134,43,4,86,112,205,34,142,120,147,252,26,2,86,248,99,18,46,101,201,146,251,42,49,244,157,217,16,26,10,56,14,227,34,118,68,35,153,43,122,125,228,180,23,227,117,141,24,61,49,174,102,137,121,161,240,55,57,17,202,119,9,47,149,134,43,4,86,112,205,34,142,120,147,252,26,2,86,248,99,18,46,101,201,146,251,42,49,244,157,217,16,28,79,101,93,28,19,203,90,149,199,215,78,46,179,89,233,42,199,254,72,173,157,164,132,109,200,23,214,238,28,156,187,225,122,151,236,219,224,54,47,112,197,46,29,246,29,105,42,66,214,178,206,61,219,206,132,245,159,159,124,100,29,253,0,131,115,91,40,146,183,9,11,253,128,231,91,83,123,247,28,235,55,17,175,45,47,10,130,125,182,211,90,32,101,147,100,233,93,70,65,21,141,143,89,143,146,210,57,211,217,223,24,20,153,223,130,92,14,226,188,44,87,44,70,198,23,191,63,49,227,47,149,70,38,70,6,80,167,253,84,157,123,255,6,98,232,148,71,177,174,211,232,200,92,171,149,91,23,240,14,230,10,201,96,29,68,155,188,208,7,192,116,93,255,212,132,172,139,142,121,64,99,119,55,85,243,102,210,136,30,72,187,160,175,113,178,77,47,3,206,165,188,130,225,172,175,199,64,162,55,188,104,159,223,218,116,88,247,167,60,131,209,170,44,208,98,91,40,146,183,9,11,253,128,231,91,83,123,247,28,234,83,143,113,239,17,57,201,33,165,148,134,206,82,76,100,233,93,47,149,70,38,70,6,80,167,253,84,157,123,255,6,98,232,148,71,177,174,211,232,200,91,74,216,112,157,217,16,26,10,171,212,12,59,44,148,107,164,115,140,56,141,54,163,71,180,121,31,117,158,149,162,185,74,82,241,134,179,82,183,83,189,187,209,148,182,34,149,175,173,192,85,175,252,231,130,76,40,175,177,44,111,250,168,3,236,149,34,236,19,46,9,66,138,6,221,246,225,215,101,161,147,217,203,225,70,206,235,121,172,28,180,133,237,95,91,55,145,58,140,245,133,126,255,0,169,13,186,28,52,26,119,115,94,210,96,195,36,182,190,250,187,9,244,245,52,7,50,47,49,172,28,41,212,233,209,175,49,11,112,101,177,227,209,124,69,56,157,82,127,107,4,195,205,88,184,108,115,26,160,253,181,73,182,209,188,3,248,41,70,183,200,146,174,179,202,255,59,237,71,198,14,93,80,130,36,243,239,156,123,105,168,4,229,71,114,217,106,120,80,194,62,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,167,213,23,25,44,92,81,33,140,201,76,61,74,241,127,88,218,238,8,155,161,253,68,227,219,217,138,0,0,0,0,171,212,12,59,44,148,107,164,115,140,35,199,105,0,159,210,184,40,107,188,31,1,100,108,207,75,166,17,226,183,83,189,130,131,135,32,113,60,44,78,85,97,231,146,60,57,129,72,71,229,99,140,205,42,175,85,79,60,2,195,134,224,132,55,12,11,9,188,163,72,31,129,1,245,97,110,85,233,233,212,126,91,245,121,77,102,76,9,137,66,49,240,175,59,106,23,131,79,219,30,88,36,153,69,200,48,40,136,227,246,197,102,213,141,122,4,186,57,98,62,139,154,145,152,59,13,211,149,1,24,26,1,2,3,4,5,6,13,1,14,15,0,16,17,18,19,20,21,7,8,9,10,11,12,0,22,23,17,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}"`; diff --git a/test/transactions/metaplex.test.ts b/test/transactions/metaplex.test.ts index cb56a80..446b98f 100644 --- a/test/transactions/metaplex.test.ts +++ b/test/transactions/metaplex.test.ts @@ -2,12 +2,28 @@ import { AUCTION_EXTENDED_PUBKEY, AUCTION_MANAGER_PUBKEY, AUCTION_PUBKEY, + BID_METADATA_PUBKEY, + BID_REDEMPTION_PUBKEY, CURRENT_AUTHORITY_PUBKEY, + EDITION_MARK_PUBKEY, + FEE_PAYER, + MASTER_EDITION_PUBKEY, + METADATA_PUBKEY, mockTransaction, + NEW_EDITION_PUBKEY, + NEW_METADATA_PUBKEY, + PRIZE_TRACKING_TICKET_PUBKEY, + SAFETY_DEPOSIT_BOX_PUBKEY, + SAFETY_DEPOSIT_CONFIG_PUBKEY, + SAFETY_DEPOSIT_TOKEN_STORE_PUBKEY, serializeConfig, STORE_PUBKEY, + TOKEN_ACCOUNT_PUBKEY, + TOKEN_MINT_PUBKEY, + VAULT_PUBKEY, } from '../utils'; import { EndAuction } from '../../src/programs/metaplex/transactions/EndAuction'; +import { RedeemPrintingV2Bid } from '../../src/programs/metaplex/transactions/RedeemPrintingV2Bid'; import BN from 'bn.js'; describe('Metaplex transactions', () => { @@ -39,4 +55,33 @@ describe('Metaplex transactions', () => { // const serializedData = data.serialize(serializeConfig); // expect(JSON.stringify(serializedData)).toMatchSnapshot(); // }); + + test('RedeemPrintingV2Bid', async () => { + const data = new RedeemPrintingV2Bid(mockTransaction, { + store: STORE_PUBKEY, + vault: VAULT_PUBKEY, + auction: AUCTION_PUBKEY, + auctionManager: AUCTION_PUBKEY, + bidRedemption: BID_REDEMPTION_PUBKEY, + bidMetadata: BID_METADATA_PUBKEY, + safetyDepositTokenStore: SAFETY_DEPOSIT_TOKEN_STORE_PUBKEY, + destination: TOKEN_ACCOUNT_PUBKEY, + safetyDeposit: SAFETY_DEPOSIT_BOX_PUBKEY, + bidder: FEE_PAYER.publicKey, + safetyDepositConfig: SAFETY_DEPOSIT_CONFIG_PUBKEY, + auctionExtended: AUCTION_EXTENDED_PUBKEY, + newMint: TOKEN_MINT_PUBKEY, + newEdition: NEW_EDITION_PUBKEY, + newMetadata: NEW_METADATA_PUBKEY, + metadata: METADATA_PUBKEY, + masterEdition: MASTER_EDITION_PUBKEY, + editionMark: EDITION_MARK_PUBKEY, + prizeTrackingTicket: PRIZE_TRACKING_TICKET_PUBKEY, + winIndex: new BN(0), + editionOffset: new BN(0), + }); + + const serializedData = data.serialize(serializeConfig); + expect(JSON.stringify(serializedData)).toMatchSnapshot(); + }); }); diff --git a/test/utils.ts b/test/utils.ts index 8e2b686..4f9ccab 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -89,6 +89,21 @@ export const mockTransaction: TransactionCtorFields = { feePayer: new PublicKey('7J6QvJGCB22vDvYB33ikrWCXRBRsFY74ntAArSK4KJUn'), recentBlockhash: RECENT_ISH_BLOCKHASH, }; +export const BID_METADATA_PUBKEY = new PublicKey('CZkFeERacU42qjGTPyjamS13fNtz7y1wYLu5jyLpN1WL'); +export const BID_REDEMPTION_PUBKEY = new PublicKey('4CkQJBxhU8EZ1UjhfgbtdaPbpTe6mqf811fipYBFbSYN'); +export const SAFETY_DEPOSIT_TOKEN_STORE_PUBKEY = new PublicKey( + '4CkQJBxhU8EZ1UjhfgbtdaPbpTe6mqf811fipYBFbSNM', +); +export const SAFETY_DEPOSIT_CONFIG_PUBKEY = new PublicKey( + '4CkBUBxhU8EZ1UjhfgbtdaPbpTe6mqf811fipYBFbSNM', +); +export const NEW_EDITION_PUBKEY = new PublicKey('4CkBUBxhU8EZ1UjhfgbtdaPbpTe6mqf822fipYBFbSNM'); +export const NEW_METADATA_PUBKEY = new PublicKey('5jF6nAQ5GTK8rsdzW8hGCEsWjY9YCV2jXCwZ111BPsWz'); +export const EDITION_MARK_PUBKEY = new PublicKey('78qz3gehg9YqktdaYt6o71DSUPFQ41tLMACHpnFjdYdS'); +export const PRIZE_TRACKING_TICKET_PUBKEY = new PublicKey( + '78qz3gehg9YqktdaYt6o99DSUPFQ41tLMACHpnFjdYdS', +); + export const serializeConfig = { verifySignatures: false, requireAllSignatures: false }; export async function pause(ms: number) {