Merge remote-tracking branch 'origin/feature/m-jordan' into feature/m

This commit is contained in:
bartosz-lipinski 2021-04-26 21:48:02 -05:00
commit bb3f9fd192
6 changed files with 275 additions and 29 deletions

View File

@ -49,6 +49,18 @@ export const decodeAuction = (buffer: Buffer) => {
return deserializeBorsh(AUCTION_SCHEMA, AuctionData, buffer) as AuctionData; 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 const BASE_AUCTION_DATA_SIZE = 32 + 32 + 32 + 8 + 8 + 1 + 9 + 9 + 9 + 9;
export class AuctionData { export class AuctionData {
@ -127,8 +139,16 @@ export class BidderMetadata {
export class BidderPot { export class BidderPot {
/// Points at actual pot that is a token account /// Points at actual pot that is a token account
bidderPot: PublicKey; bidderPot: PublicKey;
constructor(args: { bidderPot: PublicKey }) { bidderAct: PublicKey;
auctionAct: PublicKey;
constructor(args: {
bidderPot: PublicKey;
bidderAct: PublicKey;
auctionAct: PublicKey;
}) {
this.bidderPot = args.bidderPot; this.bidderPot = args.bidderPot;
this.bidderAct = args.bidderAct;
this.auctionAct = args.auctionAct;
} }
} }
@ -240,8 +260,8 @@ export const AUCTION_SCHEMA = new Map<any, any>([
kind: 'struct', kind: 'struct',
fields: [ fields: [
['instruction', 'u8'], ['instruction', 'u8'],
['resource', 'pubkey'],
['amount', 'u64'], ['amount', 'u64'],
['resource', 'pubkey'],
], ],
}, },
], ],
@ -300,7 +320,11 @@ export const AUCTION_SCHEMA = new Map<any, any>([
BidderPot, BidderPot,
{ {
kind: 'struct', kind: 'struct',
fields: [['bidderPot', 'pubkey']], fields: [
['bidderPot', 'pubkey'],
['bidderAct', 'pubkey'],
['auctionAct', 'pubkey'],
],
}, },
], ],
]); ]);
@ -490,7 +514,7 @@ export async function placeBid(
const keys = [ const keys = [
{ {
pubkey: bidderPubkey, pubkey: bidderPubkey,
isSigner: true, isSigner: false,
isWritable: true, isWritable: true,
}, },
{ {

View File

@ -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',
);
}

View File

@ -10,16 +10,21 @@ import {
TokenAccount, TokenAccount,
useConnection, useConnection,
useUserAccounts, useUserAccounts,
hooks,
contexts,
} from '@oyster/common'; } from '@oyster/common';
import { AuctionView } from '../../hooks'; import { AuctionView } from '../../hooks';
import { sendPlaceBid } from '../../actions/sendPlaceBid';
const { useWallet } = contexts.Wallet;
export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => { export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
const [hours, setHours] = useState<number>(23); const [hours, setHours] = useState<number>(23);
const [minutes, setMinutes] = useState<number>(59); const [minutes, setMinutes] = useState<number>(59);
const [seconds, setSeconds] = useState<number>(59); const [seconds, setSeconds] = useState<number>(59);
const [clock, setClock] = useState<number>(0); const [clock, setClock] = useState<number>(0);
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet();
const { userAccounts } = useUserAccounts(); const { userAccounts } = useUserAccounts();
const [value, setValue] = useState<number>();
const accountByMint = userAccounts.reduce((prev, acc) => { const accountByMint = userAccounts.reduce((prev, acc) => {
prev.set(acc.info.mint.toBase58(), acc); prev.set(acc.info.mint.toBase58(), acc);
return prev; return prev;
@ -84,14 +89,8 @@ export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
<InputNumber <InputNumber
autoFocus autoFocus
className="input" className="input"
placeholder="Max 50 characters" value={value}
// value={props.attributes.name} onChange={setValue}
// onChange={info =>
// props.setAttributes({
// ...props.attributes,
// name: info.target.value,
// })
// }
/> />
<div <div
@ -106,6 +105,18 @@ export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
type="primary" type="primary"
size="large" size="large"
className="action-btn" 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 }} style={{ marginTop: 20 }}
> >
PLACE BID PLACE BID

View File

@ -20,6 +20,10 @@ import {
SafetyDepositBox, SafetyDepositBox,
VaultKey, VaultKey,
decodeSafetyDeposit, decodeSafetyDeposit,
BidderMetadata,
decodeBidderMetadata,
BidderPot,
decodeBidderPot,
} from '@oyster/common'; } from '@oyster/common';
import { MintInfo } from '@solana/spl-token'; import { MintInfo } from '@solana/spl-token';
import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js'; import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js';
@ -42,10 +46,15 @@ export interface MetaContextState {
masterEditions: Record<string, ParsedAccount<MasterEdition>>; masterEditions: Record<string, ParsedAccount<MasterEdition>>;
auctionManagers: Record<string, ParsedAccount<AuctionManager>>; auctionManagers: Record<string, ParsedAccount<AuctionManager>>;
auctions: Record<string, ParsedAccount<AuctionData>>; auctions: Record<string, ParsedAccount<AuctionData>>;
bidderMetadataByAuctionAndBidder: Record<
string,
ParsedAccount<BidderMetadata>
>;
safetyDepositBoxesByVaultAndIndex: Record< safetyDepositBoxesByVaultAndIndex: Record<
string, string,
ParsedAccount<SafetyDepositBox> ParsedAccount<SafetyDepositBox>
>; >;
bidderPotsByAuctionAndBidder: Record<string, ParsedAccount<BidderPot>>;
} }
const MetaContext = React.createContext<MetaContextState>({ const MetaContext = React.createContext<MetaContextState>({
@ -56,7 +65,9 @@ const MetaContext = React.createContext<MetaContextState>({
editions: {}, editions: {},
auctionManagers: {}, auctionManagers: {},
auctions: {}, auctions: {},
bidderMetadataByAuctionAndBidder: {},
safetyDepositBoxesByVaultAndIndex: {}, safetyDepositBoxesByVaultAndIndex: {},
bidderPotsByAuctionAndBidder: {},
}); });
export function MetaProvider({ children = null as any }) { export function MetaProvider({ children = null as any }) {
@ -80,6 +91,14 @@ export function MetaProvider({ children = null as any }) {
const [auctions, setAuctions] = useState< const [auctions, setAuctions] = useState<
Record<string, ParsedAccount<AuctionData>> Record<string, ParsedAccount<AuctionData>>
>({}); >({});
const [
bidderMetadataByAuctionAndBidder,
setBidderMetadataByAuctionAndBidder,
] = useState<Record<string, ParsedAccount<BidderMetadata>>>({});
const [
bidderPotsByAuctionAndBidder,
setBidderPotsByAuctionAndBidder,
] = useState<Record<string, ParsedAccount<BidderPot>>>({});
const [ const [
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
setSafetyDepositBoxesByVaultAndIndex, setSafetyDepositBoxesByVaultAndIndex,
@ -107,6 +126,42 @@ export function MetaProvider({ children = null as any }) {
} catch { } catch {
// ignore errors // ignore errors
// add type as first byte for easier deserialization // add type as first byte for easier deserialization
try {
const bidderMetadata = await decodeBidderMetadata(a.account.data);
const account: ParsedAccount<BidderMetadata> = {
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<BidderPot> = {
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, auctions,
metadataByMint, metadataByMint,
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
}} }}
> >
{children} {children}

View File

@ -1,12 +1,19 @@
import { useConnection } from '@oyster/common'; import { TokenAccount, useConnection, useUserAccounts } from '@oyster/common';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { AuctionView, processAccountsIntoAuctionView } from '.'; import { AuctionView, processAccountsIntoAuctionView } from '.';
import { useMeta } from '../contexts'; import { useMeta } from '../contexts';
export const useAuction = (id: string) => { export const useAuction = (id: string) => {
const connection = useConnection(); const connection = useConnection();
const { userAccounts } = useUserAccounts();
const accountByMint = userAccounts.reduce((prev, acc) => {
prev.set(acc.info.mint.toBase58(), acc);
return prev;
}, new Map<string, TokenAccount>());
const [clock, setClock] = useState<number>(0); const [clock, setClock] = useState<number>(0);
const [auctionView, setAuctionView] = useState<AuctionView | null>(null); const [existingAuctionView, setAuctionView] = useState<AuctionView | null>(
null,
);
useEffect(() => { useEffect(() => {
connection.getSlot().then(setClock); connection.getSlot().then(setClock);
}, [connection]); }, [connection]);
@ -16,6 +23,8 @@ export const useAuction = (id: string) => {
auctionManagers, auctionManagers,
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
metadataByMint, metadataByMint,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
} = useMeta(); } = useMeta();
useEffect(() => { useEffect(() => {
@ -26,8 +35,12 @@ export const useAuction = (id: string) => {
auctionManagers, auctionManagers,
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
metadataByMint, metadataByMint,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
accountByMint,
clock, clock,
undefined, undefined,
existingAuctionView || undefined,
); );
if (auctionView) setAuctionView(auctionView); if (auctionView) setAuctionView(auctionView);
} }
@ -37,6 +50,9 @@ export const useAuction = (id: string) => {
auctionManagers, auctionManagers,
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
metadataByMint, metadataByMint,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
userAccounts,
]); ]);
return auctionView; return existingAuctionView;
}; };

View File

@ -5,6 +5,11 @@ import {
AuctionData, AuctionData,
useConnection, useConnection,
AuctionState, AuctionState,
BidderMetadata,
BidderPot,
useWallet,
useUserAccounts,
TokenAccount,
} from '@oyster/common'; } from '@oyster/common';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useMeta } from '../contexts'; import { useMeta } from '../contexts';
@ -30,12 +35,23 @@ export interface AuctionView {
openEditionItem?: AuctionViewItem; openEditionItem?: AuctionViewItem;
state: AuctionViewState; state: AuctionViewState;
thumbnail: AuctionViewItem; thumbnail: AuctionViewItem;
myBidderMetadata?: ParsedAccount<BidderMetadata>;
myBidderPot?: ParsedAccount<BidderPot>;
totallyComplete: boolean;
} }
export const useAuctions = (state: AuctionViewState) => { export const useAuctions = (state: AuctionViewState) => {
const connection = useConnection(); const connection = useConnection();
const { userAccounts } = useUserAccounts();
const accountByMint = userAccounts.reduce((prev, acc) => {
prev.set(acc.info.mint.toBase58(), acc);
return prev;
}, new Map<string, TokenAccount>());
const [clock, setClock] = useState<number>(0); const [clock, setClock] = useState<number>(0);
const [auctionViews, setAuctionViews] = useState<AuctionView[]>([]); const [auctionViews, setAuctionViews] = useState<
Record<string, AuctionView | undefined>
>({});
useEffect(() => { useEffect(() => {
connection.getSlot().then(setClock); connection.getSlot().then(setClock);
}, [connection]); }, [connection]);
@ -45,23 +61,28 @@ export const useAuctions = (state: AuctionViewState) => {
auctionManagers, auctionManagers,
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
metadataByMint, metadataByMint,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
} = useMeta(); } = useMeta();
useEffect(() => { useEffect(() => {
const newAuctionViews: AuctionView[] = [];
Object.keys(auctions).forEach(a => { Object.keys(auctions).forEach(a => {
const auction = auctions[a]; const auction = auctions[a];
const auctionView = processAccountsIntoAuctionView( const existingAuctionView = auctionViews[a];
const nextAuctionView = processAccountsIntoAuctionView(
auction, auction,
auctionManagers, auctionManagers,
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
metadataByMint, metadataByMint,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
accountByMint,
clock, clock,
state, state,
existingAuctionView,
); );
if (auctionView) newAuctionViews.push(auctionView); setAuctionViews(nA => ({ ...nA, [a]: nextAuctionView }));
}); });
setAuctionViews(newAuctionViews);
}, [ }, [
clock, clock,
state, state,
@ -69,9 +90,12 @@ export const useAuctions = (state: AuctionViewState) => {
auctionManagers, auctionManagers,
safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex,
metadataByMint, metadataByMint,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
userAccounts,
]); ]);
return auctionViews; return Object.values(auctionViews).filter(v => v) as AuctionView[];
}; };
export function processAccountsIntoAuctionView( export function processAccountsIntoAuctionView(
@ -82,9 +106,16 @@ export function processAccountsIntoAuctionView(
ParsedAccount<SafetyDepositBox> ParsedAccount<SafetyDepositBox>
>, >,
metadataByMint: Record<string, ParsedAccount<Metadata>>, metadataByMint: Record<string, ParsedAccount<Metadata>>,
bidderMetadataByAuctionAndBidder: Record<
string,
ParsedAccount<BidderMetadata>
>,
bidderPotsByAuctionAndBidder: Record<string, ParsedAccount<BidderPot>>,
accountByMint: Map<string, TokenAccount>,
clock: number, clock: number,
desiredState: AuctionViewState | undefined, desiredState: AuctionViewState | undefined,
) { existingAuctionView?: AuctionView,
): AuctionView | undefined {
let state: AuctionViewState; let state: AuctionViewState;
if ( if (
auction.info.state == AuctionState.Ended || auction.info.state == AuctionState.Ended ||
@ -102,12 +133,32 @@ export function processAccountsIntoAuctionView(
state = AuctionViewState.BuyNow; 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 = const auctionManager =
auctionManagers[auction.info.auctionManagerKey?.toBase58() || '']; auctionManagers[auction.info.auctionManagerKey?.toBase58() || ''];
if (auctionManager) { 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<SafetyDepositBox>[] = []; let boxes: ParsedAccount<SafetyDepositBox>[] = [];
let box = let box =
safetyDepositBoxesByVaultAndIndex[ safetyDepositBoxesByVaultAndIndex[
auctionManager.info.vault.toBase58() + '-0' auctionManager.info.vault.toBase58() + '-0'
@ -126,7 +177,7 @@ export function processAccountsIntoAuctionView(
} }
if (boxes.length > 0) { if (boxes.length > 0) {
let view: any = { let view: Partial<AuctionView> = {
auction, auction,
auctionManager, auctionManager,
state, state,
@ -150,13 +201,22 @@ export function processAccountsIntoAuctionView(
boxes[auctionManager.info.settings.openEditionConfig], boxes[auctionManager.info.settings.openEditionConfig],
} }
: undefined, : undefined,
myBidderMetadata: bidderMetadata,
myBidderPot: bidderPot,
}; };
view.thumbnail = view.items[0] || view.openEditionItem; view.thumbnail = (view.items || [])[0] || view.openEditionItem;
if (!view.thumbnail || !view.thumbnail.metadata) return null; view.totallyComplete = !!(
return view; 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;
} }