Bid redemption

This commit is contained in:
Jordan Prince 2021-04-27 15:33:20 -05:00
parent 0be625108f
commit 09885f6a2f
14 changed files with 281 additions and 160 deletions

View File

@ -90,6 +90,8 @@ export class AuctionData {
/// Used for precalculation on the front end, not a backend key
auctionManagerKey?: PublicKey;
/// Used for precalculation on the front end, not a backend key
bidRedemptionKey?: PublicKey;
constructor(args: {
authority: PublicKey;

View File

@ -648,7 +648,7 @@ export async function mintNewEditionFromMasterEditionViaToken(
const newEdition = await getEdition(newMint);
const masterEdition = await getEdition(tokenMint);
const data = Buffer.from('5');
const data = Buffer.from([5]);
const keys = [
{

View File

@ -219,7 +219,7 @@ export const VAULT_SCHEMA = new Map<any, any>([
['authority', 'pubkey'],
['fractionTreasury', 'pubkey'],
['redeemTreasury', 'pubkey'],
['allowFurtherShareCreation', 'boolean'],
['allowFurtherShareCreation', 'u8'],
['pricingLookupAddress', 'u8'],
['tokenTypeCount', 'u8'],
['state', 'u8'],

View File

@ -14,6 +14,10 @@ import {
createMint,
mintNewEditionFromMasterEditionViaToken,
SafetyDepositBox,
SequenceType,
sendTransactions,
sendSignedTransaction,
sendTransactionWithRetry,
} from '@oyster/common';
import { AccountLayout, MintLayout, Token } from '@solana/spl-token';
@ -57,9 +61,8 @@ export async function sendRedeemBid(
if (winnerIndex != null) {
const winningConfig =
auctionView.auctionManager.info.settings.winningConfigs[winnerIndex];
const item = auctionView.items[winningConfig.safetyDepositBoxIndex];
const item = auctionView.items[winnerIndex];
const safetyDeposit = item.safetyDeposit;
let newTokenAccount: PublicKey | undefined;
switch (winningConfig.editionType) {
case EditionType.LimitedEdition:
await setupRedeemLimitedInstructions(
@ -109,6 +112,7 @@ export async function sendRedeemBid(
(winnerIndex != null &&
auctionView.auctionManager.info.settings.openEditionWinnerConstraint !=
WinningConstraint.NoOpenEdition);
if (auctionView.openEditionItem && eligibleForOpenEdition) {
const item = auctionView.openEditionItem;
const safetyDeposit = item.safetyDeposit;
@ -124,6 +128,24 @@ export async function sendRedeemBid(
instructions,
);
}
if (signers.length == 1)
await sendTransactionWithRetry(
connection,
wallet,
instructions[0],
signers[0],
'single',
);
else
await sendTransactions(
connection,
wallet,
instructions,
signers,
SequenceType.StopOnFailure,
'single',
);
}
async function setupRedeemInstructions(
@ -224,11 +246,6 @@ async function setupRedeemLimitedInstructions(
signers: Array<Account[]>,
instructions: Array<TransactionInstruction[]>,
) {
let winningPrizeSigner: Account[] = [];
let winningPrizeInstructions: TransactionInstruction[] = [];
signers.push(winningPrizeSigner);
instructions.push(winningPrizeInstructions);
const updateAuth =
item.metadata.info.nonUniqueSpecificUpdateAuthority ||
item.nameSymbol?.info.updateAuthority;
@ -237,6 +254,13 @@ async function setupRedeemLimitedInstructions(
let newTokenAccount: PublicKey | undefined = accountsByMint.get(
item.masterEdition.info.masterMint.toBase58(),
)?.pubkey;
if (!auctionView.myBidRedemption?.info.bidRedeemed) {
let winningPrizeSigner: Account[] = [];
let winningPrizeInstructions: TransactionInstruction[] = [];
signers.push(winningPrizeSigner);
instructions.push(winningPrizeInstructions);
if (!newTokenAccount)
newTokenAccount = createTokenAccount(
winningPrizeInstructions,
@ -270,6 +294,7 @@ async function setupRedeemLimitedInstructions(
item.metadata.info.mint,
item.masterEdition.info.masterMint,
);
}
let cashInLimitedPrizeAuthorizationTokenSigner: Account[] = [];
let cashInLimitedPrizeAuthorizationTokenInstruction: TransactionInstruction[] = [];
@ -341,19 +366,20 @@ async function setupRedeemOpenInstructions(
signers: Array<Account[]>,
instructions: Array<TransactionInstruction[]>,
) {
const updateAuth =
item.metadata.info.nonUniqueSpecificUpdateAuthority ||
item.nameSymbol?.info.updateAuthority;
if (item.masterEdition && updateAuth && auctionView.myBidderMetadata) {
let newTokenAccount: PublicKey | undefined = accountsByMint.get(
item.masterEdition.info.masterMint.toBase58(),
)?.pubkey;
if (!auctionView.myBidRedemption?.info.bidRedeemed) {
let winningPrizeSigner: Account[] = [];
let winningPrizeInstructions: TransactionInstruction[] = [];
signers.push(winningPrizeSigner);
instructions.push(winningPrizeInstructions);
const updateAuth =
item.metadata.info.nonUniqueSpecificUpdateAuthority ||
item.nameSymbol?.info.updateAuthority;
if (item.masterEdition && updateAuth && auctionView.myBidderMetadata) {
let newTokenAccount: PublicKey | undefined = accountsByMint.get(
item.masterEdition.info.masterMint.toBase58(),
)?.pubkey;
if (!newTokenAccount)
newTokenAccount = createTokenAccount(
winningPrizeInstructions,
@ -388,7 +414,9 @@ async function setupRedeemOpenInstructions(
transferAuthority.publicKey,
auctionView.auctionManager.info.acceptPayment,
);
}
if (newTokenAccount) {
let cashInOpenPrizeAuthorizationTokenSigner: Account[] = [];
let cashInOpenPrizeAuthorizationTokenInstruction: TransactionInstruction[] = [];
signers.push(cashInOpenPrizeAuthorizationTokenSigner);
@ -433,7 +461,11 @@ async function setupRedeemOpenInstructions(
cashInOpenPrizeAuthorizationTokenSigner.push(burnAuthority);
mintNewEditionFromMasterEditionViaToken(
console.log(
'My master edition key',
item.masterEdition.pubkey.toBase58(),
);
await mintNewEditionFromMasterEditionViaToken(
newOpenEditionMint,
item.metadata.info.mint,
wallet.publicKey,
@ -446,3 +478,4 @@ async function setupRedeemOpenInstructions(
);
}
}
}

View File

@ -13,8 +13,9 @@ import {
hooks,
contexts,
} from '@oyster/common';
import { AuctionView } from '../../hooks';
import { AuctionView, AuctionViewState } from '../../hooks';
import { sendPlaceBid } from '../../actions/sendPlaceBid';
import { sendRedeemBid } from '../../actions/sendRedeemBid';
const { useWallet } = contexts.Wallet;
export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
const [hours, setHours] = useState<number>(23);
@ -101,6 +102,21 @@ export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
{myPayingAccount ? myPayingAccount.info.amount.toNumber() : 0.0}
</div>
{auctionView.state == AuctionViewState.Ended ? (
<Button
type="primary"
size="large"
className="action-btn"
disabled={!auctionView.myBidderMetadata}
onClick={() => {
console.log('Auctionview', auctionView);
sendRedeemBid(connection, wallet, auctionView, accountByMint);
}}
style={{ marginTop: 20 }}
>
REDEEM BID
</Button>
) : (
<Button
type="primary"
size="large"
@ -121,6 +137,7 @@ export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
>
PLACE BID
</Button>
)}
</div>
);
};

View File

@ -28,6 +28,8 @@ import {
BIDDER_POT_LEN,
decodeVault,
Vault,
TokenAccount,
useUserAccounts,
} from '@oyster/common';
import { MintInfo } from '@solana/spl-token';
import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js';
@ -36,8 +38,11 @@ import React, { useContext, useEffect, useState } from 'react';
import {
AuctionManager,
AuctionManagerStatus,
BidRedemptionTicket,
decodeAuctionManager,
decodeBidRedemptionTicket,
getAuctionManagerKey,
getBidderKeys,
MetaplexKey,
} from '../models/metaplex';
@ -60,6 +65,7 @@ export interface MetaContextState {
ParsedAccount<SafetyDepositBox>
>;
bidderPotsByAuctionAndBidder: Record<string, ParsedAccount<BidderPot>>;
bidRedemptions: Record<string, ParsedAccount<BidRedemptionTicket>>;
}
const MetaContext = React.createContext<MetaContextState>({
@ -74,10 +80,17 @@ const MetaContext = React.createContext<MetaContextState>({
bidderMetadataByAuctionAndBidder: {},
safetyDepositBoxesByVaultAndIndex: {},
bidderPotsByAuctionAndBidder: {},
bidRedemptions: {},
});
export function MetaProvider({ children = null as any }) {
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 [metadata, setMetadata] = useState<ParsedAccount<Metadata>[]>([]);
const [metadataByMint, setMetadataByMint] = useState<
Record<string, ParsedAccount<Metadata>>
@ -94,6 +107,9 @@ export function MetaProvider({ children = null as any }) {
const [auctionManagers, setAuctionManagers] = useState<
Record<string, ParsedAccount<AuctionManager>>
>({});
const [bidRedemptions, setBidRedemptions] = useState<
Record<string, ParsedAccount<BidRedemptionTicket>>
>({});
const [auctions, setAuctions] = useState<
Record<string, ParsedAccount<AuctionData>>
>({});
@ -123,6 +139,11 @@ export function MetaProvider({ children = null as any }) {
auction.resource,
a.pubkey,
);
const payerAcct = accountByMint.get(auction.tokenMint.toBase58());
if (payerAcct)
auction.bidRedemptionKey = (
await getBidderKeys(a.pubkey, payerAcct.pubkey)
).bidRedemption;
const account: ParsedAccount<AuctionData> = {
pubkey: a.pubkey,
account: a.account,
@ -206,7 +227,7 @@ export function MetaProvider({ children = null as any }) {
return () => {
dispose();
};
}, [connection, setAuctions]);
}, [connection, setAuctions, userAccounts]);
useEffect(() => {
let dispose = () => {};
@ -270,7 +291,7 @@ export function MetaProvider({ children = null as any }) {
return () => {
dispose();
};
}, [connection, setSafetyDepositBoxesByVaultAndIndex]);
}, [connection, setSafetyDepositBoxesByVaultAndIndex, setVaults]);
useEffect(() => {
let dispose = () => {};
@ -288,6 +309,17 @@ export function MetaProvider({ children = null as any }) {
...e,
[a.pubkey.toBase58()]: account,
}));
} else if (a.account.data[0] == MetaplexKey.BidRedemptionTicketV1) {
const ticket = await decodeBidRedemptionTicket(a.account.data);
const account: ParsedAccount<BidRedemptionTicket> = {
pubkey: a.pubkey,
account: a.account,
info: ticket,
};
setBidRedemptions(e => ({
...e,
[a.pubkey.toBase58()]: account,
}));
}
} catch {
// ignore errors
@ -323,7 +355,7 @@ export function MetaProvider({ children = null as any }) {
return () => {
dispose();
};
}, [connection, setAuctionManagers]);
}, [connection, setAuctionManagers, setBidRedemptions]);
useEffect(() => {
let dispose = () => {};
@ -452,6 +484,7 @@ export function MetaProvider({ children = null as any }) {
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
vaults,
bidRedemptions,
}}
>
{children}

View File

@ -27,6 +27,7 @@ export const useAuction = (id: string) => {
bidderPotsByAuctionAndBidder,
masterEditions,
nameSymbolTuples,
bidRedemptions,
vaults,
} = useMeta();
@ -39,6 +40,7 @@ export const useAuction = (id: string) => {
safetyDepositBoxesByVaultAndIndex,
metadataByMint,
nameSymbolTuples,
bidRedemptions,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
masterEditions,
@ -61,6 +63,7 @@ export const useAuction = (id: string) => {
vaults,
nameSymbolTuples,
masterEditions,
bidRedemptions,
userAccounts,
]);
return existingAuctionView;

View File

@ -15,7 +15,7 @@ import {
} from '@oyster/common';
import { useEffect, useState } from 'react';
import { useMeta } from '../contexts';
import { AuctionManager } from '../models/metaplex';
import { AuctionManager, BidRedemptionTicket } from '../models/metaplex';
export enum AuctionViewState {
Live = '0',
@ -41,6 +41,7 @@ export interface AuctionView {
thumbnail: AuctionViewItem;
myBidderMetadata?: ParsedAccount<BidderMetadata>;
myBidderPot?: ParsedAccount<BidderPot>;
myBidRedemption?: ParsedAccount<BidRedemptionTicket>;
vault: ParsedAccount<Vault>;
totallyComplete: boolean;
}
@ -71,6 +72,7 @@ export const useAuctions = (state: AuctionViewState) => {
vaults,
nameSymbolTuples,
masterEditions,
bidRedemptions,
} = useMeta();
useEffect(() => {
@ -84,6 +86,7 @@ export const useAuctions = (state: AuctionViewState) => {
safetyDepositBoxesByVaultAndIndex,
metadataByMint,
nameSymbolTuples,
bidRedemptions,
bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder,
masterEditions,
@ -108,6 +111,7 @@ export const useAuctions = (state: AuctionViewState) => {
vaults,
nameSymbolTuples,
masterEditions,
bidRedemptions,
]);
return Object.values(auctionViews).filter(v => v) as AuctionView[];
@ -122,6 +126,7 @@ export function processAccountsIntoAuctionView(
>,
metadataByMint: Record<string, ParsedAccount<Metadata>>,
nameSymbolTuples: Record<string, ParsedAccount<NameSymbolTuple>>,
bidRedemptions: Record<string, ParsedAccount<BidRedemptionTicket>>,
bidderMetadataByAuctionAndBidder: Record<
string,
ParsedAccount<BidderMetadata>
@ -159,6 +164,14 @@ export function processAccountsIntoAuctionView(
auctionManagers[auction.info.auctionManagerKey?.toBase58() || ''];
if (auctionManager) {
const boxesExpected = auctionManager.info.state.winningConfigsValidated;
let bidRedemption:
| ParsedAccount<BidRedemptionTicket>
| undefined = undefined;
if (auction.info.bidRedemptionKey?.toBase58()) {
bidRedemption = bidRedemptions[auction.info.bidRedemptionKey?.toBase58()];
}
const bidderMetadata =
bidderMetadataByAuctionAndBidder[
auction.pubkey.toBase58() + '-' + myPayingAccount?.pubkey.toBase58()
@ -173,6 +186,7 @@ export function processAccountsIntoAuctionView(
// and only update the two things that could possibly change
existingAuctionView.myBidderPot = bidderPot;
existingAuctionView.myBidderMetadata = bidderMetadata;
existingAuctionView.myBidRedemption = bidRedemption;
for (let i = 0; i < existingAuctionView.items.length; i++) {
let curr = existingAuctionView.items[i];
if (!curr.metadata) {
@ -256,16 +270,27 @@ export function processAccountsIntoAuctionView(
],
safetyDeposit:
boxes[auctionManager.info.settings.openEditionConfig],
masterEdition:
masterEditions[
metadataByMint[
boxes[
auctionManager.info.settings.openEditionConfig
].info.tokenMint.toBase58()
]?.info.masterEdition?.toBase58() || ''
],
}
: undefined,
myBidderMetadata: bidderMetadata,
myBidderPot: bidderPot,
myBidRedemption: bidRedemption,
};
view.thumbnail = (view.items || [])[0] || view.openEditionItem;
view.totallyComplete = !!(
view.thumbnail &&
boxesExpected == (view.items || []).length &&
boxesExpected ==
(view.items || []).length +
(auctionManager.info.settings.openEditionConfig == null ? 0 : 1) &&
(auctionManager.info.settings.openEditionConfig == null ||
(auctionManager.info.settings.openEditionConfig != null &&
view.openEditionItem)) &&

View File

@ -145,6 +145,14 @@ export const decodeAuctionManager = (buffer: Buffer) => {
return deserializeBorsh(SCHEMA, AuctionManager, buffer) as AuctionManager;
};
export const decodeBidRedemptionTicket = (buffer: Buffer) => {
return deserializeBorsh(
SCHEMA,
BidRedemptionTicket,
buffer,
) as BidRedemptionTicket;
};
export class WinningConfigState {
amountMinted: number = 0;
validated: boolean = false;
@ -362,6 +370,7 @@ export async function getBidderKeys(
await PublicKey.findProgramAddress(
[
Buffer.from(AUCTION_PREFIX),
PROGRAM_IDS.auction.toBuffer(),
auctionKey.toBuffer(),
bidder.toBuffer(),
Buffer.from(METADATA),

View File

@ -86,7 +86,7 @@ export async function redeemBid(
},
{
pubkey: bidder,
isSigner: true,
isSigner: false,
isWritable: false,
},
{

View File

@ -99,7 +99,7 @@ export async function redeemLimitedEditionBid(
},
{
pubkey: bidder,
isSigner: true,
isSigner: false,
isWritable: false,
},
{

View File

@ -94,7 +94,7 @@ export async function redeemMasterEditionBid(
},
{
pubkey: bidder,
isSigner: true,
isSigner: false,
isWritable: false,
},
{

View File

@ -37,7 +37,6 @@ export async function redeemOpenEditionBid(
auctionKey,
bidder,
);
const masterMetadata: PublicKey = await getMetadata(tokenMint);
const masterEdition: PublicKey = await getEdition(tokenMint);
@ -92,8 +91,8 @@ export async function redeemOpenEditionBid(
},
{
pubkey: bidder,
isSigner: true,
isWritable: false,
isSigner: false,
isWritable: true,
},
{
pubkey: payer,
@ -138,7 +137,7 @@ export async function redeemOpenEditionBid(
{
pubkey: masterMint,
isSigner: false,
isWritable: false,
isWritable: true,
},
{
pubkey: masterEdition,