ソースを参照

Add an instruction plan to mint to an ATA (#110)

Callum McIntyre 2 日 前
コミット
3e0522faf6

+ 13 - 13
clients/js/src/createMint.ts

@@ -35,36 +35,36 @@ export type CreateMintInstructionPlanInput = {
 };
 
 type CreateMintInstructionPlanConfig = {
-  systemProgramAddress?: Address;
-  tokenProgramAddress?: Address;
+  systemProgram?: Address;
+  tokenProgram?: Address;
 };
 
 export function createMintInstructionPlan(
-  params: CreateMintInstructionPlanInput,
+  input: CreateMintInstructionPlanInput,
   config?: CreateMintInstructionPlanConfig
 ): InstructionPlan {
   return sequentialInstructionPlan([
     getCreateAccountInstruction(
       {
-        payer: params.payer,
-        newAccount: params.newMint,
-        lamports: params.mintAccountLamports ?? MINIMUM_BALANCE_FOR_MINT,
+        payer: input.payer,
+        newAccount: input.newMint,
+        lamports: input.mintAccountLamports ?? MINIMUM_BALANCE_FOR_MINT,
         space: getMintSize(),
-        programAddress: config?.tokenProgramAddress ?? TOKEN_PROGRAM_ADDRESS,
+        programAddress: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS,
       },
       {
-        programAddress: config?.systemProgramAddress,
+        programAddress: config?.systemProgram,
       }
     ),
     getInitializeMint2Instruction(
       {
-        mint: params.newMint.address,
-        decimals: params.decimals,
-        mintAuthority: params.mintAuthority,
-        freezeAuthority: params.freezeAuthority,
+        mint: input.newMint.address,
+        decimals: input.decimals,
+        mintAuthority: input.mintAuthority,
+        freezeAuthority: input.freezeAuthority,
       },
       {
-        programAddress: config?.tokenProgramAddress,
+        programAddress: config?.tokenProgram,
       }
     ),
   ]);

+ 1 - 0
clients/js/src/index.ts

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

+ 98 - 0
clients/js/src/mintToATA.ts

@@ -0,0 +1,98 @@
+import {
+  InstructionPlan,
+  sequentialInstructionPlan,
+  Address,
+  TransactionSigner,
+} from '@solana/kit';
+import {
+  findAssociatedTokenPda,
+  getCreateAssociatedTokenIdempotentInstruction,
+  getMintToCheckedInstruction,
+  TOKEN_PROGRAM_ADDRESS,
+} from './generated';
+
+type MintToATAInstructionPlanInput = {
+  /** Funding account (must be a system account). */
+  payer: TransactionSigner;
+  /** Associated token account address to mint to.
+   * Will be created if it does not already exist.
+   * Note: Use {@link mintToATAInstructionPlanAsync} instead to derive this automatically.
+   * Note: Use {@link findAssociatedTokenPda} to derive the associated token account address.
+   */
+  ata: Address;
+  /** Wallet address for the associated token account. */
+  owner: Address;
+  /** The token mint for the associated token account. */
+  mint: Address;
+  /** The mint's minting authority or its multisignature account. */
+  mintAuthority: Address | TransactionSigner;
+  /** The amount of new tokens to mint. */
+  amount: number | bigint;
+  /** Expected number of base 10 digits to the right of the decimal place. */
+  decimals: number;
+  multiSigners?: Array<TransactionSigner>;
+};
+
+type MintToATAInstructionPlanConfig = {
+  systemProgram?: Address;
+  tokenProgram?: Address;
+  associatedTokenProgram?: Address;
+};
+
+export function mintToATAInstructionPlan(
+  input: MintToATAInstructionPlanInput,
+  config?: MintToATAInstructionPlanConfig
+): InstructionPlan {
+  return sequentialInstructionPlan([
+    getCreateAssociatedTokenIdempotentInstruction(
+      {
+        payer: input.payer,
+        ata: input.ata,
+        owner: input.owner,
+        mint: input.mint,
+        systemProgram: config?.systemProgram,
+        tokenProgram: config?.tokenProgram,
+      },
+      {
+        programAddress: config?.associatedTokenProgram,
+      }
+    ),
+    // mint to this token account
+    getMintToCheckedInstruction(
+      {
+        mint: input.mint,
+        token: input.ata,
+        mintAuthority: input.mintAuthority,
+        amount: input.amount,
+        decimals: input.decimals,
+        multiSigners: input.multiSigners,
+      },
+      {
+        programAddress: config?.tokenProgram,
+      }
+    ),
+  ]);
+}
+
+type MintToATAInstructionPlanAsyncInput = Omit<
+  MintToATAInstructionPlanInput,
+  'ata'
+>;
+
+export async function mintToATAInstructionPlanAsync(
+  input: MintToATAInstructionPlanAsyncInput,
+  config?: MintToATAInstructionPlanConfig
+): Promise<InstructionPlan> {
+  const [ataAddress] = await findAssociatedTokenPda({
+    owner: input.owner,
+    tokenProgram: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS,
+    mint: input.mint,
+  });
+  return mintToATAInstructionPlan(
+    {
+      ...input,
+      ata: ataAddress,
+    },
+    config
+  );
+}

+ 176 - 0
clients/js/test/mintToATA.test.ts

@@ -0,0 +1,176 @@
+import { Account, generateKeyPairSigner, none } from '@solana/kit';
+import test from 'ava';
+import {
+  AccountState,
+  TOKEN_PROGRAM_ADDRESS,
+  Token,
+  mintToATAInstructionPlan,
+  mintToATAInstructionPlanAsync,
+  fetchToken,
+  findAssociatedTokenPda,
+} from '../src';
+import {
+  createDefaultSolanaClient,
+  createDefaultTransactionPlanExecutor,
+  createDefaultTransactionPlanner,
+  createMint,
+  generateKeyPairSignerWithSol,
+} from './_setup';
+
+test('it creates a new associated token account with an initial balance', async (t) => {
+  // Given a mint account, its mint authority, a token owner and the ATA.
+  const client = createDefaultSolanaClient();
+  const [payer, mintAuthority, owner] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+  ]);
+  const decimals = 2;
+  const mint = await createMint(client, payer, mintAuthority.address, decimals);
+  const [ata] = await findAssociatedTokenPda({
+    mint,
+    owner: owner.address,
+    tokenProgram: TOKEN_PROGRAM_ADDRESS,
+  });
+
+  // When we mint to a token account at this address.
+  const instructionPlan = mintToATAInstructionPlan({
+    payer,
+    ata,
+    mint,
+    owner: owner.address,
+    mintAuthority,
+    amount: 1_000n,
+    decimals,
+  });
+
+  const transactionPlanner = createDefaultTransactionPlanner(client, payer);
+  const transactionPlan = await transactionPlanner(instructionPlan);
+  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
+  await transactionPlanExecutor(transactionPlan);
+
+  // Then we expect the token account to exist and have the following data.
+  t.like(await fetchToken(client.rpc, ata), <Account<Token>>{
+    address: ata,
+    data: {
+      mint,
+      owner: owner.address,
+      amount: 1000n,
+      delegate: none(),
+      state: AccountState.Initialized,
+      isNative: none(),
+      delegatedAmount: 0n,
+      closeAuthority: none(),
+    },
+  });
+});
+
+test('it derives a new associated token account with an initial balance', async (t) => {
+  // Given a mint account, its mint authority, a token owner and the ATA.
+  const client = createDefaultSolanaClient();
+  const [payer, mintAuthority, owner] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+  ]);
+  const decimals = 2;
+  const mint = await createMint(client, payer, mintAuthority.address, decimals);
+
+  // When we mint to a token account for the mint.
+  const instructionPlan = await mintToATAInstructionPlanAsync({
+    payer,
+    mint,
+    owner: owner.address,
+    mintAuthority,
+    amount: 1_000n,
+    decimals,
+  });
+
+  const transactionPlanner = createDefaultTransactionPlanner(client, payer);
+  const transactionPlan = await transactionPlanner(instructionPlan);
+  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
+  await transactionPlanExecutor(transactionPlan);
+
+  // Then we expect the token account to exist and have the following data.
+  const [ata] = await findAssociatedTokenPda({
+    mint,
+    owner: owner.address,
+    tokenProgram: TOKEN_PROGRAM_ADDRESS,
+  });
+
+  t.like(await fetchToken(client.rpc, ata), <Account<Token>>{
+    address: ata,
+    data: {
+      mint,
+      owner: owner.address,
+      amount: 1000n,
+      delegate: none(),
+      state: AccountState.Initialized,
+      isNative: none(),
+      delegatedAmount: 0n,
+      closeAuthority: none(),
+    },
+  });
+});
+
+test('it also mints to an existing associated token account', async (t) => {
+  // Given a mint account, its mint authority, a token owner and the ATA.
+  const client = createDefaultSolanaClient();
+  const [payer, mintAuthority, owner] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+  ]);
+  const decimals = 2;
+  const mint = await createMint(client, payer, mintAuthority.address, decimals);
+  const [ata] = await findAssociatedTokenPda({
+    mint,
+    owner: owner.address,
+    tokenProgram: TOKEN_PROGRAM_ADDRESS,
+  });
+
+  // When we create and initialize a token account at this address.
+  const instructionPlan = mintToATAInstructionPlan({
+    payer,
+    ata,
+    mint,
+    owner: owner.address,
+    mintAuthority,
+    amount: 1_000n,
+    decimals,
+  });
+
+  const transactionPlanner = createDefaultTransactionPlanner(client, payer);
+  const transactionPlan = await transactionPlanner(instructionPlan);
+  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
+  await transactionPlanExecutor(transactionPlan);
+
+  // And then we mint additional tokens to the same account.
+  const instructionPlan2 = mintToATAInstructionPlan({
+    payer,
+    ata,
+    mint,
+    owner: owner.address,
+    mintAuthority,
+    amount: 1_000n,
+    decimals,
+  });
+
+  const transactionPlan2 = await transactionPlanner(instructionPlan2);
+  await transactionPlanExecutor(transactionPlan2);
+
+  // Then we expect the token account to exist and have the following data.
+  t.like(await fetchToken(client.rpc, ata), <Account<Token>>{
+    address: ata,
+    data: {
+      mint,
+      owner: owner.address,
+      amount: 2000n,
+      delegate: none(),
+      state: AccountState.Initialized,
+      isNative: none(),
+      delegatedAmount: 0n,
+      closeAuthority: none(),
+    },
+  });
+});