Skip to content
Open
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 minimumBalanceForMint = 1461600;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To flag this, I think it's wasteful/annoying to require you to pass Rpc<GetMinimumBalanceForRentExemption> API and re-calculate this every time, given this value doesn't change (without a server change).

Can change to do that though!

I added an optional minimumBalanceForMintOverride override in case you want to use this with a Solana network with different logic, or to account for a hypothetical change in future before the library is updated, but intended most usage to just not have to think about it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that's a great idea. IIRC we even discussed the possibility of adding a sync helper like that to Kit. Not sure where we landed on that though.

I also like the override parameter. What do you think of calling it mintBalance or mintLamports instead? I'm not a fan of "minimum balance" here because the created account will have this exact balance after the transaction. Also "override" might be redundant since it's optional.

Copy link
Member Author

@mcintyre94 mcintyre94 Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like mintLamports, and good to drop the override!
Edit: Went with mintAccountLamports

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Kit sync helper, my issue is anza-xyz/kit#777

I don't think we really discussed it but I still think this would be worth having. Feels like the risk of this changing frequently is very low.


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 ?? minimumBalanceForMint,
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),
},
});
});