feat: mint editions into wallet (#289)
feat(ArtMinting): add mint destination address validation feat(ArtMinting): add cofetti feat(ArtMinting): add remounting logic chore(mint editions): upodate types chore(art minting): update imports Co-authored-by: shotgunofdeath <42133844+shotgunofdeath@users.noreply.github.com>
This commit is contained in:
parent
0b064b6df4
commit
14401e878d
|
@ -0,0 +1,65 @@
|
|||
import { Keypair, TransactionInstruction } from '@solana/web3.js';
|
||||
import { Token } from '@solana/spl-token';
|
||||
import {
|
||||
createAssociatedTokenAccountInstruction,
|
||||
createMint,
|
||||
findProgramAddress,
|
||||
programIds,
|
||||
StringPublicKey,
|
||||
toPublicKey,
|
||||
} from '@oyster/common';
|
||||
import { WalletNotConnectedError } from '@solana/wallet-adapter-base';
|
||||
|
||||
export async function createMintAndAccountWithOne(
|
||||
wallet: any,
|
||||
receiverWallet: StringPublicKey,
|
||||
mintRent: any,
|
||||
instructions: TransactionInstruction[],
|
||||
signers: Keypair[],
|
||||
): Promise<{ mint: StringPublicKey; account: StringPublicKey }> {
|
||||
if (!wallet.publicKey) throw new WalletNotConnectedError();
|
||||
|
||||
const mint = createMint(
|
||||
instructions,
|
||||
wallet.publicKey,
|
||||
mintRent,
|
||||
0,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
signers,
|
||||
);
|
||||
|
||||
const PROGRAM_IDS = programIds();
|
||||
|
||||
const account: StringPublicKey = (
|
||||
await findProgramAddress(
|
||||
[
|
||||
toPublicKey(receiverWallet).toBuffer(),
|
||||
PROGRAM_IDS.token.toBuffer(),
|
||||
mint.toBuffer(),
|
||||
],
|
||||
PROGRAM_IDS.associatedToken,
|
||||
)
|
||||
)[0];
|
||||
|
||||
createAssociatedTokenAccountInstruction(
|
||||
instructions,
|
||||
toPublicKey(account),
|
||||
wallet.publicKey,
|
||||
toPublicKey(receiverWallet),
|
||||
mint,
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
Token.createMintToInstruction(
|
||||
PROGRAM_IDS.token,
|
||||
mint,
|
||||
toPublicKey(account),
|
||||
wallet.publicKey,
|
||||
[],
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
||||
return { mint: mint.toBase58(), account };
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import BN from 'bn.js';
|
||||
import { Connection, Keypair, TransactionInstruction } from '@solana/web3.js';
|
||||
import {
|
||||
sendTransactions,
|
||||
sendTransactionWithRetry,
|
||||
SequenceType,
|
||||
StringPublicKey,
|
||||
TokenAccount,
|
||||
} from '@oyster/common';
|
||||
import { setupMintEditionIntoWalletInstructions } from './setupMintEditionIntoWalletInstructions';
|
||||
import { Art } from '../types';
|
||||
import { WalletAdapter } from '@solana/wallet-base';
|
||||
|
||||
// TODO: Refactor. Extract batching logic,
|
||||
// as the similar one is used in settle.ts and convertMasterEditions.ts
|
||||
const MINT_TRANSACTION_SIZE = 5;
|
||||
const BATCH_SIZE = 10;
|
||||
|
||||
export async function mintEditionsToWallet(
|
||||
art: Art,
|
||||
wallet: WalletAdapter,
|
||||
connection: Connection,
|
||||
mintTokenAccount: TokenAccount,
|
||||
editions: number = 1,
|
||||
mintDestination: StringPublicKey,
|
||||
) {
|
||||
let signers: Array<Array<Keypair[]>> = [];
|
||||
let instructions: Array<Array<TransactionInstruction[]>> = [];
|
||||
|
||||
let currSignerBatch: Array<Keypair[]> = [];
|
||||
let currInstrBatch: Array<TransactionInstruction[]> = [];
|
||||
|
||||
let mintEditionIntoWalletSigners: Keypair[] = [];
|
||||
let mintEditionIntoWalletInstructions: TransactionInstruction[] = [];
|
||||
|
||||
// TODO replace all this with payer account so user doesnt need to click approve several times.
|
||||
|
||||
// Overall we have 10 parallel txns.
|
||||
// That's what this loop is building.
|
||||
for (let i = 0; i < editions; i++) {
|
||||
console.log('Minting', i);
|
||||
await setupMintEditionIntoWalletInstructions(
|
||||
art,
|
||||
wallet,
|
||||
connection,
|
||||
mintTokenAccount,
|
||||
new BN(art.supply! + 1 + i),
|
||||
mintEditionIntoWalletInstructions,
|
||||
mintEditionIntoWalletSigners,
|
||||
mintDestination,
|
||||
);
|
||||
|
||||
if (mintEditionIntoWalletInstructions.length === MINT_TRANSACTION_SIZE) {
|
||||
currSignerBatch.push(mintEditionIntoWalletSigners);
|
||||
currInstrBatch.push(mintEditionIntoWalletInstructions);
|
||||
mintEditionIntoWalletSigners = [];
|
||||
mintEditionIntoWalletInstructions = [];
|
||||
}
|
||||
|
||||
if (currInstrBatch.length === BATCH_SIZE) {
|
||||
signers.push(currSignerBatch);
|
||||
instructions.push(currInstrBatch);
|
||||
currSignerBatch = [];
|
||||
currInstrBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
mintEditionIntoWalletInstructions.length < MINT_TRANSACTION_SIZE &&
|
||||
mintEditionIntoWalletInstructions.length > 0
|
||||
) {
|
||||
currSignerBatch.push(mintEditionIntoWalletSigners);
|
||||
currInstrBatch.push(mintEditionIntoWalletInstructions);
|
||||
}
|
||||
|
||||
if (currInstrBatch.length <= BATCH_SIZE && currInstrBatch.length > 0) {
|
||||
// add the last one on
|
||||
signers.push(currSignerBatch);
|
||||
instructions.push(currInstrBatch);
|
||||
}
|
||||
console.log('Instructions', instructions);
|
||||
for (let i = 0; i < instructions.length; i++) {
|
||||
const instructionBatch = instructions[i];
|
||||
const signerBatch = signers[i];
|
||||
console.log('Running batch', i);
|
||||
if (instructionBatch.length >= 2)
|
||||
// Pump em through!
|
||||
await sendTransactions(
|
||||
connection,
|
||||
wallet,
|
||||
instructionBatch,
|
||||
signerBatch,
|
||||
SequenceType.StopOnFailure,
|
||||
'single',
|
||||
);
|
||||
else
|
||||
await sendTransactionWithRetry(
|
||||
connection,
|
||||
wallet,
|
||||
instructionBatch[0],
|
||||
signerBatch[0],
|
||||
'single',
|
||||
);
|
||||
console.log('Done');
|
||||
}
|
||||
}
|
|
@ -16,15 +16,12 @@ import {
|
|||
sendTransactionsWithManualRetry,
|
||||
MasterEditionV1,
|
||||
MasterEditionV2,
|
||||
findProgramAddress,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
deprecatedMintNewEditionFromMasterEditionViaPrintingToken,
|
||||
MetadataKey,
|
||||
TokenAccountParser,
|
||||
BidderMetadata,
|
||||
getEditionMarkPda,
|
||||
decodeEditionMarker,
|
||||
BidStateType,
|
||||
StringPublicKey,
|
||||
toPublicKey,
|
||||
WalletSigner,
|
||||
|
@ -44,13 +41,13 @@ import {
|
|||
PrizeTrackingTicket,
|
||||
getPrizeTrackingTicket,
|
||||
BidRedemptionTicket,
|
||||
getBidRedemption,
|
||||
} from '../models/metaplex';
|
||||
import { claimBid } from '../models/metaplex/claimBid';
|
||||
import { setupCancelBid } from './cancelBid';
|
||||
import { deprecatedPopulateParticipationPrintingAccount } from '../models/metaplex/deprecatedPopulateParticipationPrintingAccount';
|
||||
import { setupPlaceBid } from './sendPlaceBid';
|
||||
import { claimUnusedPrizes } from './claimUnusedPrizes';
|
||||
import { createMintAndAccountWithOne } from './createMintAndAccountWithOne';
|
||||
import { BN } from 'bn.js';
|
||||
import { QUOTE_MINT } from '../constants';
|
||||
import {
|
||||
|
@ -414,60 +411,6 @@ async function setupRedeemFullRightsTransferInstructions(
|
|||
}
|
||||
}
|
||||
|
||||
async function createMintAndAccountWithOne(
|
||||
wallet: WalletSigner,
|
||||
receiverWallet: StringPublicKey,
|
||||
mintRent: any,
|
||||
instructions: TransactionInstruction[],
|
||||
signers: Keypair[],
|
||||
): Promise<{ mint: StringPublicKey; account: StringPublicKey }> {
|
||||
if (!wallet.publicKey) throw new WalletNotConnectedError();
|
||||
|
||||
const mint = createMint(
|
||||
instructions,
|
||||
wallet.publicKey,
|
||||
mintRent,
|
||||
0,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
signers,
|
||||
);
|
||||
|
||||
const PROGRAM_IDS = programIds();
|
||||
|
||||
const account: StringPublicKey = (
|
||||
await findProgramAddress(
|
||||
[
|
||||
toPublicKey(receiverWallet).toBuffer(),
|
||||
PROGRAM_IDS.token.toBuffer(),
|
||||
mint.toBuffer(),
|
||||
],
|
||||
PROGRAM_IDS.associatedToken,
|
||||
)
|
||||
)[0];
|
||||
|
||||
createAssociatedTokenAccountInstruction(
|
||||
instructions,
|
||||
toPublicKey(account),
|
||||
wallet.publicKey,
|
||||
toPublicKey(receiverWallet),
|
||||
mint,
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
Token.createMintToInstruction(
|
||||
PROGRAM_IDS.token,
|
||||
mint,
|
||||
toPublicKey(account),
|
||||
wallet.publicKey,
|
||||
[],
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
||||
return { mint: mint.toBase58(), account };
|
||||
}
|
||||
|
||||
export async function setupRedeemPrintingV2Instructions(
|
||||
connection: Connection,
|
||||
auctionView: AuctionView,
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { Connection } from '@solana/web3.js';
|
||||
import { MintLayout } from '@solana/spl-token';
|
||||
import BN from 'bn.js';
|
||||
import {
|
||||
mintNewEditionFromMasterEditionViaToken,
|
||||
StringPublicKey,
|
||||
TokenAccount,
|
||||
} from '@oyster/common';
|
||||
import { createMintAndAccountWithOne } from './createMintAndAccountWithOne';
|
||||
import { Art } from '../types';
|
||||
import { WalletAdapter } from '@solana/wallet-base';
|
||||
|
||||
export async function setupMintEditionIntoWalletInstructions(
|
||||
art: Art,
|
||||
wallet: WalletAdapter,
|
||||
connection: Connection,
|
||||
mintTokenAccount: TokenAccount,
|
||||
edition: BN,
|
||||
instructions: any,
|
||||
signers: any,
|
||||
mintDestination: StringPublicKey,
|
||||
) {
|
||||
if (!art.mint) throw new Error('Art mint is not provided');
|
||||
if (typeof art.supply === 'undefined') {
|
||||
throw new Error('Art supply is not provided');
|
||||
}
|
||||
if (!wallet.publicKey) throw new Error('Wallet pubKey is not provided');
|
||||
if (!mintTokenAccount) {
|
||||
throw new Error('Art mint token account is not provided');
|
||||
}
|
||||
const walletPubKey = wallet.publicKey.toString();
|
||||
const { mint: tokenMint } = art;
|
||||
const { pubkey: mintTokenAccountPubKey } = mintTokenAccount;
|
||||
const mintTokenAccountOwner = mintTokenAccount.info.owner.toString();
|
||||
|
||||
const mintRentExempt = await connection.getMinimumBalanceForRentExemption(
|
||||
MintLayout.span,
|
||||
);
|
||||
const { mint: newMint } = await createMintAndAccountWithOne(
|
||||
wallet,
|
||||
mintDestination,
|
||||
mintRentExempt,
|
||||
instructions,
|
||||
signers,
|
||||
);
|
||||
|
||||
await mintNewEditionFromMasterEditionViaToken(
|
||||
newMint,
|
||||
tokenMint,
|
||||
walletPubKey,
|
||||
walletPubKey,
|
||||
mintTokenAccountOwner,
|
||||
mintTokenAccountPubKey,
|
||||
instructions,
|
||||
walletPubKey,
|
||||
edition,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
|
||||
import { MintLayout, AccountLayout } from '@solana/spl-token';
|
||||
import { Button, Form, Input, Modal, InputNumber } from 'antd';
|
||||
import debounce from 'lodash/debounce';
|
||||
import {
|
||||
decodeMasterEdition,
|
||||
MAX_EDITION_LEN,
|
||||
MAX_METADATA_LEN,
|
||||
MetadataKey,
|
||||
MetaplexOverlay,
|
||||
useConnection,
|
||||
useUserAccounts,
|
||||
useWallet,
|
||||
} from '@oyster/common';
|
||||
import { useArt } from '../../hooks';
|
||||
import { mintEditionsToWallet } from '../../actions/mintEditionsIntoWallet';
|
||||
import { ArtType } from '../../types';
|
||||
import { Confetti } from '../Confetti';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface ArtMintingProps {
|
||||
id: string;
|
||||
onMint: Function;
|
||||
}
|
||||
|
||||
export const ArtMinting = ({ id, onMint }: ArtMintingProps) => {
|
||||
const { wallet } = useWallet();
|
||||
const connection = useConnection();
|
||||
const { accountByMint } = useUserAccounts();
|
||||
const [showMintModal, setShowMintModal] = useState<boolean>(false);
|
||||
const [showCongrats, setShowCongrats] = useState<boolean>(false);
|
||||
const [mintingDestination, setMintingDestination] = useState<string>('');
|
||||
const [editions, setEditions] = useState<number>(1);
|
||||
const [totalCost, setTotalCost] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const art = useArt(id);
|
||||
|
||||
const walletPubKey = wallet?.publicKey?.toString() || '';
|
||||
const maxEditionsToMint = art.maxSupply! - art.supply!;
|
||||
const isArtMasterEdition = art.type === ArtType.Master;
|
||||
const artMintTokenAccount = accountByMint.get(art.mint!);
|
||||
const isArtOwnedByUser =
|
||||
((accountByMint.has(art.mint!) &&
|
||||
artMintTokenAccount?.info.amount.toNumber()) ||
|
||||
0) > 0;
|
||||
const isMasterEditionV1 = artMintTokenAccount
|
||||
? decodeMasterEdition(artMintTokenAccount.account.data).key ===
|
||||
MetadataKey.MasterEditionV1
|
||||
: false;
|
||||
const renderMintEdition =
|
||||
isArtMasterEdition &&
|
||||
isArtOwnedByUser &&
|
||||
!isMasterEditionV1 &&
|
||||
maxEditionsToMint !== 0;
|
||||
|
||||
const mintingDestinationErr = useMemo(() => {
|
||||
if (!mintingDestination) return 'Required';
|
||||
|
||||
try {
|
||||
new PublicKey(mintingDestination);
|
||||
return '';
|
||||
} catch (e) {
|
||||
return 'Invalid address format';
|
||||
}
|
||||
}, [mintingDestination]);
|
||||
|
||||
const isMintingDisabled =
|
||||
isLoading || editions < 1 || Boolean(mintingDestinationErr);
|
||||
|
||||
const debouncedEditionsChangeHandler = useCallback(
|
||||
debounce(val => {
|
||||
setEditions(val < 1 ? 1 : val);
|
||||
}, 300),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (editions < 1) return;
|
||||
|
||||
(async () => {
|
||||
const mintRentExempt = await connection.getMinimumBalanceForRentExemption(
|
||||
MintLayout.span,
|
||||
);
|
||||
const accountRentExempt =
|
||||
await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
|
||||
const metadataRentExempt =
|
||||
await connection.getMinimumBalanceForRentExemption(MAX_METADATA_LEN);
|
||||
const editionRentExempt =
|
||||
await connection.getMinimumBalanceForRentExemption(MAX_EDITION_LEN);
|
||||
|
||||
const cost =
|
||||
((mintRentExempt +
|
||||
accountRentExempt +
|
||||
metadataRentExempt +
|
||||
editionRentExempt) *
|
||||
editions) /
|
||||
LAMPORTS_PER_SOL;
|
||||
|
||||
setTotalCost(cost);
|
||||
})();
|
||||
}, [connection, editions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!walletPubKey) return;
|
||||
|
||||
setMintingDestination(walletPubKey);
|
||||
}, [walletPubKey]);
|
||||
|
||||
useEffect(() => {
|
||||
return debouncedEditionsChangeHandler.cancel();
|
||||
}, []);
|
||||
|
||||
const onSuccessfulMint = () => {
|
||||
setShowMintModal(false);
|
||||
setMintingDestination(walletPubKey);
|
||||
setEditions(1);
|
||||
setShowCongrats(true);
|
||||
};
|
||||
|
||||
const mint = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await mintEditionsToWallet(
|
||||
art,
|
||||
wallet!,
|
||||
connection,
|
||||
artMintTokenAccount!,
|
||||
editions,
|
||||
mintingDestination,
|
||||
);
|
||||
onSuccessfulMint();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderMintEdition && (
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="action-btn"
|
||||
style={{ marginTop: 20 }}
|
||||
onClick={() => setShowMintModal(true)}
|
||||
>
|
||||
Mint
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
visible={showMintModal}
|
||||
centered
|
||||
okText="Mint"
|
||||
closable={!isLoading}
|
||||
okButtonProps={{
|
||||
disabled: isMintingDisabled,
|
||||
}}
|
||||
cancelButtonProps={{ disabled: isLoading }}
|
||||
onOk={mint}
|
||||
onCancel={() => setShowMintModal(false)}
|
||||
>
|
||||
<Form.Item
|
||||
style={{
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
paddingTop: 30,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
label={<h3>Mint to</h3>}
|
||||
labelAlign="left"
|
||||
colon={false}
|
||||
validateStatus={mintingDestinationErr ? 'error' : 'success'}
|
||||
help={mintingDestinationErr}
|
||||
>
|
||||
<Input
|
||||
placeholder="Address to mint edition to"
|
||||
value={mintingDestination}
|
||||
onChange={e => {
|
||||
setMintingDestination(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
style={{
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
paddingTop: 30,
|
||||
}}
|
||||
label={<h3>Number of editions to mint</h3>}
|
||||
labelAlign="left"
|
||||
colon={false}
|
||||
>
|
||||
<InputNumber
|
||||
type="number"
|
||||
placeholder="1"
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={maxEditionsToMint}
|
||||
value={editions}
|
||||
precision={0}
|
||||
onChange={debouncedEditionsChangeHandler}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div>Total cost: {`◎${totalCost}`}</div>
|
||||
</Modal>
|
||||
|
||||
<MetaplexOverlay visible={showCongrats}>
|
||||
<Confetti />
|
||||
<h1
|
||||
className="title"
|
||||
style={{
|
||||
fontSize: '3rem',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
Congratulations
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
fontSize: '2rem',
|
||||
}}
|
||||
>
|
||||
New editions have been minted please view your NFTs in{' '}
|
||||
<Link to="/artworks">My Items</Link>.
|
||||
</p>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await onMint();
|
||||
setShowCongrats(false);
|
||||
}}
|
||||
className="overlay-btn"
|
||||
>
|
||||
Got it
|
||||
</Button>
|
||||
</MetaplexOverlay>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,21 +1,33 @@
|
|||
import React from 'react';
|
||||
import { Row, Col, Divider, Layout, Tag, Button, Skeleton, List, Card } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Divider,
|
||||
Layout,
|
||||
Tag,
|
||||
Button,
|
||||
Skeleton,
|
||||
List,
|
||||
Card,
|
||||
} from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useArt, useExtendedArt } from './../../hooks';
|
||||
import { useArt, useExtendedArt } from '../../hooks';
|
||||
|
||||
import { ArtContent } from '../../components/ArtContent';
|
||||
import { shortenAddress, useConnection } from '@oyster/common';
|
||||
import { useWallet } from '@solana/wallet-adapter-react';
|
||||
import { MetaAvatar } from '../../components/MetaAvatar';
|
||||
import { sendSignMetadata } from '../../actions/sendSignMetadata';
|
||||
import { ViewOn } from './../../components/ViewOn';
|
||||
import { ViewOn } from '../../components/ViewOn';
|
||||
import { ArtType } from '../../types';
|
||||
import { ArtMinting } from '../../components/ArtMinting';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
export const ArtView = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const wallet = useWallet();
|
||||
const [remountArtMinting, setRemountArtMinting] = useState(0);
|
||||
|
||||
const connection = useConnection();
|
||||
const art = useArt(id);
|
||||
|
@ -39,7 +51,7 @@ export const ArtView = () => {
|
|||
const description = data?.description;
|
||||
const attributes = data?.attributes;
|
||||
|
||||
const pubkey = wallet.publicKey?.toBase58() || '';
|
||||
const pubkey = wallet?.publicKey?.toBase58() || '';
|
||||
|
||||
const tag = (
|
||||
<div className="info-header">
|
||||
|
@ -181,6 +193,13 @@ export const ArtView = () => {
|
|||
>
|
||||
Mark as Sold
|
||||
</Button> */}
|
||||
|
||||
{/* TODO: Add conversion of MasterEditionV1 to MasterEditionV2 */}
|
||||
<ArtMinting
|
||||
id={id}
|
||||
key={remountArtMinting}
|
||||
onMint={async () => await setRemountArtMinting(prev => prev + 1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span="12">
|
||||
<Divider />
|
||||
|
@ -197,23 +216,22 @@ export const ArtView = () => {
|
|||
<div className="info-content">{art.about}</div> */}
|
||||
</Col>
|
||||
<Col span="12">
|
||||
{attributes &&
|
||||
{attributes && (
|
||||
<>
|
||||
<Divider />
|
||||
<br />
|
||||
<div className="info-header">Attributes</div>
|
||||
<List
|
||||
size="large"
|
||||
grid={{ column: 4 }}
|
||||
>
|
||||
{attributes.map(attribute =>
|
||||
<List size="large" grid={{ column: 4 }}>
|
||||
{attributes.map(attribute => (
|
||||
<List.Item>
|
||||
<Card title={attribute.trait_type}>{attribute.value}</Card>
|
||||
<Card title={attribute.trait_type}>
|
||||
{attribute.value}
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
|
Loading…
Reference in New Issue