@deserialize/auction-sdk
Version:
An SDK for Auction on SVMs
425 lines (365 loc) • 18.8 kB
text/typescript
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}`);
}
}
}