I can bid

This commit is contained in:
Jordan Prince 2021-04-26 21:46:30 -05:00
parent b46406d61b
commit 7ab009e0e6
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;
};
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<any, any>([
kind: 'struct',
fields: [
['instruction', 'u8'],
['resource', 'pubkey'],
['amount', 'u64'],
['resource', 'pubkey'],
],
},
],
@ -300,7 +320,11 @@ export const AUCTION_SCHEMA = new Map<any, any>([
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,
},
{

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,
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<number>(23);
const [minutes, setMinutes] = useState<number>(59);
const [seconds, setSeconds] = useState<number>(59);
const [clock, setClock] = useState<number>(0);
const connection = useConnection();
const { wallet } = useWallet();
const { userAccounts } = useUserAccounts();
const [value, setValue] = useState<number>();
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 }) => {
<InputNumber
autoFocus
className="input"
placeholder="Max 50 characters"
// value={props.attributes.name}
// onChange={info =>
// props.setAttributes({
// ...props.attributes,
// name: info.target.value,
// })
// }
value={value}
onChange={setValue}
/>
<div
@ -106,6 +105,18 @@ export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
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

View File

@ -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<string, ParsedAccount<MasterEdition>>;
auctionManagers: Record<string, ParsedAccount<AuctionManager>>;
auctions: Record<string, ParsedAccount<AuctionData>>;
bidderMetadataByAuctionAndBidder: Record<
string,
ParsedAccount<BidderMetadata>
>;
safetyDepositBoxesByVaultAndIndex: Record<
string,
ParsedAccount<SafetyDepositBox>
>;
bidderPotsByAuctionAndBidder: Record<string, ParsedAccount<BidderPot>>;
}
const MetaContext = React.createContext<MetaContextState>({
@ -56,7 +65,9 @@ const MetaContext = React.createContext<MetaContextState>({
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<string, ParsedAccount<AuctionData>>
>({});
const [
bidderMetadataByAuctionAndBidder,
setBidderMetadataByAuctionAndBidder,
] = useState<Record<string, ParsedAccount<BidderMetadata>>>({});
const [
bidderPotsByAuctionAndBidder,
setBidderPotsByAuctionAndBidder,
] = useState<Record<string, ParsedAccount<BidderPot>>>({});
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<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,
metadataByMint,
safetyDepositBoxesByVaultAndIndex,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
}}
>
{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 { 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<string, TokenAccount>());
const [clock, setClock] = useState<number>(0);
const [auctionView, setAuctionView] = useState<AuctionView | null>(null);
const [existingAuctionView, setAuctionView] = useState<AuctionView | null>(
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;
};

View File

@ -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<BidderMetadata>;
myBidderPot?: ParsedAccount<BidderPot>;
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<string, TokenAccount>());
const [clock, setClock] = useState<number>(0);
const [auctionViews, setAuctionViews] = useState<AuctionView[]>([]);
const [auctionViews, setAuctionViews] = useState<
Record<string, AuctionView | undefined>
>({});
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<SafetyDepositBox>
>,
metadataByMint: Record<string, ParsedAccount<Metadata>>,
bidderMetadataByAuctionAndBidder: Record<
string,
ParsedAccount<BidderMetadata>
>,
bidderPotsByAuctionAndBidder: Record<string, ParsedAccount<BidderPot>>,
accountByMint: Map<string, TokenAccount>,
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<SafetyDepositBox>[] = [];
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<AuctionView> = {
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;
}