diff --git a/clients/js/src/createMint.ts b/clients/js/src/createMint.ts index e52ea81..23d4f37 100644 --- a/clients/js/src/createMint.ts +++ b/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, } ), ]); diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index c997616..0b19bf2 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1,2 +1,3 @@ export * from './generated'; export * from './createMint'; +export * from './mintToATA'; diff --git a/clients/js/src/mintToATA.ts b/clients/js/src/mintToATA.ts new file mode 100644 index 0000000..8e999d3 --- /dev/null +++ b/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; +}; + +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 { + const [ataAddress] = await findAssociatedTokenPda({ + owner: input.owner, + tokenProgram: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS, + mint: input.mint, + }); + return mintToATAInstructionPlan( + { + ...input, + ata: ataAddress, + }, + config + ); +} diff --git a/clients/js/test/mintToATA.test.ts b/clients/js/test/mintToATA.test.ts new file mode 100644 index 0000000..5e9e3b0 --- /dev/null +++ b/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), >{ + 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), >{ + 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), >{ + address: ata, + data: { + mint, + owner: owner.address, + amount: 2000n, + delegate: none(), + state: AccountState.Initialized, + isNative: none(), + delegatedAmount: 0n, + closeAuthority: none(), + }, + }); +});