Mint NFT from CLI (#513)
* Mint NFT from CLI * removed unused schema structures & set maxSupply to 0
This commit is contained in:
parent
59873ada64
commit
044df3563a
|
@ -0,0 +1,45 @@
|
|||
import { program } from 'commander';
|
||||
import log from 'loglevel';
|
||||
import { mintNFT } from './commands/mint-nft';
|
||||
import { loadWalletKey } from './helpers/accounts';
|
||||
import { web3 } from '@project-serum/anchor';
|
||||
|
||||
program.version('0.0.1');
|
||||
log.setLevel('info');
|
||||
|
||||
programCommand('mint')
|
||||
.option('-m, --metadata <string>', 'metadata url')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.action(async (directory, cmd) => {
|
||||
const { keypair, env, metadata } = cmd.opts();
|
||||
const solConnection = new web3.Connection(web3.clusterApiUrl(env));
|
||||
const walletKeyPair = loadWalletKey(keypair);
|
||||
await mintNFT(solConnection, walletKeyPair, metadata);
|
||||
});
|
||||
|
||||
function programCommand(name: string) {
|
||||
return program
|
||||
.command(name)
|
||||
.option(
|
||||
'-e, --env <string>',
|
||||
'Solana cluster env name',
|
||||
'devnet', //mainnet-beta, testnet, devnet
|
||||
)
|
||||
.option(
|
||||
'-k, --keypair <path>',
|
||||
`Solana wallet location`,
|
||||
'--keypair not provided',
|
||||
)
|
||||
.option('-l, --log-level <string>', 'log level', setLogLevel);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function setLogLevel(value, prev) {
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
log.info('setting the log value to: ' + value);
|
||||
log.setLevel(value);
|
||||
}
|
||||
|
||||
program.parse(process.argv);
|
|
@ -0,0 +1,203 @@
|
|||
import {
|
||||
createAssociatedTokenAccountInstruction,
|
||||
createMetadataInstruction,
|
||||
createMasterEditionInstruction,
|
||||
} from '../helpers/instructions';
|
||||
import { sendTransactionWithRetryWithKeypair } from '../helpers/transactions';
|
||||
import {
|
||||
getTokenWallet,
|
||||
getMetadata,
|
||||
getMasterEdition,
|
||||
} from '../helpers/accounts';
|
||||
import * as anchor from '@project-serum/anchor';
|
||||
import {
|
||||
Data,
|
||||
Creator,
|
||||
CreateMetadataArgs,
|
||||
CreateMasterEditionArgs,
|
||||
METADATA_SCHEMA,
|
||||
} from '../helpers/schema';
|
||||
import { serialize } from 'borsh';
|
||||
import { TOKEN_PROGRAM_ID } from '../helpers/constants';
|
||||
import fetch from 'node-fetch';
|
||||
import { MintLayout, Token } from '@solana/spl-token';
|
||||
import {
|
||||
Keypair,
|
||||
Connection,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import log from 'loglevel';
|
||||
|
||||
export const mintNFT = async (
|
||||
connection: Connection,
|
||||
walletKeypair: Keypair,
|
||||
metadataLink: string,
|
||||
mutableMetadata: boolean = true,
|
||||
): Promise<{
|
||||
metadataAccount: PublicKey;
|
||||
} | void> => {
|
||||
// Metadata
|
||||
let metadata;
|
||||
try {
|
||||
metadata = await (await fetch(metadataLink, { method: 'GET' })).json();
|
||||
} catch (e) {
|
||||
log.error('Could not find metadata at', metadataLink);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate metadata
|
||||
if (
|
||||
!metadata.name ||
|
||||
!metadata.image ||
|
||||
isNaN(metadata.seller_fee_basis_points) ||
|
||||
!metadata.properties ||
|
||||
!Array.isArray(metadata.properties.creators)
|
||||
) {
|
||||
log.error('Invalid metadata file', metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate creators
|
||||
const metaCreators = metadata.properties.creators;
|
||||
if (
|
||||
metaCreators.some(creator => !creator.address) ||
|
||||
metaCreators.reduce((sum, creator) => creator.share + sum, 0) !== 100
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wallet from keypair
|
||||
const wallet = new anchor.Wallet(walletKeypair);
|
||||
if (!wallet?.publicKey) return;
|
||||
|
||||
// Allocate memory for the account
|
||||
const mintRent = await connection.getMinimumBalanceForRentExemption(
|
||||
MintLayout.span,
|
||||
);
|
||||
|
||||
// Generate a mint
|
||||
const mint = anchor.web3.Keypair.generate();
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
const signers: anchor.web3.Keypair[] = [mint, walletKeypair];
|
||||
|
||||
instructions.push(
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: wallet.publicKey,
|
||||
newAccountPubkey: mint.publicKey,
|
||||
lamports: mintRent,
|
||||
space: MintLayout.span,
|
||||
programId: TOKEN_PROGRAM_ID,
|
||||
}),
|
||||
);
|
||||
instructions.push(
|
||||
Token.createInitMintInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
mint.publicKey,
|
||||
0,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
),
|
||||
);
|
||||
|
||||
const userTokenAccoutAddress = await getTokenWallet(
|
||||
wallet.publicKey,
|
||||
mint.publicKey,
|
||||
);
|
||||
instructions.push(
|
||||
createAssociatedTokenAccountInstruction(
|
||||
userTokenAccoutAddress,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
mint.publicKey,
|
||||
),
|
||||
);
|
||||
|
||||
// Create metadata
|
||||
const metadataAccount = await getMetadata(mint.publicKey);
|
||||
const creators = metaCreators.map(
|
||||
creator =>
|
||||
new Creator({
|
||||
address: creator.address,
|
||||
share: creator.share,
|
||||
verified: 1,
|
||||
}),
|
||||
);
|
||||
const data = new Data({
|
||||
symbol: metadata.symbol,
|
||||
name: metadata.name,
|
||||
uri: metadataLink,
|
||||
sellerFeeBasisPoints: metadata.seller_fee_basis_points,
|
||||
creators: creators,
|
||||
});
|
||||
|
||||
let txnData = Buffer.from(
|
||||
serialize(
|
||||
METADATA_SCHEMA,
|
||||
new CreateMetadataArgs({ data, isMutable: mutableMetadata }),
|
||||
),
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
createMetadataInstruction(
|
||||
metadataAccount,
|
||||
mint.publicKey,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
txnData,
|
||||
),
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
Token.createMintToInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
mint.publicKey,
|
||||
userTokenAccoutAddress,
|
||||
wallet.publicKey,
|
||||
[],
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
||||
// Create master edition
|
||||
const editionAccount = await getMasterEdition(mint.publicKey);
|
||||
txnData = Buffer.from(
|
||||
serialize(
|
||||
METADATA_SCHEMA,
|
||||
new CreateMasterEditionArgs({ maxSupply: new BN(0) }),
|
||||
),
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
createMasterEditionInstruction(
|
||||
metadataAccount,
|
||||
editionAccount,
|
||||
mint.publicKey,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
txnData,
|
||||
),
|
||||
);
|
||||
|
||||
const res = await sendTransactionWithRetryWithKeypair(
|
||||
connection,
|
||||
walletKeypair,
|
||||
instructions,
|
||||
signers,
|
||||
);
|
||||
|
||||
try {
|
||||
await connection.confirmTransaction(res.txid, 'max');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Force wait for max confirmations
|
||||
await connection.getParsedConfirmedTransaction(res.txid, 'confirmed');
|
||||
log.info('NFT created', res.txid);
|
||||
return { metadataAccount };
|
||||
};
|
|
@ -10,6 +10,7 @@ import {
|
|||
CONFIG_LINE_SIZE,
|
||||
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
TOKEN_METADATA_PROGRAM_ID,
|
||||
} from './constants';
|
||||
import * as anchor from '@project-serum/anchor';
|
||||
|
||||
|
@ -63,6 +64,123 @@ export function createAssociatedTokenAccountInstruction(
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
export function createMetadataInstruction(
|
||||
metadataAccount: PublicKey,
|
||||
mint: PublicKey,
|
||||
mintAuthority: PublicKey,
|
||||
payer: PublicKey,
|
||||
updateAuthority: PublicKey,
|
||||
txnData: Buffer,
|
||||
) {
|
||||
const keys = [
|
||||
{
|
||||
pubkey: metadataAccount,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: mint,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: mintAuthority,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: payer,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: updateAuthority,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: SystemProgram.programId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: SYSVAR_RENT_PUBKEY,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: TOKEN_METADATA_PROGRAM_ID,
|
||||
data: txnData,
|
||||
});
|
||||
}
|
||||
|
||||
export function createMasterEditionInstruction(
|
||||
metadataAccount: PublicKey,
|
||||
editionAccount: PublicKey,
|
||||
mint: PublicKey,
|
||||
mintAuthority: PublicKey,
|
||||
payer: PublicKey,
|
||||
updateAuthority: PublicKey,
|
||||
txnData: Buffer,
|
||||
) {
|
||||
|
||||
const keys = [
|
||||
{
|
||||
pubkey: editionAccount,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: mint,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: updateAuthority,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: mintAuthority,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: payer,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: metadataAccount,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: TOKEN_PROGRAM_ID,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: SystemProgram.programId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: SYSVAR_RENT_PUBKEY,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: TOKEN_METADATA_PROGRAM_ID,
|
||||
data: txnData,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createConfigAccount(
|
||||
anchorProgram,
|
||||
configData,
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
import { BinaryReader, BinaryWriter } from 'borsh';
|
||||
import base58 from 'bs58';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
type StringPublicKey = string;
|
||||
|
||||
import BN from 'bn.js';
|
||||
|
||||
export class Creator {
|
||||
address: StringPublicKey;
|
||||
verified: number;
|
||||
share: number;
|
||||
|
||||
constructor(args: {
|
||||
address: StringPublicKey;
|
||||
verified: number;
|
||||
share: number;
|
||||
}) {
|
||||
this.address = args.address;
|
||||
this.verified = args.verified;
|
||||
this.share = args.share;
|
||||
}
|
||||
}
|
||||
|
||||
export class Data {
|
||||
name: string;
|
||||
symbol: string;
|
||||
uri: string;
|
||||
sellerFeeBasisPoints: number;
|
||||
creators: Creator[] | null;
|
||||
constructor(args: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
uri: string;
|
||||
sellerFeeBasisPoints: number;
|
||||
creators: Creator[] | null;
|
||||
}) {
|
||||
this.name = args.name;
|
||||
this.symbol = args.symbol;
|
||||
this.uri = args.uri;
|
||||
this.sellerFeeBasisPoints = args.sellerFeeBasisPoints;
|
||||
this.creators = args.creators;
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateMetadataArgs {
|
||||
instruction: number = 0;
|
||||
data: Data;
|
||||
isMutable: boolean;
|
||||
|
||||
constructor(args: { data: Data; isMutable: boolean }) {
|
||||
this.data = args.data;
|
||||
this.isMutable = args.isMutable;
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateMasterEditionArgs {
|
||||
instruction: number = 10;
|
||||
maxSupply: BN | null;
|
||||
constructor(args: { maxSupply: BN | null }) {
|
||||
this.maxSupply = args.maxSupply;
|
||||
}
|
||||
}
|
||||
|
||||
export const METADATA_SCHEMA = new Map<any, any>([
|
||||
[
|
||||
CreateMetadataArgs,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['instruction', 'u8'],
|
||||
['data', Data],
|
||||
['isMutable', 'u8'], // bool
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateMasterEditionArgs,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['instruction', 'u8'],
|
||||
['maxSupply', { kind: 'option', type: 'u64' }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Data,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['name', 'string'],
|
||||
['symbol', 'string'],
|
||||
['uri', 'string'],
|
||||
['sellerFeeBasisPoints', 'u16'],
|
||||
['creators', { kind: 'option', type: [Creator] }],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
Creator,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['address', 'pubkeyAsString'],
|
||||
['verified', 'u8'],
|
||||
['share', 'u8'],
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export const extendBorsh = () => {
|
||||
(BinaryReader.prototype as any).readPubkey = function () {
|
||||
const reader = this as unknown as BinaryReader;
|
||||
const array = reader.readFixedArray(32);
|
||||
return new PublicKey(array);
|
||||
};
|
||||
|
||||
(BinaryWriter.prototype as any).writePubkey = function (value: PublicKey) {
|
||||
const writer = this as unknown as BinaryWriter;
|
||||
writer.writeFixedArray(value.toBuffer());
|
||||
};
|
||||
|
||||
(BinaryReader.prototype as any).readPubkeyAsString = function () {
|
||||
const reader = this as unknown as BinaryReader;
|
||||
const array = reader.readFixedArray(32);
|
||||
return base58.encode(array) as StringPublicKey;
|
||||
};
|
||||
|
||||
(BinaryWriter.prototype as any).writePubkeyAsString = function (
|
||||
value: StringPublicKey,
|
||||
) {
|
||||
const writer = this as unknown as BinaryWriter;
|
||||
writer.writeFixedArray(base58.decode(value));
|
||||
};
|
||||
};
|
||||
|
||||
extendBorsh();
|
Loading…
Reference in New Issue