UNPKG

@deserialize/auction-sdk

Version:

An SDK for Auction on SVMs

425 lines (365 loc) 18.8 kB
import * as anchor from "@coral-xyz/anchor"; import { PublicKey, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js"; import { createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { ListingState, Listing as ListingType } from "../types/listing"; import ProgramLoader from "./ProgramLoader"; import { BidInfo, BidReceipt as BidReceiptType } from "../types/bid-receipt"; import BidReceipt from "./BidReciept"; import AuctionManager from "./AuctionManager"; /** * Listing * * @description This is the main contract for the listing. It is responsible for managing the listing, including creating and updating the listing, and executing bids. * * - create a new listing * - get a listing * - fetch many listings * - get all listings * - cancel a listing * - get all bids for a listing * - get listings by state * - get one-time sale listings * - get auction listings * - get highest bid on a listing * - get highest bid amount and bidder * - get bids to refund on a listing and bidder * - then make a call to close the nft account and auction manager account */ export default class Listing { private auctionManagerProgram: AuctionManager; public constructor(protected readonly programLoader: ProgramLoader) { this.auctionManagerProgram = new AuctionManager(programLoader); } /** * @description Create a new listing * * @param {PublicKey} nft - The NFT to be listed * @param {PublicKey} tokenMint - The token mint to be used for the listing * @param {PublicKey} tokenMintProgramId - The token mint program ID (SPL Token or SPL Token 2022) * * @param {PublicKey} lister - The lister of the listing * @param {number} timeExtension - The time extension for the listing * @param {number} startingPrice - The starting price for the listing * @param {number} period - The period for the listing * @returns {TransactionInstruction[]} */ public async create(nft: PublicKey, nftProgramId: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey, lister: PublicKey, timeExtension: number, startingPrice: number, period?: number, minimumBidThreshold?: number): Promise<TransactionInstruction[]> { try { // const seeds1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())] // const [applicationState, _] = PublicKey.findProgramAddressSync(seeds1, this.programLoader.program.programId); const seeds2 = [anchor.utils.bytes.utf8.encode("auction_manager"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(lister.toBuffer())]; const [auctionManager, auctionManagerBump] = PublicKey.findProgramAddressSync(seeds2, this.programLoader.program.programId); console.log("[*] Auction manager: ", auctionManager); console.log("[*] Auction manager bump: ", auctionManagerBump); const seeds3 = [anchor.utils.bytes.utf8.encode("listing"), Uint8Array.from(nft.toBuffer()), Uint8Array.from(lister.toBuffer()), Uint8Array.from(auctionManager.toBuffer()), Uint8Array.from(tokenMint.toBuffer())]; const [listing, listingBump] = PublicKey.findProgramAddressSync(seeds3, this.programLoader.program.programId); console.log("[*] Listing: ", listing); console.log("[*] Listing bump: ", listingBump); let appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState) const seeds = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(lister.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")]; const [userdata, userdataBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId); console.log("[*] Userdata: ", userdata); console.log("[*] Userdata bump: ", userdataBump); let txns: TransactionInstruction[] = []; //check if there's an nft escrow account let nftEscrow = await getAssociatedTokenAddress(nft, auctionManager, true); let nftOwner = await getAssociatedTokenAddress(nft, lister, true); let proceedsWallet = await getAssociatedTokenAddress(tokenMint, lister, true, tokenMintProgramId); //check if the accounts exists let nftEscrowAccountInfo = await this.programLoader.connection.getAccountInfo(nftEscrow); let nftOwnerAccountInfo = await this.programLoader.connection.getAccountInfo(nftOwner); let proceedsWalletAccountInfo = await this.programLoader.connection.getAccountInfo(proceedsWallet); let auctionManagerAccountInfo = await this.programLoader.connection.getAccountInfo(auctionManager); if (!auctionManagerAccountInfo) { let inx = await this.auctionManagerProgram.create(nft, lister, nftProgramId); txns.push(inx); } if (!nftEscrowAccountInfo) { //create the transactions let inx = createAssociatedTokenAccountInstruction(lister, nftEscrow, auctionManager, nft); txns.push(inx); } if (!nftOwnerAccountInfo) { //create the transactions let inx = createAssociatedTokenAccountInstruction(lister, nftOwner, lister, nft); txns.push(inx); } if (!proceedsWalletAccountInfo) { //create the transactions let inx = createAssociatedTokenAccountInstruction(lister, proceedsWallet, lister, tokenMint, tokenMintProgramId); txns.push(inx); } const startTime = Math.floor(Date.now() / 1000); let endTime = null; if (period) { endTime = startTime + ((60 * 60 * 24) * period); } console.log('tokenMintProgramId: ', tokenMintProgramId); const tx = await this.programLoader.program.methods .createListing(new anchor.BN(timeExtension), new anchor.BN(startingPrice), new anchor.BN(startTime), endTime ? new anchor.BN(endTime) : endTime, new anchor.BN(minimumBidThreshold)) .accounts({ applicationState: this.programLoader.applicationState, auctionManager, listing, owner: lister, userdata, nft, nftEscrow, proceedsWallet, tokenMint, feeAccount: appState.feeAccount, nftOwner: nftOwner, tokenProgram: nftProgramId, systemProgram: SystemProgram.programId, rent: anchor.web3.SYSVAR_RENT_PUBKEY }) .instruction() txns.push(tx); return txns; } catch (e: any) { console.error(e); throw new Error(`Failed to create listing: ${e.message}`); } } /** * @description Create a new listing * * @param {PublicKey} nft - The NFT to be listed * @param {PublicKey} nftProgramId - The NFT to be listed * @param {PublicKey} tokenMint - The token mint to be used for the listing * @param {PublicKey} tokenMintProgramId - The token mint to be used for the listing * @param {PublicKey} lister - The lister of the listing * @param {number} rewardPercentage - The reward percentage for the lister * @param {number} timeExtension - The time extension for the listing * @param {number} startingPrice - The starting price for the listing * @param {number} period - The period for the listing * @returns {Transaction} */ public async createTransaction(nft: PublicKey, nftProgramId: PublicKey, tokenMint: PublicKey, tokenMintProgramId: PublicKey, lister: PublicKey, timeExtension: number, startingPrice: number, period?: number, minimumBidThreshold?: number): Promise<Transaction> { console.log('tokenMintProgramId 1: ', tokenMintProgramId); try { let instructions = await this.create(nft, nftProgramId, tokenMint, tokenMintProgramId, lister, timeExtension, startingPrice, period, minimumBidThreshold); let txn = new Transaction().add(...instructions); txn.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash; txn.feePayer = lister; return txn; } catch (e: any) { console.error(e); throw new Error(`Failed to create listing transaction: ${e.message}`); } } /** * @description Get a listing * * @param {PublicKey} listingPubkey - The public key of the listing * @returns {ListingType} The listing */ public async get(listingPubkey: PublicKey): Promise<ListingType> { try { console.log("[*] Listing publicKey is " + listingPubkey); const listing = await this.programLoader.program.account.listing.fetch(listingPubkey); console.log(JSON.stringify(listing, null, 4)); return listing as unknown as ListingType; } catch (e: any) { console.error(e); throw new Error(`Failed to get listing: ${e.message}`); } } /** * @description Fetch many listings * * @param {PublicKey[]} listingPubkeys - The public keys of the listings * @returns {ListingType[]} The listings */ public async fetchMany(listingPubkeys: PublicKey[]): Promise<ListingType[]> { try { const listings = await this.programLoader.program.account.listing.fetchMultiple(listingPubkeys); return listings as unknown as ListingType[]; } catch (e: any) { console.error(e); throw new Error(`Failed to fetch listings: ${e.message}`); } } /** * @description Get all listings * * @returns {ListingType[]} The listings */ public async getAll(): Promise<ListingType[]> { try { const listings = await this.programLoader.program.account.listing.all(); console.log("[*] Listings", listings); return listings as unknown as ListingType[]; } catch (e: any) { console.error(e); throw new Error(`Failed to get all listings: ${e.message}`); } } /** * @description Cancel a listing * * @param {PublicKey} listingPubkey - The public key of the listing * @param {PublicKey} tokenProgramId - The program ID of the token mint * @returns {Transaction} */ public async cancel(listingPubkey: PublicKey, tokenProgramId: PublicKey): Promise<Transaction> { try { let listingData = await this.get(listingPubkey); // const seeds1 = [anchor.utils.bytes.utf8.encode("deserialize"), Uint8Array.from(this.programLoader.wallet.publicKey.toBuffer())] // const [applicationState, applicationStateBump] = PublicKey.findProgramAddressSync(seeds1, this.programLoader.program.programId); const seeds2 = [anchor.utils.bytes.utf8.encode("auction_manager"), Uint8Array.from(listingData.nft.toBuffer()), Uint8Array.from(listingData.authority.toBuffer())]; const [auctionManager, auctionManagerBump] = PublicKey.findProgramAddressSync(seeds2, this.programLoader.program.programId); const seeds3 = [anchor.utils.bytes.utf8.encode("listing"), Uint8Array.from(listingData.nft.toBuffer()), Uint8Array.from(listingData.authority.toBuffer()), Uint8Array.from(auctionManager.toBuffer()), Uint8Array.from(listingData.tokenMint.toBuffer())]; const [listing, listingBump] = PublicKey.findProgramAddressSync(seeds3, this.programLoader.program.programId); console.log("[*] Listing: ", listing); console.log("[*] Listing bump: ", listingBump); let appState = await this.programLoader.program.account.applicationState.fetch(this.programLoader.applicationState) const seeds = [anchor.utils.bytes.utf8.encode("user_account"), Uint8Array.from(listingData.authority.toBuffer()), Uint8Array.from(appState.listingAuthority.toBuffer()), anchor.utils.bytes.utf8.encode("deserialize")]; const [userdata, userdataBump] = PublicKey.findProgramAddressSync(seeds, this.programLoader.program.programId); console.log("[*] Userdata: ", userdata); console.log("[*] Userdata bump: ", userdataBump); //check if there's an nft escrow account let nftEscrow = await getAssociatedTokenAddress(listingData.nft, auctionManager, true); let originalNftHolder = await getAssociatedTokenAddress(listingData.nft, listingData.authority, true); const cancelledAt = Math.floor(Date.now() / 1000); const tx = await this.programLoader.program.methods .cancelListing(new anchor.BN(cancelledAt)) .accounts({ applicationState: this.programLoader.applicationState, auctionManager, listing, userdata, nft: listingData.nft, nftEscrow, originalNftHolder, tokenMint: listingData.tokenMint, feeAccount: appState.feeAccount, user: listingData.authority, tokenProgram: tokenProgramId, systemProgram: SystemProgram.programId, }) .transaction() tx.recentBlockhash = (await this.programLoader.getRecentBlockHash()).blockhash; tx.feePayer = listingData.authority; return tx; } catch (e: any) { console.error(e); throw new Error(`Failed to cancel listing (${listingPubkey}): ${e.message}`); } } /** * @description Get all bids for a listing * * @param {PublicKey} listingPubkey - The public key of the listing * @returns {BidReceiptType[]} The bids */ public async getBids(listingPubkey: PublicKey): Promise<BidReceiptType[]> { try { const bidReceiptProgram = new BidReceipt(this.programLoader); const listing = await this.programLoader.program.account.listing.fetch(listingPubkey); const bids = await bidReceiptProgram.fetchMany(listing.bidReceipts); return bids; } catch (e: any) { console.error(e); throw new Error(`Failed to get bids for listing (${listingPubkey}): ${e.message}`); } } /** * @description Get listings by state * * @param {ListingState} state - The state of the listings * @returns {ListingType[]} The listings */ public async getByState(state: ListingState): Promise<ListingType[]> { try { const allListings = await this.getAll(); const listings = allListings.filter((listing) => listing.state === state); return listings; } catch (e: any) { console.error(e) throw new Error(`Failed to get listings by state (${state}): ${e.message}`); } } /** * @description Get one-time sale listings * * @returns {ListingType[]} The one-time sale listings */ public async getOneTimeSales(): Promise<ListingType[]> { try { const allListings = await this.getAll(); const oneTimeSaleListings = allListings.filter(listing => listing.endTime === null); return oneTimeSaleListings; } catch (e: any) { console.error(e); throw new Error(`Failed to get one-time sale listings: ${e.message}`); } } /** * @description Get auction listings * * @returns {ListingType[]} The auction listings */ public async getAuctions(): Promise<ListingType[]> { try { const allListings = await this.getAll(); const auctionListings = allListings.filter(listing => listing.endTime !== null); return auctionListings; } catch (e: any) { console.error(e); throw new Error(`Failed to get auction listings: ${e.message}`); } } /** * @description Get the highest bid info * * @param {PublicKey} listingPubkey - The public key of the listing * @returns {BidInfo} The highest bid info */ public async getHighestBidInfo(listingPubkey: PublicKey): Promise<BidInfo> { try { const bidReceiptProgram = new BidReceipt(this.programLoader); const listing = await this.get(listingPubkey); if (!listing.highestBidReceipt) throw new Error("No bids on this listing"); const bidReceipt = await bidReceiptProgram.get(listing.highestBidReceipt); return { bidder: bidReceipt.bidder, receipt: listing.highestBidReceipt, amount: bidReceipt.amount } } catch (e: any) { console.error(e) throw new Error(`Failed to get highest bid info: ${e.message}`); } } /** * @description Get bids info to refund * * @param {PublicKey} listingPubkey - The public key of the listing * @returns {BidInfo[]} The bids info to refund */ public async getBidsInfoToRefund(listingPubkey: PublicKey): Promise<BidInfo[]> { try { const listing = await this.get(listingPubkey); const bidsToRefund = listing.bidReceipts.filter(receipt => receipt !== listing.highestBidReceipt); const bidReceiptProgram = new BidReceipt(this.programLoader); if (listing.bidReceipts.length === 0) return []; const bidInfos: BidInfo[] = []; for await (let receipt of bidsToRefund) { if (!receipt.equals(listing.highestBidReceipt)) { let bidReceipt = await bidReceiptProgram.get(receipt); let bidInfo = { bidder: bidReceipt.bidder, receipt, amount: bidReceipt.amount } bidInfos.push(bidInfo); } } return bidInfos; } catch (e: any) { console.error(e); throw new Error(`Failed to get bids info to refund: ${e.message}`); } } }