Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions clients/js/src/createMint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getCreateAccountInstruction } from '@solana-program/system';
import {
Address,
InstructionPlan,
OptionOrNullable,
sequentialInstructionPlan,
TransactionSigner,
} from '@solana/kit';
import {
getInitializeMint2Instruction,
getMintSize,
TOKEN_PROGRAM_ADDRESS,
} from './generated';

// RPC `getMinimumBalanceForRentExemption` for 82 bytes, which is token mint size
// Hardcoded to avoid requiring an RPC request each time
const MINIMUM_BALANCE_FOR_MINT = 1461600;

export type CreateMintInstructionPlanInput = {
/** Funding account (must be a system account). */
payer: TransactionSigner;
/** New mint account to create. */
newMint: TransactionSigner;
/** Number of base 10 digits to the right of the decimal place. */
decimals: number;
/** The authority/multisignature to mint tokens. */
mintAuthority: Address;
/** The optional freeze authority/multisignature of the mint. */
freezeAuthority?: OptionOrNullable<Address>;
/**
* Optional override for the amount of Lamports to fund the mint account with.
* @default 1461600
* */
mintAccountLamports?: number;
};

type CreateMintInstructionPlanConfig = {
systemProgramAddress?: Address;
tokenProgramAddress?: Address;
};

export function createMintInstructionPlan(
params: CreateMintInstructionPlanInput,
config?: CreateMintInstructionPlanConfig
): InstructionPlan {
return sequentialInstructionPlan([
getCreateAccountInstruction(
{
payer: params.payer,
newAccount: params.newMint,
lamports: params.mintAccountLamports ?? MINIMUM_BALANCE_FOR_MINT,
space: getMintSize(),
programAddress: config?.tokenProgramAddress ?? TOKEN_PROGRAM_ADDRESS,
},
{
programAddress: config?.systemProgramAddress,
}
),
getInitializeMint2Instruction(
{
mint: params.newMint.address,
decimals: params.decimals,
mintAuthority: params.mintAuthority,
freezeAuthority: params.freezeAuthority,
},
{
programAddress: config?.tokenProgramAddress,
}
),
]);
}
1 change: 1 addition & 0 deletions clients/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './generated';
export * from './createMint';
43 changes: 43 additions & 0 deletions clients/js/test/_setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ import {
SolanaRpcSubscriptionsApi,
TransactionMessageWithBlockhashLifetime,
TransactionMessageWithFeePayer,
TransactionPlanExecutor,
TransactionPlanner,
TransactionSigner,
airdropFactory,
appendTransactionMessageInstructions,
assertIsSendableTransaction,
assertIsTransactionWithBlockhashLifetime,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
createTransactionPlanExecutor,
createTransactionPlanner,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
Expand Down Expand Up @@ -83,12 +88,50 @@ export const signAndSendTransaction = async (
await signTransactionMessageWithSigners(transactionMessage);
const signature = getSignatureFromTransaction(signedTransaction);
assertIsSendableTransaction(signedTransaction);
assertIsTransactionWithBlockhashLifetime(signedTransaction);
await sendAndConfirmTransactionFactory(client)(signedTransaction, {
commitment,
});
return signature;
};

export const createDefaultTransactionPlanner = (
client: Client,
feePayer: TransactionSigner
): TransactionPlanner => {
return createTransactionPlanner({
createTransactionMessage: async () => {
const { value: latestBlockhash } = await client.rpc
.getLatestBlockhash()
.send();

return pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
},
});
};

export const createDefaultTransactionPlanExecutor = (
client: Client,
commitment: Commitment = 'confirmed'
): TransactionPlanExecutor => {
return createTransactionPlanExecutor({
executeTransactionMessage: async (transactionMessage) => {
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
assertIsSendableTransaction(signedTransaction);
assertIsTransactionWithBlockhashLifetime(signedTransaction);
await sendAndConfirmTransactionFactory(client)(signedTransaction, {
commitment,
});
return { transaction: signedTransaction };
},
});
};

export const getBalance = async (client: Client, address: Address) =>
(await client.rpc.getBalance(address, { commitment: 'confirmed' }).send())
.value;
Expand Down
77 changes: 77 additions & 0 deletions clients/js/test/createMint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { generateKeyPairSigner, Account, some, none } from '@solana/kit';
import test from 'ava';
import { fetchMint, Mint, createMintInstructionPlan } from '../src';
import {
createDefaultSolanaClient,
generateKeyPairSignerWithSol,
createDefaultTransactionPlanner,
createDefaultTransactionPlanExecutor,
} from './_setup';

test('it creates and initializes a new mint account', async (t) => {
// Given an authority and a mint account.
const client = createDefaultSolanaClient();
const authority = await generateKeyPairSignerWithSol(client);
const mint = await generateKeyPairSigner();

// When we create and initialize a mint account at this address.
const instructionPlan = createMintInstructionPlan({
payer: authority,
newMint: mint,
decimals: 2,
mintAuthority: authority.address,
});

const transactionPlanner = createDefaultTransactionPlanner(client, authority);
const transactionPlan = await transactionPlanner(instructionPlan);
const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
await transactionPlanExecutor(transactionPlan);

// Then we expect the mint account to exist and have the following data.
const mintAccount = await fetchMint(client.rpc, mint.address);
t.like(mintAccount, <Account<Mint>>{
address: mint.address,
data: {
mintAuthority: some(authority.address),
supply: 0n,
decimals: 2,
isInitialized: true,
freezeAuthority: none(),
},
});
});

test('it creates a new mint account with a freeze authority', async (t) => {
// Given an authority and a mint account.
const client = createDefaultSolanaClient();
const [payer, mintAuthority, freezeAuthority, mint] = await Promise.all([
generateKeyPairSignerWithSol(client),
generateKeyPairSigner(),
generateKeyPairSigner(),
generateKeyPairSigner(),
]);

// When we create and initialize a mint account at this address.
const instructionPlan = createMintInstructionPlan({
payer: payer,
newMint: mint,
decimals: 2,
mintAuthority: mintAuthority.address,
freezeAuthority: freezeAuthority.address,
});

const transactionPlanner = createDefaultTransactionPlanner(client, payer);
const transactionPlan = await transactionPlanner(instructionPlan);
const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
await transactionPlanExecutor(transactionPlan);

// Then we expect the mint account to exist and have the following data.
const mintAccount = await fetchMint(client.rpc, mint.address);
t.like(mintAccount, <Account<Mint>>{
address: mint.address,
data: {
mintAuthority: some(mintAuthority.address),
freezeAuthority: some(freezeAuthority.address),
},
});
});