diff --git a/clients/js/src/createMint.ts b/clients/js/src/createMint.ts new file mode 100644 index 00000000..8fe1a5af --- /dev/null +++ b/clients/js/src/createMint.ts @@ -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; + +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
; + /** + * 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, + } + ), + ]); +} diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index 69e4e4eb..c9976168 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1 +1,2 @@ export * from './generated'; +export * from './createMint'; diff --git a/clients/js/test/_setup.ts b/clients/js/test/_setup.ts index 7ed687cd..060f1a1c 100644 --- a/clients/js/test/_setup.ts +++ b/clients/js/test/_setup.ts @@ -9,13 +9,18 @@ import { SolanaRpcSubscriptionsApi, TransactionMessageWithBlockhashLifetime, TransactionMessageWithFeePayer, + TransactionPlanExecutor, + TransactionPlanner, TransactionSigner, airdropFactory, appendTransactionMessageInstructions, assertIsSendableTransaction, + assertIsTransactionWithBlockhashLifetime, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, + createTransactionPlanExecutor, + createTransactionPlanner, generateKeyPairSigner, getSignatureFromTransaction, lamports, @@ -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; diff --git a/clients/js/test/createMint.test.ts b/clients/js/test/createMint.test.ts new file mode 100644 index 00000000..945349d5 --- /dev/null +++ b/clients/js/test/createMint.test.ts @@ -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,