Skip to content

Commit

Permalink
feat: Mint NFT Action (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
kespinola authored Nov 5, 2021
1 parent a8ea43e commit 122d08c
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const rates = await new Coingecko().getRate([Currency.AR, Currency.SOL], Currenc
- [ ] Update
- [ ] Sign
- [ ] Send
- [ ] Mint
- [X] Mint
- [ ] Burn
- [ ] Metaplex
- [ ] Accounts
Expand Down
152 changes: 150 additions & 2 deletions src/actions/mintNFT.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,151 @@
export const mintNFT = async () => {
throw new Error("It's not implemented");
import { MintLayout, TOKEN_PROGRAM_ID, Token } from '@solana/spl-token';
import { Keypair, PublicKey } from '@solana/web3.js';
import { ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token';
import BN from 'bn.js';
import { Connection } from './../Connection';
import { MintTo, CreateAssociatedTokenAccount, CreateMint } from './../programs';
import {
CreateMasterEdition,
CreateMetadata,
Creator,
MasterEdition,
Metadata,
MetadataDataData,
} from './../programs/metadata';
import { Wallet } from './../wallet';
import { sendTransaction } from './transactions';
import { lookup } from './../utils/metadata';

interface MintNFTParams {
connection: Connection;
wallet: Wallet;
uri: string;
maxSupply: number;
}

interface MintNFTResponse {
txId: string;
mint: PublicKey;
metadata: PublicKey;
edition: PublicKey;
}

export const mintNFT = async ({
connection,
wallet,
uri,
maxSupply,
}: MintNFTParams): Promise<MintNFTResponse> => {
const mint = Keypair.generate();

const metadataPDA = await Metadata.getPDA(mint.publicKey);
const editionPDA = await MasterEdition.getPDA(mint.publicKey);

const mintRent = await connection.getMinimumBalanceForRentExemption(MintLayout.span);

const {
name,
symbol,
seller_fee_basis_points,
properties: { creators },
} = await lookup(uri);

const creatorsData = creators.reduce<Creator[]>((memo, { address, share }) => {
const verified = address === wallet.publicKey.toString();

const creator = new Creator({
address,
share,
verified,
});

memo = [...memo, creator];

return memo;
}, []);

const createMintTx = new CreateMint(
{ feePayer: wallet.publicKey },
{
newAccountPubkey: mint.publicKey,
lamports: mintRent,
},
);

const metadataData = new MetadataDataData({
name,
symbol,
uri,
sellerFeeBasisPoints: seller_fee_basis_points,
creators: creatorsData,
});

const createMetadataTx = new CreateMetadata(
{
feePayer: wallet.publicKey,
},
{
metadata: metadataPDA,
metadataData,
updateAuthority: wallet.publicKey,
mint: mint.publicKey,
mintAuthority: wallet.publicKey,
},
);

const recipient = await Token.getAssociatedTokenAddress(
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
mint.publicKey,
wallet.publicKey,
);

const createAssociatedTokenAccountTx = new CreateAssociatedTokenAccount(
{ feePayer: wallet.publicKey },
{
associatedTokenAddress: recipient,
splTokenMintAddress: mint.publicKey,
},
);

const mintToTx = new MintTo(
{ feePayer: wallet.publicKey },
{
mint: mint.publicKey,
dest: recipient,
amount: 1,
},
);

const masterEditionTx = new CreateMasterEdition(
{ feePayer: wallet.publicKey },
{
edition: editionPDA,
metadata: metadataPDA,
updateAuthority: wallet.publicKey,
mint: mint.publicKey,
mintAuthority: wallet.publicKey,
maxSupply: new BN(maxSupply),
},
);

const txId = await sendTransaction({
connection,
signers: [mint],
txs: [
createMintTx,
createMetadataTx,
createAssociatedTokenAccountTx,
mintToTx,
masterEditionTx,
],
wallet,
});

return {
txId,
mint: mint.publicKey,
metadata: metadataPDA,
edition: editionPDA,
};
};
12 changes: 12 additions & 0 deletions src/utils/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import axios, { AxiosResponse } from 'axios';
import { MetadataJson } from './../types';

export const lookup = async (url: string): Promise<MetadataJson> => {
try {
const { data } = await axios.get<any, AxiosResponse<MetadataJson>>(url);

return data;
} catch {
throw new Error(`unable to get metadata json from url ${url}`);
}
};
109 changes: 109 additions & 0 deletions test/actions/mintNFT.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Keypair, PublicKey } from '@solana/web3.js';
import axios, { AxiosResponse } from 'axios';
import { Connection } from './../../src/Connection';
import { NodeWallet, Wallet } from './../../src/wallet';
import { mintNFT } from './../../src/actions';
import { FEE_PAYER } from './../utils';
import { MasterEdition, Metadata } from './../../src/programs/metadata';

jest.mock('axios');

const mockedAxiosGet = axios.get as jest.MockedFunction<typeof axios>;
const uri = 'https://bafkreibj4hjlhf3ehpugvfy6bzhhu2c7frvyhrykjqmoocsvdw24omfqga.ipfs.dweb.link';

describe('minting an NFT', () => {
let connection: Connection;
let mint: Keypair;
let wallet: Wallet;

beforeAll(() => {
connection = new Connection('devnet');
wallet = new NodeWallet(FEE_PAYER);
jest
.spyOn(connection, 'sendRawTransaction')
.mockResolvedValue(
'64Tpr1DNj9UWg1P89Zss5Y4Mh2gGyRUMYZPNenZKY2hiNjsotrCDMBriDrsvhg5BJt3mY4hH6jcparNHCZGhAwf6',
);
});

beforeEach(() => {
mint = Keypair.generate();
jest.spyOn(Keypair, 'generate').mockReturnValue(mint);
});

describe('when can find metadata json', () => {
beforeEach(() => {
const mockedResponse: AxiosResponse = {
data: {
name: 'Holo Design (0)',
symbol: '',
description:
'A holo of some design in a lovely purple, pink, and yellow. Pulled from the Internet. Demo only.',
seller_fee_basis_points: 100,
image:
'https://bafybeidq34cu23fq4u57xu3hp2usqqs7miszscyu4kjqyjo3hv7xea6upe.ipfs.dweb.link',
external_url: '',
properties: {
creators: [
{
address: wallet.publicKey.toString(),
share: 100,
},
],
},
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};

mockedAxiosGet.mockResolvedValue(mockedResponse);
});

test('generates a unique mint and creates metadata plus master edition from metadata URL and max supply', async () => {
const mintResponse = await mintNFT({
connection,
wallet,
uri,
maxSupply: 0,
});

const metadata = await Metadata.getPDA(mint.publicKey);
const edition = await MasterEdition.getPDA(mint.publicKey);

expect(mintResponse).toMatchObject({
metadata,
edition,
mint: mint.publicKey,
});
});
});

describe('when metadata json not found', () => {
beforeEach(() => {
const mockedResponse: AxiosResponse = {
data: {},
status: 404,
statusText: 'NOT FOUND',
headers: {},
config: {},
};

mockedAxiosGet.mockRejectedValue(mockedResponse);
});

test('exits the action and throws an error', async () => {
try {
await mintNFT({
connection,
wallet,
uri,
maxSupply: 0,
});
} catch (e) {
expect(e).not.toBeNull();
}
});
});
});

0 comments on commit 122d08c

Please sign in to comment.