Prechádzať zdrojové kódy

Add an instruction plan for creating a mint (#109)

Callum McIntyre 2 dní pred
rodič
commit
62944d66ce

+ 71 - 0
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 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 - 0
clients/js/src/index.ts

@@ -1 +1,2 @@
 export * from './generated';
+export * from './createMint';

+ 43 - 0
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;

+ 77 - 0
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, <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),
+    },
+  });
+});