Merge branch 'feature/m' of https://github.com/solana-labs/oyster into feature/m

This commit is contained in:
bartosz-lipinski 2021-04-23 14:53:31 -05:00
commit 6183122b6a
6 changed files with 1359 additions and 879 deletions

View File

@ -1,5 +1,4 @@
import {
Account,
PublicKey,
SystemProgram,
SYSVAR_RENT_PUBKEY,
@ -8,6 +7,7 @@ import {
import { programIds } from '../utils/ids';
import { deserializeBorsh } from './../utils/borsh';
import { serialize } from 'borsh';
import BN from 'bn.js';
export const MAX_NAME_LENGTH = 32;
@ -16,12 +16,17 @@ export const MAX_SYMBOL_LENGTH = 10;
export const MAX_URI_LENGTH = 200;
export const MAX_METADATA_LEN =
32 + MAX_NAME_LENGTH + MAX_SYMBOL_LENGTH + MAX_URI_LENGTH + 200;
1 + 32 + MAX_NAME_LENGTH + MAX_SYMBOL_LENGTH + MAX_URI_LENGTH + 200;
export const MAX_OWNER_LEN = 32 + 32;
export const MAX_NAME_SYMBOL_LEN = 1 + 32 + 8;
export const MAX_MASTER_EDITION_KEN = 1 + 9 + 8 + 32;
export const METADATA_KEY = 0;
export const NAME_SYMBOL_KEY = 1;
export enum Key {
MetadataV1 = 0,
NameSymbolTupleV1 = 1,
EditionV1 = 2,
MasterEditionV1 = 3,
}
export enum MetadataCategory {
Audio = 'audio',
@ -42,9 +47,29 @@ export interface IMetadataExtension {
category: MetadataCategory;
}
export class MasterEdition {
key: Key;
supply: BN;
maxSupply?: BN;
/// Can be used to mint tokens that give one-time permission to mint a single limited edition.
masterMint: PublicKey;
constructor(args: {
key: Key;
supply: BN;
maxSupply?: BN;
/// Can be used to mint tokens that give one-time permission to mint a single limited edition.
masterMint: PublicKey;
}) {
this.key = Key.MasterEditionV1;
this.supply = args.supply;
this.maxSupply = args.maxSupply;
this.masterMint = args.masterMint;
}
}
export class Metadata {
key: number;
updateAuthority?: PublicKey;
key: Key;
nonUniqueSpecificUpdateAuthority?: PublicKey;
mint: PublicKey;
name: string;
@ -54,16 +79,16 @@ export class Metadata {
extended?: IMetadataExtension;
constructor(args: {
updateAuthority?: Buffer;
mint: Buffer;
nonUniqueSpecificUpdateAuthority?: PublicKey;
mint: PublicKey;
name: string;
symbol: string;
uri: string;
}) {
this.key = METADATA_KEY;
this.updateAuthority =
args.updateAuthority && new PublicKey(args.updateAuthority);
this.mint = new PublicKey(args.mint);
this.key = Key.MetadataV1;
this.nonUniqueSpecificUpdateAuthority =
args.nonUniqueSpecificUpdateAuthority;
this.mint = args.mint;
this.name = args.name;
this.symbol = args.symbol;
this.uri = args.uri;
@ -71,12 +96,12 @@ export class Metadata {
}
export class NameSymbolTuple {
key: number;
key: Key;
updateAuthority: PublicKey;
metadata: PublicKey;
constructor(args: { updateAuthority: Buffer; metadata: Buffer }) {
this.key = NAME_SYMBOL_KEY;
this.key = Key.NameSymbolTupleV1;
this.updateAuthority = new PublicKey(args.updateAuthority);
this.metadata = new PublicKey(args.metadata);
}
@ -84,7 +109,7 @@ export class NameSymbolTuple {
class CreateMetadataArgs {
instruction: number = 0;
allow_duplicates: boolean = false;
allowDuplicates: boolean = false;
name: string;
symbol: string;
uri: string;
@ -93,31 +118,44 @@ class CreateMetadataArgs {
name: string;
symbol: string;
uri: string;
allow_duplicates?: boolean;
allowDuplicates?: boolean;
}) {
this.name = args.name;
this.symbol = args.symbol;
this.uri = args.uri;
this.allow_duplicates = !!args.allow_duplicates;
this.allowDuplicates = !!args.allowDuplicates;
}
}
class UpdateMetadataArgs {
instruction: number = 1;
uri: string;
// Not used by this app, just required for instruction
non_unique_specific_update_authority: number;
nonUniqueSpecificUpdateAuthority?: PublicKey;
constructor(args: { uri: string }) {
constructor(args: {
uri: string;
nonUniqueSpecificUpdateAuthority?: string;
}) {
this.uri = args.uri;
this.non_unique_specific_update_authority = 0;
this.nonUniqueSpecificUpdateAuthority = args.nonUniqueSpecificUpdateAuthority
? new PublicKey(args.nonUniqueSpecificUpdateAuthority)
: undefined;
}
}
class TransferMetadataArgs {
class TransferUpdateAuthorityArgs {
instruction: number = 2;
constructor() {}
}
class CreateMasterEditionArgs {
instruction: number = 3;
maxSupply?: BN;
constructor(args: { maxSupply?: BN }) {
this.maxSupply = args.maxSupply;
}
}
export const SCHEMA = new Map<any, any>([
[
CreateMetadataArgs,
@ -125,7 +163,7 @@ export const SCHEMA = new Map<any, any>([
kind: 'struct',
fields: [
['instruction', 'u8'],
['allow_duplicates', 'u8'],
['allowDuplicates', 'u8'],
['name', 'string'],
['symbol', 'string'],
['uri', 'string'],
@ -139,25 +177,53 @@ export const SCHEMA = new Map<any, any>([
fields: [
['instruction', 'u8'],
['uri', 'string'],
['non_unique_specific_update_authority', 'u8'],
[
'nonUniqueSpecificUpdateAuthority',
{ kind: 'option', type: 'pubkey' },
],
],
},
],
[
TransferMetadataArgs,
TransferUpdateAuthorityArgs,
{
kind: 'struct',
fields: [['instruction', 'u8']],
},
],
[
CreateMasterEditionArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['maxSupply', { kind: 'option', type: 'u64' }],
],
},
],
[
MasterEdition,
{
kind: 'struct',
fields: [
['key', 'u8'],
['supply', 'u64'],
['maxSupply', { kind: 'option', type: 'u64' }],
['masterMint', 'publicKey'],
],
},
],
[
Metadata,
{
kind: 'struct',
fields: [
['key', 'u8'],
['allow_duplicates', { kind: 'option', type: 'u8' }],
['mint', [32]],
[
'nonUniqueSpecificUpdateAuthority',
{ kind: 'option', type: 'pubkey' },
],
['mint', 'pubkey'],
['name', 'string'],
['symbol', 'string'],
['uri', 'string'],
@ -170,8 +236,8 @@ export const SCHEMA = new Map<any, any>([
kind: 'struct',
fields: [
['key', 'u8'],
['update_authority', [32]],
['metadata', [32]],
['updateAuthority', 'pubkey'],
['metadata', 'pubkey'],
],
},
],
@ -181,37 +247,21 @@ export const decodeMetadata = (buffer: Buffer) => {
return deserializeBorsh(SCHEMA, Metadata, buffer) as Metadata;
};
export async function transferMetadata(
symbol: string,
name: string,
export async function transferUpdateAuthority(
account: PublicKey,
currentUpdateAuthority: PublicKey,
newUpdateAuthority: PublicKey,
instructions: TransactionInstruction[],
signers: Account[],
metadataAccount?: PublicKey,
metadataOwnerAccount?: PublicKey,
) {
const metadataProgramId = programIds().metadata;
metadataOwnerAccount =
metadataOwnerAccount ||
(
await PublicKey.findProgramAddress(
[
Buffer.from('metadata'),
metadataProgramId.toBuffer(),
Buffer.from(name),
Buffer.from(symbol),
],
metadataProgramId,
)
)[0];
const data = Buffer.from(serialize(SCHEMA, new TransferMetadataArgs()));
const data = Buffer.from(
serialize(SCHEMA, new TransferUpdateAuthorityArgs()),
);
const keys = [
{
pubkey: metadataOwnerAccount,
pubkey: account,
isSigner: false,
isWritable: true,
},
@ -233,20 +283,18 @@ export async function transferMetadata(
data: data,
}),
);
return [metadataAccount, metadataOwnerAccount];
}
export async function updateMetadata(
symbol: string,
name: string,
uri: string,
newNonUniqueSpecificUpdateAuthority: string | undefined,
mintKey: PublicKey,
updateAuthority: PublicKey,
instructions: TransactionInstruction[],
signers: Account[],
metadataAccount?: PublicKey,
metadataOwnerAccount?: PublicKey,
nameSymbolAccount?: PublicKey,
) {
const metadataProgramId = programIds().metadata;
@ -263,8 +311,8 @@ export async function updateMetadata(
)
)[0];
metadataOwnerAccount =
metadataOwnerAccount ||
nameSymbolAccount =
nameSymbolAccount ||
(
await PublicKey.findProgramAddress(
[
@ -277,9 +325,13 @@ export async function updateMetadata(
)
)[0];
const value = new UpdateMetadataArgs({ uri });
const value = new UpdateMetadataArgs({
uri,
nonUniqueSpecificUpdateAuthority: !newNonUniqueSpecificUpdateAuthority
? undefined
: newNonUniqueSpecificUpdateAuthority,
});
const data = Buffer.from(serialize(SCHEMA, value));
const keys = [
{
pubkey: metadataAccount,
@ -292,7 +344,7 @@ export async function updateMetadata(
isWritable: false,
},
{
pubkey: metadataOwnerAccount,
pubkey: nameSymbolAccount,
isSigner: false,
isWritable: false,
},
@ -305,20 +357,19 @@ export async function updateMetadata(
}),
);
return [metadataAccount, metadataOwnerAccount];
return [metadataAccount, nameSymbolAccount];
}
export async function createMetadata(
symbol: string,
name: string,
uri: string,
allow_duplicates: boolean,
allowDuplicates: boolean,
updateAuthority: PublicKey,
mintKey: PublicKey,
mintAuthorityKey: PublicKey,
instructions: TransactionInstruction[],
payer: PublicKey,
signers: Account[],
) {
const metadataProgramId = programIds().metadata;
@ -333,7 +384,7 @@ export async function createMetadata(
)
)[0];
const metadataOwnerAccount = (
const nameSymbolAccount = (
await PublicKey.findProgramAddress(
[
Buffer.from('metadata'),
@ -345,12 +396,12 @@ export async function createMetadata(
)
)[0];
const value = new CreateMetadataArgs({ name, symbol, uri, allow_duplicates });
const value = new CreateMetadataArgs({ name, symbol, uri, allowDuplicates });
const data = Buffer.from(serialize(SCHEMA, value));
const keys = [
{
pubkey: metadataOwnerAccount,
pubkey: nameSymbolAccount,
isSigner: false,
isWritable: true,
},
@ -398,5 +449,122 @@ export async function createMetadata(
}),
);
return [metadataAccount, metadataOwnerAccount];
return [metadataAccount, nameSymbolAccount];
}
export async function createMasterEdition(
name: string,
symbol: string,
maxSupply: BN | undefined,
mintKey: PublicKey,
masterMintKey: PublicKey,
updateAuthorityKey: PublicKey,
mintAuthorityKey: PublicKey,
instructions: TransactionInstruction[],
payer: PublicKey,
) {
const metadataProgramId = programIds().metadata;
const metadataAccount = (
await PublicKey.findProgramAddress(
[
Buffer.from('metadata'),
metadataProgramId.toBuffer(),
mintKey.toBuffer(),
],
metadataProgramId,
)
)[0];
const nameSymbolAccount = (
await PublicKey.findProgramAddress(
[
Buffer.from('metadata'),
metadataProgramId.toBuffer(),
Buffer.from(name),
Buffer.from(symbol),
],
metadataProgramId,
)
)[0];
const editionAccount = (
await PublicKey.findProgramAddress(
[
Buffer.from('metadata'),
metadataProgramId.toBuffer(),
mintKey.toBuffer(),
Buffer.from('edition'),
],
metadataProgramId,
)
)[0];
const value = new CreateMasterEditionArgs({ maxSupply });
const data = Buffer.from(serialize(SCHEMA, value));
const keys = [
{
pubkey: editionAccount,
isSigner: false,
isWritable: true,
},
{
pubkey: mintKey,
isSigner: false,
isWritable: true,
},
{
pubkey: masterMintKey,
isSigner: false,
isWritable: false,
},
{
pubkey: updateAuthorityKey,
isSigner: true,
isWritable: false,
},
{
pubkey: mintAuthorityKey,
isSigner: true,
isWritable: false,
},
{
pubkey: metadataAccount,
isSigner: false,
isWritable: false,
},
{
pubkey: nameSymbolAccount,
isSigner: false,
isWritable: false,
},
{
pubkey: payer,
isSigner: true,
isWritable: false,
},
{
pubkey: programIds().token,
isSigner: false,
isWritable: false,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isSigner: false,
isWritable: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: metadataProgramId,
data,
}),
);
}

View File

@ -392,7 +392,7 @@ export async function sendSignedTransaction({
sentMessage?: string;
successMessage?: string;
timeout?: number;
}): Promise<{ txid: string, slot: number }> {
}): Promise<{ txid: string; slot: number }> {
const rawTransaction = signedTransaction.serialize();
const startTime = getUnixTs();
let slot = 0;
@ -415,7 +415,11 @@ export async function sendSignedTransaction({
}
})();
try {
slot = await awaitTransactionSignatureConfirmation(txid, timeout, connection);
slot = await awaitTransactionSignatureConfirmation(
txid,
timeout,
connection,
);
} catch (err) {
if (err.timeout) {
throw new Error('Timed out awaiting confirmation on transaction');
@ -495,7 +499,7 @@ async function awaitTransactionSignatureConfirmation(
try {
subId = connection.onSignature(
txid,
(result) => {
result => {
console.log('WS confirmed', txid, result);
done = true;
if (result.err) {
@ -544,11 +548,17 @@ async function awaitTransactionSignatureConfirmation(
await sleep(500);
}
})();
}).catch(_ => {
connection.removeSignatureListener(subId);
}).then(_ => {
connection.removeSignatureListener(subId);
});
})
.catch(_ => {
//@ts-ignore
if (connection._signatureSubscriptions[subId])
connection.removeSignatureListener(subId);
})
.then(_ => {
//@ts-ignore
if (connection._signatureSubscriptions[subId])
connection.removeSignatureListener(subId);
});
done = true;
return slot;
}

View File

@ -1,14 +1,14 @@
import EventEmitter from "eventemitter3";
import { PublicKey, Transaction } from "@solana/web3.js";
import { notify } from "../../utils/notifications";
import { WalletAdapter } from "@solana/wallet-base";
import EventEmitter from 'eventemitter3';
import { PublicKey, Transaction } from '@solana/web3.js';
import { notify } from '../../utils/notifications';
import { WalletAdapter } from '@solana/wallet-base';
type PhantomEvent = "disconnect" | "connect";
type PhantomEvent = 'disconnect' | 'connect';
type PhantomRequestMethod =
| "connect"
| "disconnect"
| "signTransaction"
| "signAllTransactions";
| 'connect'
| 'disconnect'
| 'signTransaction'
| 'signAllTransactions';
interface PhantomProvider {
publicKey?: PublicKey;
@ -26,6 +26,7 @@ export class PhantomWalletAdapter
extends EventEmitter
implements WalletAdapter {
_provider: PhantomProvider | undefined;
_cachedCorrectKey?: PublicKey;
constructor() {
super();
this.connect = this.connect.bind(this);
@ -40,7 +41,7 @@ export class PhantomWalletAdapter
}
async signAllTransactions(
transactions: Transaction[]
transactions: Transaction[],
): Promise<Transaction[]> {
if (!this._provider) {
return transactions;
@ -50,7 +51,13 @@ export class PhantomWalletAdapter
}
get publicKey() {
return this._provider?.publicKey || null;
// Due to weird phantom bug where their public key isnt quite like ours
if (!this._cachedCorrectKey && this._provider?.publicKey)
this._cachedCorrectKey = new PublicKey(
this._provider.publicKey.toBase58(),
);
return this._cachedCorrectKey || null;
}
async signTransaction(transaction: Transaction) {
@ -70,32 +77,32 @@ export class PhantomWalletAdapter
if ((window as any)?.solana?.isPhantom) {
provider = (window as any).solana;
} else {
window.open("https://phantom.app/", "_blank");
window.open('https://phantom.app/', '_blank');
notify({
message: "Phantom Error",
description: "Please install Phantom wallet from Chrome ",
message: 'Phantom Error',
description: 'Please install Phantom wallet from Chrome ',
});
return;
}
provider.on('connect', () => {
this._provider = provider;
this.emit("connect");
})
this.emit('connect');
});
if (!provider.isConnected) {
await provider.connect();
}
this._provider = provider;
this.emit("connect");
}
this.emit('connect');
};
disconnect() {
if (this._provider) {
this._provider.disconnect();
this._provider = undefined;
this.emit("disconnect");
this.emit('disconnect');
}
}
}

View File

@ -2,12 +2,14 @@ import {
createAssociatedTokenAccountInstruction,
createMint,
createMetadata,
transferMetadata,
transferUpdateAuthority,
programIds,
sendTransaction,
sendTransactions,
notify,
ENV,
updateMetadata,
createMasterEdition,
} from '@oyster/common';
import React from 'react';
import { MintLayout, Token } from '@solana/spl-token';
@ -61,18 +63,23 @@ export const mintNFT = async (
MintLayout.span,
);
// This owner is a temporary signer and owner of metadata we use to circumvent requesting signing
// twice post Arweave. We store in an account (payer) and use it post-Arweave to update MD with new link
// then give control back to the user.
const owner = new Account();
const payer = new Account();
const instructions: TransactionInstruction[] = [...pushInstructions];
const signers: Account[] = [...pushSigners, owner];
// This is only temporarily owned by wallet...transferred to program by createMasterEdition below
const mintKey = createMint(
instructions,
wallet.publicKey,
mintRent,
0,
owner.publicKey,
owner.publicKey,
// Some weird bug with phantom where it's public key doesnt mesh with data encode well
wallet.publicKey,
wallet.publicKey,
signers,
);
@ -100,23 +107,23 @@ export const mintNFT = async (
TOKEN_PROGRAM_ID,
mintKey,
recipientKey,
owner.publicKey,
// Some weird bug with phantom where it's public key doesnt mesh with data encode well
new PublicKey(wallet.publicKey.toBase58()),
[],
1,
),
);
const [metadataAccount, metadataOwnerAccount] = await createMetadata(
const [metadataAccount, nameSymbolAccount] = await createMetadata(
metadata.symbol,
metadata.name,
`https://-------.---/rfX69WKd7Bin_RTbcnH4wM3BuWWsR_ZhWSSqZBLYdMY`,
false,
true,
payer.publicKey,
mintKey,
owner.publicKey,
instructions,
wallet.publicKey,
signers,
);
const block = await connection.getRecentBlockhash('singleGossip');
@ -125,41 +132,74 @@ export const mintNFT = async (
fromPubkey: wallet.publicKey,
toPubkey: payer.publicKey,
lamports: block.feeCalculator.lamportsPerSignature * 2,
}));
}),
);
const response = await sendTransaction(
let masterEdInstruction: TransactionInstruction[] = [];
let masterEdSigner: Account[] = [payer];
// This mint, which allows limited editions to be made, stays with user's wallet.
const masterMint = createMint(
masterEdInstruction,
wallet.publicKey,
mintRent,
0,
// Some weird bug with phantom where it's public key doesnt mesh with data encode well
wallet.publicKey,
wallet.publicKey,
masterEdSigner,
);
// In this instruction, mint authority will be removed from the main mint, while
// minting authority will be maintained for the master mint (which we want.)
await createMasterEdition(
metadata.symbol,
metadata.name,
undefined,
mintKey,
masterMint,
payer.publicKey,
wallet.publicKey,
instructions,
wallet.publicKey,
);
const txIds: string[] = [];
const response = await sendTransactions(
connection,
wallet,
instructions,
signers,
[instructions, masterEdInstruction],
[signers, masterEdSigner],
true,
true,
'max',
false,
block);
txId => {
txIds.push(txId);
},
undefined,
block,
);
// this means we're done getting AR txn setup. Ship it off to ARWeave!
const data = new FormData();
const tags = realFiles.reduce(
(
acc: Record<string, Array<{ name: string; value: string }>>,
f,
) => {
(acc: Record<string, Array<{ name: string; value: string }>>, f) => {
acc[f.name] = [{ name: 'mint', value: mintKey.toBase58() }];
return acc;
},
{},
);
data.append('tags', JSON.stringify(tags));
data.append('transaction', response.txid);
data.append('transaction', txIds[0]);
realFiles.map(f => data.append('file[]', f));
const result: IArweaveResult = await (
await fetch(
// TODO: add CNAME
env === 'mainnet-beta' ?
'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFileProd' :
'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile',
env === 'mainnet-beta'
? 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFileProd'
: 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile',
{
method: 'POST',
body: data,
@ -170,8 +210,7 @@ export const mintNFT = async (
const metadataFile = result.messages?.find(
m => m.filename == RESERVED_TXN_MANIFEST,
);
if (metadataFile?.transactionId && wallet.publicKey)
{
if (metadataFile?.transactionId && wallet.publicKey) {
const updateInstructions: TransactionInstruction[] = [];
const updateSigners: Account[] = [payer];
@ -181,23 +220,19 @@ export const mintNFT = async (
metadata.symbol,
metadata.name,
arweaveLink,
undefined,
mintKey,
payer.publicKey,
updateInstructions,
updateSigners,
metadataAccount,
metadataOwnerAccount,
nameSymbolAccount,
);
await transferMetadata(
metadata.symbol,
metadata.name,
await transferUpdateAuthority(
metadataAccount,
payer.publicKey,
wallet.publicKey,
updateInstructions,
updateSigners,
metadataAccount,
metadataOwnerAccount,
);
const txid = await sendTransaction(
@ -207,12 +242,16 @@ export const mintNFT = async (
updateSigners,
true,
'singleGossip',
true
true,
);
notify({
message: 'Art created on Solana',
description: <a href={arweaveLink} target="_blank" >Arweave Link</a>,
description: (
<a href={arweaveLink} target="_blank">
Arweave Link
</a>
),
type: 'success',
});

View File

@ -20,7 +20,7 @@ import './../styles.less';
import { mintNFT } from '../../actions';
import {
MAX_METADATA_LEN,
MAX_OWNER_LEN,
MAX_NAME_SYMBOL_LEN,
MAX_URI_LENGTH,
useConnection,
useWallet,
@ -28,7 +28,11 @@ import {
MetadataCategory,
useConnectionConfig,
} from '@oyster/common';
import { getAssetCostToStore, LAMPORT_MULTIPLIER, solanaToUSD } from '../../utils/assets';
import {
getAssetCostToStore,
LAMPORT_MULTIPLIER,
solanaToUSD,
} from '../../utils/assets';
import { Connection } from '@solana/web3.js';
import { MintLayout } from '@solana/spl-token';
import { useHistory, useParams } from 'react-router-dom';
@ -41,8 +45,8 @@ export const ArtCreateView = () => {
const connection = useConnection();
const { env } = useConnectionConfig();
const { wallet, connected } = useWallet();
const { step_param }: { step_param: string } = useParams()
const history = useHistory()
const { step_param }: { step_param: string } = useParams();
const history = useHistory();
const [step, setStep] = useState<number>(0);
const [saving, setSaving] = useState<boolean>(false);
@ -59,45 +63,48 @@ export const ArtCreateView = () => {
});
useEffect(() => {
if (step_param) setStep(parseInt(step_param))
else gotoStep(0)
}, [step_param])
if (step_param) setStep(parseInt(step_param));
else gotoStep(0);
}, [step_param]);
const gotoStep = (_step: number) => {
history.push(`/art/create/${_step.toString()}`)
}
history.push(`/art/create/${_step.toString()}`);
};
// store files
const mint = async () => {
const metadata = {
...(attributes as any),
image: attributes.files && attributes.files?.[0] && attributes.files[0].name,
image:
attributes.files && attributes.files?.[0] && attributes.files[0].name,
files: (attributes?.files || []).map(f => f.name),
}
setSaving(true)
const inte = setInterval(() => setProgress(prog => prog + 1), 600)
};
setSaving(true);
const inte = setInterval(() => setProgress(prog => prog + 1), 600);
// Update progress inside mintNFT
await mintNFT(connection, wallet, env, (attributes?.files || []), metadata)
clearInterval(inte)
}
await mintNFT(connection, wallet, env, attributes?.files || [], metadata);
clearInterval(inte);
};
return (
<>
<Row style={{ paddingTop: 50 }}>
{!saving && <Col xl={5}>
<Steps
progressDot
direction="vertical"
current={step}
style={{ width: 200, marginLeft: 20, marginRight: 30 }}
>
<Step title="Category" />
<Step title="Upload" />
<Step title="Info" />
<Step title="Royalties" />
<Step title="Launch" />
</Steps>
</Col>}
{!saving && (
<Col xl={5}>
<Steps
progressDot
direction="vertical"
current={step}
style={{ width: 200, marginLeft: 20, marginRight: 30 }}
>
<Step title="Category" />
<Step title="Upload" />
<Step title="Info" />
<Step title="Royalties" />
<Step title="Launch" />
</Steps>
</Col>
)}
<Col {...(saving ? { xl: 24 } : { xl: 16 })}>
{step === 0 && (
<CategoryStep
@ -146,17 +153,19 @@ export const ArtCreateView = () => {
confirm={() => gotoStep(6)}
/>
)}
{step === 6 && (
<Congrats />
{step === 6 && <Congrats />}
{0 < step && step < 5 && (
<Button onClick={() => gotoStep(step - 1)}>Back</Button>
)}
{(0 < step && step < 5) && <Button onClick={() => gotoStep(step - 1)}>Back</Button>}
</Col>
</Row>
</>
);
};
const CategoryStep = (props: { confirm: (category: MetadataCategory) => void }) => {
const CategoryStep = (props: {
confirm: (category: MetadataCategory) => void;
}) => {
return (
<>
<Row className="call-to-action">
@ -190,7 +199,6 @@ const CategoryStep = (props: { confirm: (category: MetadataCategory) => void })
<div className="type-btn-description">MP3, WAV, FLAC</div>
</div>
</Button>
</Row>
<Row>
<Button
@ -228,29 +236,29 @@ const UploadStep = (props: {
setAttributes: (attr: IMetadataExtension) => void;
confirm: () => void;
}) => {
const [mainFile, setMainFile] = useState<any>()
const [coverFile, setCoverFile] = useState<any>()
const [image, setImage] = useState<string>("")
const [mainFile, setMainFile] = useState<any>();
const [coverFile, setCoverFile] = useState<any>();
const [image, setImage] = useState<string>('');
useEffect(() => {
props.setAttributes({
...props.attributes,
files: []
})
}, [])
files: [],
});
}, []);
const uploadMsg = (category: MetadataCategory) => {
switch (category) {
case MetadataCategory.Audio:
return "Upload your audio creation (MP3, FLAC, WAV)"
return 'Upload your audio creation (MP3, FLAC, WAV)';
case MetadataCategory.Image:
return "Upload your image creation (PNG, JPG, GIF)"
return 'Upload your image creation (PNG, JPG, GIF)';
case MetadataCategory.Video:
return "Upload your video creation (MP4)"
return 'Upload your video creation (MP4)';
default:
return "Please go back and choose a category"
return 'Please go back and choose a category';
}
}
};
return (
<>
@ -271,61 +279,62 @@ const UploadStep = (props: {
multiple={false}
customRequest={info => {
// dont upload files here, handled outside of the control
info?.onSuccess?.({}, null as any)
info?.onSuccess?.({}, null as any);
}}
fileList={mainFile ? [mainFile] : []}
onChange={async info => {
const file = info.file.originFileObj;
if (file) setMainFile(file)
if (file) setMainFile(file);
if (props.attributes.category != MetadataCategory.Audio) {
const reader = new FileReader();
reader.onload = function (event) {
setImage((event.target?.result as string) || '')
}
if (file) reader.readAsDataURL(file)
setImage((event.target?.result as string) || '');
};
if (file) reader.readAsDataURL(file);
}
}}
>
<div className="ant-upload-drag-icon">
<h3 style={{ fontWeight: 700 }}>Upload your creation</h3>
</div>
<p className="ant-upload-text">
Drag and drop, or click to browse
</p>
<p className="ant-upload-text">Drag and drop, or click to browse</p>
</Dragger>
</Row>
{props.attributes.category == MetadataCategory.Audio &&
{props.attributes.category == MetadataCategory.Audio && (
<Row className="content-action">
<h3>Optionally, you can upload a cover image or video (PNG, JPG, GIF, MP4)</h3>
<h3>
Optionally, you can upload a cover image or video (PNG, JPG, GIF,
MP4)
</h3>
<Dragger
style={{ padding: 20 }}
multiple={false}
customRequest={info => {
// dont upload files here, handled outside of the control
info?.onSuccess?.({}, null as any)
info?.onSuccess?.({}, null as any);
}}
fileList={coverFile ? [coverFile] : []}
onChange={async info => {
const file = info.file.originFileObj;
if (file) setCoverFile(file)
if (file) setCoverFile(file);
if (props.attributes.category == MetadataCategory.Audio) {
const reader = new FileReader();
reader.onload = function (event) {
setImage((event.target?.result as string) || '')
}
if (file) reader.readAsDataURL(file)
setImage((event.target?.result as string) || '');
};
if (file) reader.readAsDataURL(file);
}
}}
>
<div className="ant-upload-drag-icon">
<h3 style={{ fontWeight: 700 }}>Upload your cover image or video</h3>
<h3 style={{ fontWeight: 700 }}>
Upload your cover image or video
</h3>
</div>
<p className="ant-upload-text">
Drag and drop, or click to browse
</p>
<p className="ant-upload-text">Drag and drop, or click to browse</p>
</Dragger>
</Row>
}
)}
<Row>
<Button
type="primary"
@ -335,8 +344,8 @@ const UploadStep = (props: {
...props.attributes,
files: [mainFile, coverFile].filter(f => f),
image,
})
props.confirm()
});
props.confirm();
}}
className="action-btn"
>
@ -348,8 +357,8 @@ const UploadStep = (props: {
};
interface Royalty {
creator_key: string,
amount: number
creator_key: string;
amount: number;
}
const InfoStep = (props: {
@ -357,15 +366,17 @@ const InfoStep = (props: {
setAttributes: (attr: IMetadataExtension) => void;
confirm: () => void;
}) => {
const [creators, setCreators] = useState<Array<UserValue>>([])
const [royalties, setRoyalties] = useState<Array<Royalty>>([])
const [creators, setCreators] = useState<Array<UserValue>>([]);
const [royalties, setRoyalties] = useState<Array<Royalty>>([]);
useEffect(() => {
setRoyalties(creators.map(creator => ({
creator_key: creator.key,
amount: Math.trunc(100 / creators.length),
})))
}, [creators])
setRoyalties(
creators.map(creator => ({
creator_key: creator.key,
amount: Math.trunc(100 / creators.length),
})),
);
}, [creators]);
return (
<>
@ -422,9 +433,7 @@ const InfoStep = (props: {
</label>
<label className="action-field">
<span className="field-title">Creators</span>
<UserSearch
setCreators={setCreators}
/>
<UserSearch setCreators={setCreators} />
</label>
<label className="action-field">
<span className="field-title">Description</span>
@ -446,7 +455,11 @@ const InfoStep = (props: {
<Row>
<label className="action-field" style={{ width: '100%' }}>
<span className="field-title">Royalties Split</span>
<RoyaltiesSplitter creators={creators} royalties={royalties} setRoyalties={setRoyalties} />
<RoyaltiesSplitter
creators={creators}
royalties={royalties}
setRoyalties={setRoyalties}
/>
</label>
</Row>
<Row>
@ -465,63 +478,78 @@ const InfoStep = (props: {
const shuffle = (array: Array<any>) => {
array.sort(() => Math.random() - 0.5);
}
};
const RoyaltiesSplitter = (props: {
creators: Array<UserValue>,
royalties: Array<Royalty>,
setRoyalties: Function,
creators: Array<UserValue>;
royalties: Array<Royalty>;
setRoyalties: Function;
}) => {
return (
<Col>
{props.creators.map((creator, idx) => {
const royalty = props.royalties.find(royalty => royalty.creator_key == creator.key)
if (!royalty) return null
const royalty = props.royalties.find(
royalty => royalty.creator_key == creator.key,
);
if (!royalty) return null;
const amt = royalty.amount
const amt = royalty.amount;
const handleSlide = (newAmt: number) => {
const othersRoyalties = props.royalties.filter(_royalty => _royalty.creator_key != royalty.creator_key)
if (othersRoyalties.length < 1) return
shuffle(othersRoyalties)
const others_n = props.royalties.length - 1
const sign = Math.sign(newAmt - amt)
let remaining = Math.abs(newAmt - amt)
let count = 0
const othersRoyalties = props.royalties.filter(
_royalty => _royalty.creator_key != royalty.creator_key,
);
if (othersRoyalties.length < 1) return;
shuffle(othersRoyalties);
const others_n = props.royalties.length - 1;
const sign = Math.sign(newAmt - amt);
let remaining = Math.abs(newAmt - amt);
let count = 0;
while (remaining > 0 && count < 100) {
const idx = count % others_n
const _royalty = othersRoyalties[idx]
const idx = count % others_n;
const _royalty = othersRoyalties[idx];
if (
(0 < _royalty.amount && _royalty.amount < 100) // Normal
|| (_royalty.amount == 0 && sign < 0) // Low limit
|| (_royalty.amount == 100 && sign > 0) // High limit
(0 < _royalty.amount && _royalty.amount < 100) || // Normal
(_royalty.amount == 0 && sign < 0) || // Low limit
(_royalty.amount == 100 && sign > 0) // High limit
) {
_royalty.amount -= sign
remaining -= 1
_royalty.amount -= sign;
remaining -= 1;
}
count += 1
count += 1;
}
props.setRoyalties(props.royalties.map(_royalty => {
const computed_amount = othersRoyalties.find(newRoyalty =>
newRoyalty.creator_key == _royalty.creator_key
)?.amount
return {
..._royalty,
amount: _royalty.creator_key == royalty.creator_key ? newAmt : computed_amount,
}
}))
}
props.setRoyalties(
props.royalties.map(_royalty => {
const computed_amount = othersRoyalties.find(
newRoyalty => newRoyalty.creator_key == _royalty.creator_key,
)?.amount;
return {
..._royalty,
amount:
_royalty.creator_key == royalty.creator_key
? newAmt
: computed_amount,
};
}),
);
};
return (
<Row key={idx} style={{ margin: '5px auto' }}>
<Col span={11} className="slider-elem">{creator.label}</Col>
<Col span={8} className="slider-elem">{amt}%</Col>
<Col span={4}><Slider value={amt} onChange={handleSlide} /></Col>
<Col span={11} className="slider-elem">
{creator.label}
</Col>
<Col span={8} className="slider-elem">
{amt}%
</Col>
<Col span={4}>
<Slider value={amt} onChange={handleSlide} />
</Col>
</Row>
)
);
})}
</Col>
)
}
);
};
const RoyaltiesStep = (props: {
attributes: IMetadataExtension;
@ -595,15 +623,9 @@ const LaunchStep = (props: {
const [USDcost, setUSDcost] = useState(0);
useEffect(() => {
const rentCall = Promise.all([
props.connection.getMinimumBalanceForRentExemption(
MintLayout.span,
),
props.connection.getMinimumBalanceForRentExemption(
MAX_METADATA_LEN,
),
props.connection.getMinimumBalanceForRentExemption(
MAX_OWNER_LEN,
)
props.connection.getMinimumBalanceForRentExemption(MintLayout.span),
props.connection.getMinimumBalanceForRentExemption(MAX_METADATA_LEN),
props.connection.getMinimumBalanceForRentExemption(MAX_NAME_SYMBOL_LEN),
]);
getAssetCostToStore([
@ -630,8 +652,8 @@ const LaunchStep = (props: {
}, [...files, setCost]);
useEffect(() => {
cost && solanaToUSD(cost).then(setUSDcost)
}, [cost])
cost && solanaToUSD(cost).then(setUSDcost);
}, [cost]);
return (
<>
@ -661,21 +683,25 @@ const LaunchStep = (props: {
value={props.attributes.royalty}
suffix="%"
/>
{cost ? <div style={{ display: 'flex' }}>
<Statistic
className="create-statistic"
title="Cost to Create"
value={cost.toPrecision(3)}
prefix="◎"
/>
<div style={{
margin: "auto 0",
color: "rgba(255, 255, 255, 0.4)",
fontSize: "1.5rem",
}}>
${USDcost.toPrecision(2)}
{cost ? (
<div style={{ display: 'flex' }}>
<Statistic
className="create-statistic"
title="Cost to Create"
value={cost.toPrecision(3)}
prefix="◎"
/>
<div
style={{
margin: 'auto 0',
color: 'rgba(255, 255, 255, 0.4)',
fontSize: '1.5rem',
}}
>
${USDcost.toPrecision(2)}
</div>
</div>
</div> : (
) : (
<Spin />
)}
</Col>
@ -703,46 +729,52 @@ const LaunchStep = (props: {
};
const WaitingStep = (props: {
mint: Function,
progress: number,
confirm: Function,
mint: Function;
progress: number;
confirm: Function;
}) => {
useEffect(() => {
const func = async () => {
await props.mint()
props.confirm()
}
func()
}, [])
await props.mint();
props.confirm();
};
func();
}, []);
return (
<div style={{ marginTop: 70 }}>
<Progress
type="circle"
percent={props.progress}
/>
<Progress type="circle" percent={props.progress} />
<div className="waiting-title">
Your creation is being uploaded to the decentralized web...
</div>
<div className="waiting-subtitle">This can take up to 1 minute.</div>
</div>
)
}
);
};
const Congrats = () => {
return <>
<div style={{ marginTop: 70 }}>
<div className="waiting-title">
Congratulations, you created an NFT!
return (
<>
<div style={{ marginTop: 70 }}>
<div className="waiting-title">
Congratulations, you created an NFT!
</div>
<div className="congrats-button-container">
<Button className="congrats-button">
<span>Share it on Twitter</span>
<span>&gt;</span>
</Button>
<Button className="congrats-button">
<span>See it in your collection</span>
<span>&gt;</span>
</Button>
<Button className="congrats-button">
<span>Sell it via auction</span>
<span>&gt;</span>
</Button>
</div>
</div>
<div className="congrats-button-container">
<Button className="congrats-button"><span>Share it on Twitter</span><span>&gt;</span></Button>
<Button className="congrats-button"><span>See it in your collection</span><span>&gt;</span></Button>
<Button className="congrats-button"><span>Sell it via auction</span><span>&gt;</span></Button>
</div>
</div>
<Confetti />
</>
}
<Confetti />
</>
);
};

File diff suppressed because it is too large Load Diff