Mint NFT from CLI (#513)

* Mint NFT from CLI

* removed unused schema structures & set maxSupply to 0
This commit is contained in:
Bertrand 2021-10-05 18:28:47 +02:00 committed by GitHub
parent 59873ada64
commit 044df3563a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 504 additions and 0 deletions

45
js/packages/cli/src/cli-nft.ts Executable file
View File

@ -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);

View File

@ -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 };
};

View File

@ -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,

View File

@ -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();