瀏覽代碼

Add an instruction plan to transfer to an ATA

Callum 2 天之前
父節點
當前提交
0eae522836

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

@@ -39,7 +39,7 @@ type CreateMintInstructionPlanConfig = {
   tokenProgram?: Address;
 };
 
-export function createMintInstructionPlan(
+export function getCreateMintInstructionPlan(
   input: CreateMintInstructionPlanInput,
   config?: CreateMintInstructionPlanConfig
 ): InstructionPlan {

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

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

+ 4 - 4
clients/js/src/mintToATA.ts

@@ -16,7 +16,7 @@ type MintToATAInstructionPlanInput = {
   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 getMintToATAInstructionPlanAsync} instead to derive this automatically.
    * Note: Use {@link findAssociatedTokenPda} to derive the associated token account address.
    */
   ata: Address;
@@ -39,7 +39,7 @@ type MintToATAInstructionPlanConfig = {
   associatedTokenProgram?: Address;
 };
 
-export function mintToATAInstructionPlan(
+export function getMintToATAInstructionPlan(
   input: MintToATAInstructionPlanInput,
   config?: MintToATAInstructionPlanConfig
 ): InstructionPlan {
@@ -79,7 +79,7 @@ type MintToATAInstructionPlanAsyncInput = Omit<
   'ata'
 >;
 
-export async function mintToATAInstructionPlanAsync(
+export async function getMintToATAInstructionPlanAsync(
   input: MintToATAInstructionPlanAsyncInput,
   config?: MintToATAInstructionPlanConfig
 ): Promise<InstructionPlan> {
@@ -88,7 +88,7 @@ export async function mintToATAInstructionPlanAsync(
     tokenProgram: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS,
     mint: input.mint,
   });
-  return mintToATAInstructionPlan(
+  return getMintToATAInstructionPlan(
     {
       ...input,
       ata: ataAddress,

+ 100 - 0
clients/js/src/transferToATA.ts

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

+ 62 - 20
clients/js/test/_setup.ts

@@ -9,7 +9,8 @@ import {
   SolanaRpcSubscriptionsApi,
   TransactionMessageWithBlockhashLifetime,
   TransactionMessageWithFeePayer,
-  TransactionPlanExecutor,
+  TransactionPlan,
+  TransactionPlanResult,
   TransactionPlanner,
   TransactionSigner,
   airdropFactory,
@@ -32,9 +33,11 @@ import {
 } from '@solana/kit';
 import {
   TOKEN_PROGRAM_ADDRESS,
+  findAssociatedTokenPda,
   getInitializeAccountInstruction,
   getInitializeMintInstruction,
   getMintSize,
+  getMintToATAInstructionPlan,
   getMintToInstruction,
   getTokenSize,
 } from '../src';
@@ -42,12 +45,35 @@ import {
 type Client = {
   rpc: Rpc<SolanaRpcApi>;
   rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
+  sendTransactionPlan: (
+    transactionPlan: TransactionPlan
+  ) => Promise<TransactionPlanResult>;
 };
 
 export const createDefaultSolanaClient = (): Client => {
   const rpc = createSolanaRpc('http://127.0.0.1:8899');
   const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
-  return { rpc, rpcSubscriptions };
+
+  const sendAndConfirm = sendAndConfirmTransactionFactory({
+    rpc,
+    rpcSubscriptions,
+  });
+  const transactionPlanExecutor = createTransactionPlanExecutor({
+    executeTransactionMessage: async (transactionMessage) => {
+      const signedTransaction =
+        await signTransactionMessageWithSigners(transactionMessage);
+      assertIsSendableTransaction(signedTransaction);
+      assertIsTransactionWithBlockhashLifetime(signedTransaction);
+      await sendAndConfirm(signedTransaction, { commitment: 'confirmed' });
+      return { transaction: signedTransaction };
+    },
+  });
+
+  const sendTransactionPlan = async (transactionPlan: TransactionPlan) => {
+    return transactionPlanExecutor(transactionPlan);
+  };
+
+  return { rpc, rpcSubscriptions, sendTransactionPlan };
 };
 
 export const generateKeyPairSignerWithSol = async (
@@ -114,24 +140,6 @@ export const createDefaultTransactionPlanner = (
   });
 };
 
-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;
@@ -235,3 +243,37 @@ export const createTokenWithAmount = async (
 
   return token.address;
 };
+
+export const createTokenPdaWithAmount = async (
+  client: Client,
+  payer: TransactionSigner,
+  mintAuthority: TransactionSigner,
+  mint: Address,
+  owner: Address,
+  amount: bigint,
+  decimals: number
+): Promise<Address> => {
+  const [token] = await findAssociatedTokenPda({
+    owner,
+    mint,
+    tokenProgram: TOKEN_PROGRAM_ADDRESS,
+  });
+
+  const transactionPlan = await createDefaultTransactionPlanner(
+    client,
+    payer
+  )(
+    getMintToATAInstructionPlan({
+      payer,
+      ata: token,
+      owner,
+      mint,
+      mintAuthority,
+      amount,
+      decimals,
+    })
+  );
+
+  await client.sendTransactionPlan(transactionPlan);
+  return token;
+};

+ 5 - 8
clients/js/test/createMint.test.ts

@@ -1,11 +1,10 @@
 import { generateKeyPairSigner, Account, some, none } from '@solana/kit';
 import test from 'ava';
-import { fetchMint, Mint, createMintInstructionPlan } from '../src';
+import { fetchMint, Mint, getCreateMintInstructionPlan } from '../src';
 import {
   createDefaultSolanaClient,
   generateKeyPairSignerWithSol,
   createDefaultTransactionPlanner,
-  createDefaultTransactionPlanExecutor,
 } from './_setup';
 
 test('it creates and initializes a new mint account', async (t) => {
@@ -15,7 +14,7 @@ test('it creates and initializes a new mint account', async (t) => {
   const mint = await generateKeyPairSigner();
 
   // When we create and initialize a mint account at this address.
-  const instructionPlan = createMintInstructionPlan({
+  const instructionPlan = getCreateMintInstructionPlan({
     payer: authority,
     newMint: mint,
     decimals: 2,
@@ -24,8 +23,7 @@ test('it creates and initializes a new mint account', async (t) => {
 
   const transactionPlanner = createDefaultTransactionPlanner(client, authority);
   const transactionPlan = await transactionPlanner(instructionPlan);
-  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
-  await transactionPlanExecutor(transactionPlan);
+  await client.sendTransactionPlan(transactionPlan);
 
   // Then we expect the mint account to exist and have the following data.
   const mintAccount = await fetchMint(client.rpc, mint.address);
@@ -52,7 +50,7 @@ test('it creates a new mint account with a freeze authority', async (t) => {
   ]);
 
   // When we create and initialize a mint account at this address.
-  const instructionPlan = createMintInstructionPlan({
+  const instructionPlan = getCreateMintInstructionPlan({
     payer: payer,
     newMint: mint,
     decimals: 2,
@@ -62,8 +60,7 @@ test('it creates a new mint account with a freeze authority', async (t) => {
 
   const transactionPlanner = createDefaultTransactionPlanner(client, payer);
   const transactionPlan = await transactionPlanner(instructionPlan);
-  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
-  await transactionPlanExecutor(transactionPlan);
+  await client.sendTransactionPlan(transactionPlan);
 
   // Then we expect the mint account to exist and have the following data.
   const mintAccount = await fetchMint(client.rpc, mint.address);

+ 10 - 14
clients/js/test/mintToATA.test.ts

@@ -4,14 +4,13 @@ import {
   AccountState,
   TOKEN_PROGRAM_ADDRESS,
   Token,
-  mintToATAInstructionPlan,
-  mintToATAInstructionPlanAsync,
+  getMintToATAInstructionPlan,
+  getMintToATAInstructionPlanAsync,
   fetchToken,
   findAssociatedTokenPda,
 } from '../src';
 import {
   createDefaultSolanaClient,
-  createDefaultTransactionPlanExecutor,
   createDefaultTransactionPlanner,
   createMint,
   generateKeyPairSignerWithSol,
@@ -34,7 +33,7 @@ test('it creates a new associated token account with an initial balance', async
   });
 
   // When we mint to a token account at this address.
-  const instructionPlan = mintToATAInstructionPlan({
+  const instructionPlan = getMintToATAInstructionPlan({
     payer,
     ata,
     mint,
@@ -46,8 +45,7 @@ test('it creates a new associated token account with an initial balance', async
 
   const transactionPlanner = createDefaultTransactionPlanner(client, payer);
   const transactionPlan = await transactionPlanner(instructionPlan);
-  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
-  await transactionPlanExecutor(transactionPlan);
+  await client.sendTransactionPlan(transactionPlan);
 
   // Then we expect the token account to exist and have the following data.
   t.like(await fetchToken(client.rpc, ata), <Account<Token>>{
@@ -77,7 +75,7 @@ test('it derives a new associated token account with an initial balance', async
   const mint = await createMint(client, payer, mintAuthority.address, decimals);
 
   // When we mint to a token account for the mint.
-  const instructionPlan = await mintToATAInstructionPlanAsync({
+  const instructionPlan = await getMintToATAInstructionPlanAsync({
     payer,
     mint,
     owner: owner.address,
@@ -88,8 +86,7 @@ test('it derives a new associated token account with an initial balance', async
 
   const transactionPlanner = createDefaultTransactionPlanner(client, payer);
   const transactionPlan = await transactionPlanner(instructionPlan);
-  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
-  await transactionPlanExecutor(transactionPlan);
+  await client.sendTransactionPlan(transactionPlan);
 
   // Then we expect the token account to exist and have the following data.
   const [ata] = await findAssociatedTokenPda({
@@ -130,7 +127,7 @@ test('it also mints to an existing associated token account', async (t) => {
   });
 
   // When we create and initialize a token account at this address.
-  const instructionPlan = mintToATAInstructionPlan({
+  const instructionPlan = getMintToATAInstructionPlan({
     payer,
     ata,
     mint,
@@ -142,11 +139,10 @@ test('it also mints to an existing associated token account', async (t) => {
 
   const transactionPlanner = createDefaultTransactionPlanner(client, payer);
   const transactionPlan = await transactionPlanner(instructionPlan);
-  const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
-  await transactionPlanExecutor(transactionPlan);
+  await client.sendTransactionPlan(transactionPlan);
 
   // And then we mint additional tokens to the same account.
-  const instructionPlan2 = mintToATAInstructionPlan({
+  const instructionPlan2 = getMintToATAInstructionPlan({
     payer,
     ata,
     mint,
@@ -157,7 +153,7 @@ test('it also mints to an existing associated token account', async (t) => {
   });
 
   const transactionPlan2 = await transactionPlanner(instructionPlan2);
-  await transactionPlanExecutor(transactionPlan2);
+  await client.sendTransactionPlan(transactionPlan2);
 
   // Then we expect the token account to exist and have the following data.
   t.like(await fetchToken(client.rpc, ata), <Account<Token>>{

+ 187 - 0
clients/js/test/transferToATA.test.ts

@@ -0,0 +1,187 @@
+import { generateKeyPairSigner } from '@solana/kit';
+import test from 'ava';
+import {
+  Mint,
+  TOKEN_PROGRAM_ADDRESS,
+  Token,
+  fetchMint,
+  fetchToken,
+  findAssociatedTokenPda,
+  getTransferToATAInstructionPlan,
+  getTransferToATAInstructionPlanAsync,
+} from '../src';
+import {
+  createDefaultSolanaClient,
+  createDefaultTransactionPlanner,
+  createMint,
+  createTokenPdaWithAmount,
+  createTokenWithAmount,
+  generateKeyPairSignerWithSol,
+} from './_setup';
+
+test('it transfers tokens from one account to a new ATA', async (t) => {
+  // Given a mint account, one token account with 100 tokens, and a second owner.
+  const client = createDefaultSolanaClient();
+  const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+  ]);
+  const decimals = 2;
+  const mint = await createMint(client, payer, mintAuthority.address, decimals);
+  const tokenA = await createTokenWithAmount(
+    client,
+    payer,
+    mintAuthority,
+    mint,
+    ownerA.address,
+    100n
+  );
+
+  const [tokenB] = await findAssociatedTokenPda({
+    owner: ownerB.address,
+    mint,
+    tokenProgram: TOKEN_PROGRAM_ADDRESS,
+  });
+
+  // When owner A transfers 50 tokens to owner B.
+  const instructionPlan = getTransferToATAInstructionPlan({
+    payer,
+    mint,
+    source: tokenA,
+    authority: ownerA,
+    destination: tokenB,
+    recipient: ownerB.address,
+    amount: 50n,
+    decimals,
+  });
+
+  const transactionPlanner = createDefaultTransactionPlanner(client, payer);
+  const transactionPlan = await transactionPlanner(instructionPlan);
+  await client.sendTransactionPlan(transactionPlan);
+
+  // Then we expect the mint and token accounts to have the following updated data.
+  const [{ data: mintData }, { data: tokenDataA }, { data: tokenDataB }] =
+    await Promise.all([
+      fetchMint(client.rpc, mint),
+      fetchToken(client.rpc, tokenA),
+      fetchToken(client.rpc, tokenB),
+    ]);
+  t.like(mintData, <Mint>{ supply: 100n });
+  t.like(tokenDataA, <Token>{ amount: 50n });
+  t.like(tokenDataB, <Token>{ amount: 50n });
+});
+
+test('derives a new ATA and transfers tokens to it', async (t) => {
+  // Given a mint account, one token account with 100 tokens, and a second owner.
+  const client = createDefaultSolanaClient();
+  const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+  ]);
+  const decimals = 2;
+  const mint = await createMint(client, payer, mintAuthority.address, decimals);
+  const tokenA = await createTokenWithAmount(
+    client,
+    payer,
+    mintAuthority,
+    mint,
+    ownerA.address,
+    100n
+  );
+
+  // When owner A transfers 50 tokens to owner B.
+  const instructionPlan = await getTransferToATAInstructionPlanAsync({
+    payer,
+    mint,
+    source: tokenA,
+    authority: ownerA,
+    recipient: ownerB.address,
+    amount: 50n,
+    decimals,
+  });
+
+  const transactionPlanner = createDefaultTransactionPlanner(client, payer);
+  const transactionPlan = await transactionPlanner(instructionPlan);
+  await client.sendTransactionPlan(transactionPlan);
+
+  // Then we expect the mint and token accounts to have the following updated data.
+  const [tokenB] = await findAssociatedTokenPda({
+    owner: ownerB.address,
+    mint,
+    tokenProgram: TOKEN_PROGRAM_ADDRESS,
+  });
+
+  const [{ data: mintData }, { data: tokenDataA }, { data: tokenDataB }] =
+    await Promise.all([
+      fetchMint(client.rpc, mint),
+      fetchToken(client.rpc, tokenA),
+      fetchToken(client.rpc, tokenB),
+    ]);
+  t.like(mintData, <Mint>{ supply: 100n });
+  t.like(tokenDataA, <Token>{ amount: 50n });
+  t.like(tokenDataB, <Token>{ amount: 50n });
+});
+
+test('it transfers tokens from one account to an existing ATA', async (t) => {
+  // Given a mint account and two token accounts.
+  // One with 90 tokens and the other with 10 tokens.
+  const client = createDefaultSolanaClient();
+  const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+    generateKeyPairSigner(),
+  ]);
+  const decimals = 2;
+  const mint = await createMint(client, payer, mintAuthority.address, decimals);
+  const [tokenA, tokenB] = await Promise.all([
+    createTokenWithAmount(
+      client,
+      payer,
+      mintAuthority,
+      mint,
+      ownerA.address,
+      90n
+    ),
+    createTokenPdaWithAmount(
+      client,
+      payer,
+      mintAuthority,
+      mint,
+      ownerB.address,
+      10n,
+      decimals
+    ),
+  ]);
+
+  // When owner A transfers 50 tokens to owner B.
+  const instructionPlan = getTransferToATAInstructionPlan({
+    payer,
+    mint,
+    source: tokenA,
+    authority: ownerA,
+    destination: tokenB,
+    recipient: ownerB.address,
+    amount: 50n,
+    decimals,
+  });
+
+  const transactionPlanner = createDefaultTransactionPlanner(client, payer);
+  const transactionPlan = await transactionPlanner(instructionPlan);
+  await client.sendTransactionPlan(transactionPlan);
+
+  // Then we expect the mint and token accounts to have the following updated data.
+  const [{ data: mintData }, { data: tokenDataA }, { data: tokenDataB }] =
+    await Promise.all([
+      fetchMint(client.rpc, mint),
+      fetchToken(client.rpc, tokenA),
+      fetchToken(client.rpc, tokenB),
+    ]);
+  t.like(mintData, <Mint>{ supply: 100n });
+  t.like(tokenDataA, <Token>{ amount: 40n });
+  t.like(tokenDataB, <Token>{ amount: 60n });
+});