diff --git a/packages/common/src/actions/auction.ts b/packages/common/src/actions/auction.ts index 69821ec..7696a96 100644 --- a/packages/common/src/actions/auction.ts +++ b/packages/common/src/actions/auction.ts @@ -49,6 +49,18 @@ export const decodeAuction = (buffer: Buffer) => { return deserializeBorsh(AUCTION_SCHEMA, AuctionData, buffer) as AuctionData; }; +export const decodeBidderPot = (buffer: Buffer) => { + return deserializeBorsh(AUCTION_SCHEMA, BidderPot, buffer) as BidderPot; +}; + +export const decodeBidderMetadata = (buffer: Buffer) => { + return deserializeBorsh( + AUCTION_SCHEMA, + BidderMetadata, + buffer, + ) as BidderMetadata; +}; + export const BASE_AUCTION_DATA_SIZE = 32 + 32 + 32 + 8 + 8 + 1 + 9 + 9 + 9 + 9; export class AuctionData { @@ -127,8 +139,16 @@ export class BidderMetadata { export class BidderPot { /// Points at actual pot that is a token account bidderPot: PublicKey; - constructor(args: { bidderPot: PublicKey }) { + bidderAct: PublicKey; + auctionAct: PublicKey; + constructor(args: { + bidderPot: PublicKey; + bidderAct: PublicKey; + auctionAct: PublicKey; + }) { this.bidderPot = args.bidderPot; + this.bidderAct = args.bidderAct; + this.auctionAct = args.auctionAct; } } @@ -240,8 +260,8 @@ export const AUCTION_SCHEMA = new Map([ kind: 'struct', fields: [ ['instruction', 'u8'], - ['resource', 'pubkey'], ['amount', 'u64'], + ['resource', 'pubkey'], ], }, ], @@ -300,7 +320,11 @@ export const AUCTION_SCHEMA = new Map([ BidderPot, { kind: 'struct', - fields: [['bidderPot', 'pubkey']], + fields: [ + ['bidderPot', 'pubkey'], + ['bidderAct', 'pubkey'], + ['auctionAct', 'pubkey'], + ], }, ], ]); @@ -490,7 +514,7 @@ export async function placeBid( const keys = [ { pubkey: bidderPubkey, - isSigner: true, + isSigner: false, isWritable: true, }, { diff --git a/packages/metavinci/src/actions/sendPlaceBid.ts b/packages/metavinci/src/actions/sendPlaceBid.ts new file mode 100644 index 0000000..af157c8 --- /dev/null +++ b/packages/metavinci/src/actions/sendPlaceBid.ts @@ -0,0 +1,78 @@ +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + actions, + ParsedAccount, + SequenceType, + sendTransactionWithRetry, + placeBid, + programIds, + BidderPot, + models, +} from '@oyster/common'; + +import { AccountLayout } from '@solana/spl-token'; +import { AuctionView } from '../hooks'; +import BN from 'bn.js'; +const { createTokenAccount } = actions; +const { approve } = models; + +export async function sendPlaceBid( + connection: Connection, + wallet: any, + bidderAccount: PublicKey, + auctionView: AuctionView, + amount: number, +) { + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + let bidderPotTokenAccount: PublicKey; + if (!auctionView.myBidderPot) { + bidderPotTokenAccount = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt, + auctionView.auction.info.tokenMint, + programIds().auction, + signers, + ); + } else bidderPotTokenAccount = auctionView.myBidderPot?.info.bidderPot; + + const transferAuthority = approve( + instructions, + [], + bidderAccount, + wallet.publicKey, + amount, + ); + + signers.push(transferAuthority); + + await placeBid( + bidderAccount, + bidderPotTokenAccount, + auctionView.auction.info.tokenMint, + transferAuthority.publicKey, + wallet.publicKey, + auctionView.auctionManager.info.vault, + new BN(amount), + instructions, + ); + + await sendTransactionWithRetry( + connection, + wallet, + instructions, + signers, + 'single', + ); +} diff --git a/packages/metavinci/src/components/AuctionCard/index.tsx b/packages/metavinci/src/components/AuctionCard/index.tsx index 6084207..241bfb2 100644 --- a/packages/metavinci/src/components/AuctionCard/index.tsx +++ b/packages/metavinci/src/components/AuctionCard/index.tsx @@ -10,16 +10,21 @@ import { TokenAccount, useConnection, useUserAccounts, + hooks, + contexts, } from '@oyster/common'; import { AuctionView } from '../../hooks'; - +import { sendPlaceBid } from '../../actions/sendPlaceBid'; +const { useWallet } = contexts.Wallet; export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => { const [hours, setHours] = useState(23); const [minutes, setMinutes] = useState(59); const [seconds, setSeconds] = useState(59); const [clock, setClock] = useState(0); const connection = useConnection(); + const { wallet } = useWallet(); const { userAccounts } = useUserAccounts(); + const [value, setValue] = useState(); const accountByMint = userAccounts.reduce((prev, acc) => { prev.set(acc.info.mint.toBase58(), acc); return prev; @@ -84,14 +89,8 @@ export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => { - // props.setAttributes({ - // ...props.attributes, - // name: info.target.value, - // }) - // } + value={value} + onChange={setValue} />
{ type="primary" size="large" className="action-btn" + disabled={!myPayingAccount || value === undefined} + onClick={() => { + console.log('Auctionview', auctionView); + if (myPayingAccount && value) + sendPlaceBid( + connection, + wallet, + myPayingAccount.pubkey, + auctionView, + value, + ); + }} style={{ marginTop: 20 }} > PLACE BID diff --git a/packages/metavinci/src/contexts/meta.tsx b/packages/metavinci/src/contexts/meta.tsx index 6799e0d..b0e781a 100644 --- a/packages/metavinci/src/contexts/meta.tsx +++ b/packages/metavinci/src/contexts/meta.tsx @@ -20,6 +20,10 @@ import { SafetyDepositBox, VaultKey, decodeSafetyDeposit, + BidderMetadata, + decodeBidderMetadata, + BidderPot, + decodeBidderPot, } from '@oyster/common'; import { MintInfo } from '@solana/spl-token'; import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js'; @@ -42,10 +46,15 @@ export interface MetaContextState { masterEditions: Record>; auctionManagers: Record>; auctions: Record>; + bidderMetadataByAuctionAndBidder: Record< + string, + ParsedAccount + >; safetyDepositBoxesByVaultAndIndex: Record< string, ParsedAccount >; + bidderPotsByAuctionAndBidder: Record>; } const MetaContext = React.createContext({ @@ -56,7 +65,9 @@ const MetaContext = React.createContext({ editions: {}, auctionManagers: {}, auctions: {}, + bidderMetadataByAuctionAndBidder: {}, safetyDepositBoxesByVaultAndIndex: {}, + bidderPotsByAuctionAndBidder: {}, }); export function MetaProvider({ children = null as any }) { @@ -80,6 +91,14 @@ export function MetaProvider({ children = null as any }) { const [auctions, setAuctions] = useState< Record> >({}); + const [ + bidderMetadataByAuctionAndBidder, + setBidderMetadataByAuctionAndBidder, + ] = useState>>({}); + const [ + bidderPotsByAuctionAndBidder, + setBidderPotsByAuctionAndBidder, + ] = useState>>({}); const [ safetyDepositBoxesByVaultAndIndex, setSafetyDepositBoxesByVaultAndIndex, @@ -107,6 +126,42 @@ export function MetaProvider({ children = null as any }) { } catch { // ignore errors // add type as first byte for easier deserialization + try { + const bidderMetadata = await decodeBidderMetadata(a.account.data); + + const account: ParsedAccount = { + pubkey: a.pubkey, + account: a.account, + info: bidderMetadata, + }; + setBidderMetadataByAuctionAndBidder(e => ({ + ...e, + [bidderMetadata.auctionPubkey.toBase58() + + '-' + + bidderMetadata.bidderPubkey.toBase58()]: account, + })); + } catch { + // ignore errors + // add type as first byte for easier deserialization + try { + const bidderPot = await decodeBidderPot(a.account.data); + + const account: ParsedAccount = { + pubkey: a.pubkey, + account: a.account, + info: bidderPot, + }; + setBidderPotsByAuctionAndBidder(e => ({ + ...e, + [bidderPot.auctionAct.toBase58() + + '-' + + bidderPot.bidderAct.toBase58()]: account, + })); + } catch { + // ignore errors + // add type as first byte for easier deserialization + } + } } }; @@ -355,6 +410,8 @@ export function MetaProvider({ children = null as any }) { auctions, metadataByMint, safetyDepositBoxesByVaultAndIndex, + bidderMetadataByAuctionAndBidder, + bidderPotsByAuctionAndBidder, }} > {children} diff --git a/packages/metavinci/src/hooks/useAuction.ts b/packages/metavinci/src/hooks/useAuction.ts index 3dabdb2..2619b24 100644 --- a/packages/metavinci/src/hooks/useAuction.ts +++ b/packages/metavinci/src/hooks/useAuction.ts @@ -1,12 +1,19 @@ -import { useConnection } from '@oyster/common'; +import { TokenAccount, useConnection, useUserAccounts } from '@oyster/common'; import { useEffect, useState } from 'react'; import { AuctionView, processAccountsIntoAuctionView } from '.'; import { useMeta } from '../contexts'; export const useAuction = (id: string) => { const connection = useConnection(); + const { userAccounts } = useUserAccounts(); + const accountByMint = userAccounts.reduce((prev, acc) => { + prev.set(acc.info.mint.toBase58(), acc); + return prev; + }, new Map()); const [clock, setClock] = useState(0); - const [auctionView, setAuctionView] = useState(null); + const [existingAuctionView, setAuctionView] = useState( + null, + ); useEffect(() => { connection.getSlot().then(setClock); }, [connection]); @@ -16,6 +23,8 @@ export const useAuction = (id: string) => { auctionManagers, safetyDepositBoxesByVaultAndIndex, metadataByMint, + bidderMetadataByAuctionAndBidder, + bidderPotsByAuctionAndBidder, } = useMeta(); useEffect(() => { @@ -26,8 +35,12 @@ export const useAuction = (id: string) => { auctionManagers, safetyDepositBoxesByVaultAndIndex, metadataByMint, + bidderMetadataByAuctionAndBidder, + bidderPotsByAuctionAndBidder, + accountByMint, clock, undefined, + existingAuctionView || undefined, ); if (auctionView) setAuctionView(auctionView); } @@ -37,6 +50,9 @@ export const useAuction = (id: string) => { auctionManagers, safetyDepositBoxesByVaultAndIndex, metadataByMint, + bidderMetadataByAuctionAndBidder, + bidderPotsByAuctionAndBidder, + userAccounts, ]); - return auctionView; + return existingAuctionView; }; diff --git a/packages/metavinci/src/hooks/useAuctions.ts b/packages/metavinci/src/hooks/useAuctions.ts index c5d413b..be19a3f 100644 --- a/packages/metavinci/src/hooks/useAuctions.ts +++ b/packages/metavinci/src/hooks/useAuctions.ts @@ -5,6 +5,11 @@ import { AuctionData, useConnection, AuctionState, + BidderMetadata, + BidderPot, + useWallet, + useUserAccounts, + TokenAccount, } from '@oyster/common'; import { useEffect, useState } from 'react'; import { useMeta } from '../contexts'; @@ -30,12 +35,23 @@ export interface AuctionView { openEditionItem?: AuctionViewItem; state: AuctionViewState; thumbnail: AuctionViewItem; + myBidderMetadata?: ParsedAccount; + myBidderPot?: ParsedAccount; + totallyComplete: boolean; } export const useAuctions = (state: AuctionViewState) => { const connection = useConnection(); + const { userAccounts } = useUserAccounts(); + const accountByMint = userAccounts.reduce((prev, acc) => { + prev.set(acc.info.mint.toBase58(), acc); + return prev; + }, new Map()); + const [clock, setClock] = useState(0); - const [auctionViews, setAuctionViews] = useState([]); + const [auctionViews, setAuctionViews] = useState< + Record + >({}); useEffect(() => { connection.getSlot().then(setClock); }, [connection]); @@ -45,23 +61,28 @@ export const useAuctions = (state: AuctionViewState) => { auctionManagers, safetyDepositBoxesByVaultAndIndex, metadataByMint, + bidderMetadataByAuctionAndBidder, + bidderPotsByAuctionAndBidder, } = useMeta(); useEffect(() => { - const newAuctionViews: AuctionView[] = []; Object.keys(auctions).forEach(a => { const auction = auctions[a]; - const auctionView = processAccountsIntoAuctionView( + const existingAuctionView = auctionViews[a]; + const nextAuctionView = processAccountsIntoAuctionView( auction, auctionManagers, safetyDepositBoxesByVaultAndIndex, metadataByMint, + bidderMetadataByAuctionAndBidder, + bidderPotsByAuctionAndBidder, + accountByMint, clock, state, + existingAuctionView, ); - if (auctionView) newAuctionViews.push(auctionView); + setAuctionViews(nA => ({ ...nA, [a]: nextAuctionView })); }); - setAuctionViews(newAuctionViews); }, [ clock, state, @@ -69,9 +90,12 @@ export const useAuctions = (state: AuctionViewState) => { auctionManagers, safetyDepositBoxesByVaultAndIndex, metadataByMint, + bidderMetadataByAuctionAndBidder, + bidderPotsByAuctionAndBidder, + userAccounts, ]); - return auctionViews; + return Object.values(auctionViews).filter(v => v) as AuctionView[]; }; export function processAccountsIntoAuctionView( @@ -82,9 +106,16 @@ export function processAccountsIntoAuctionView( ParsedAccount >, metadataByMint: Record>, + bidderMetadataByAuctionAndBidder: Record< + string, + ParsedAccount + >, + bidderPotsByAuctionAndBidder: Record>, + accountByMint: Map, clock: number, desiredState: AuctionViewState | undefined, -) { + existingAuctionView?: AuctionView, +): AuctionView | undefined { let state: AuctionViewState; if ( auction.info.state == AuctionState.Ended || @@ -102,12 +133,32 @@ export function processAccountsIntoAuctionView( state = AuctionViewState.BuyNow; } - if (desiredState && desiredState != state) return null; + if (desiredState && desiredState != state) return undefined; + + const myPayingAccount = accountByMint.get(auction.info.tokenMint.toBase58()); const auctionManager = auctionManagers[auction.info.auctionManagerKey?.toBase58() || '']; if (auctionManager) { + const boxesExpected = auctionManager.info.state.winningConfigsValidated; + const bidderMetadata = + bidderMetadataByAuctionAndBidder[ + auction.pubkey.toBase58() + '-' + myPayingAccount?.pubkey.toBase58() + ]; + const bidderPot = + bidderPotsByAuctionAndBidder[ + auction.pubkey.toBase58() + '-' + myPayingAccount?.pubkey.toBase58() + ]; + if (existingAuctionView && existingAuctionView.totallyComplete) { + // If totally complete, we know we arent updating anythign else, let's speed things up + // and only update the two things that could possibly change + existingAuctionView.myBidderPot = bidderPot; + existingAuctionView.myBidderMetadata = bidderMetadata; + return existingAuctionView; + } + let boxes: ParsedAccount[] = []; + let box = safetyDepositBoxesByVaultAndIndex[ auctionManager.info.vault.toBase58() + '-0' @@ -126,7 +177,7 @@ export function processAccountsIntoAuctionView( } if (boxes.length > 0) { - let view: any = { + let view: Partial = { auction, auctionManager, state, @@ -150,13 +201,22 @@ export function processAccountsIntoAuctionView( boxes[auctionManager.info.settings.openEditionConfig], } : undefined, + myBidderMetadata: bidderMetadata, + myBidderPot: bidderPot, }; - view.thumbnail = view.items[0] || view.openEditionItem; - if (!view.thumbnail || !view.thumbnail.metadata) return null; - return view; + view.thumbnail = (view.items || [])[0] || view.openEditionItem; + view.totallyComplete = !!( + view.thumbnail && + boxesExpected == (view.items || []).length && + (auctionManager.info.settings.openEditionConfig == null || + (auctionManager.info.settings.openEditionConfig != null && + view.openEditionItem)) + ); + if (!view.thumbnail || !view.thumbnail.metadata) return undefined; + return view as AuctionView; } } - return null; + return undefined; }