Skip to content

Commit 0e1dc3e

Browse files
authored
Add an instruction plan to transfer to an ATA (#111)
1 parent 37b4f7d commit 0e1dc3e

File tree

8 files changed

+370
-47
lines changed

8 files changed

+370
-47
lines changed

clients/js/src/createMint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type CreateMintInstructionPlanConfig = {
3939
tokenProgram?: Address;
4040
};
4141

42-
export function createMintInstructionPlan(
42+
export function getCreateMintInstructionPlan(
4343
input: CreateMintInstructionPlanInput,
4444
config?: CreateMintInstructionPlanConfig
4545
): InstructionPlan {

clients/js/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './generated';
22
export * from './createMint';
33
export * from './mintToATA';
4+
export * from './transferToATA';

clients/js/src/mintToATA.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type MintToATAInstructionPlanInput = {
1616
payer: TransactionSigner;
1717
/** Associated token account address to mint to.
1818
* Will be created if it does not already exist.
19-
* Note: Use {@link mintToATAInstructionPlanAsync} instead to derive this automatically.
19+
* Note: Use {@link getMintToATAInstructionPlanAsync} instead to derive this automatically.
2020
* Note: Use {@link findAssociatedTokenPda} to derive the associated token account address.
2121
*/
2222
ata: Address;
@@ -39,7 +39,7 @@ type MintToATAInstructionPlanConfig = {
3939
associatedTokenProgram?: Address;
4040
};
4141

42-
export function mintToATAInstructionPlan(
42+
export function getMintToATAInstructionPlan(
4343
input: MintToATAInstructionPlanInput,
4444
config?: MintToATAInstructionPlanConfig
4545
): InstructionPlan {
@@ -79,7 +79,7 @@ type MintToATAInstructionPlanAsyncInput = Omit<
7979
'ata'
8080
>;
8181

82-
export async function mintToATAInstructionPlanAsync(
82+
export async function getMintToATAInstructionPlanAsync(
8383
input: MintToATAInstructionPlanAsyncInput,
8484
config?: MintToATAInstructionPlanConfig
8585
): Promise<InstructionPlan> {
@@ -88,7 +88,7 @@ export async function mintToATAInstructionPlanAsync(
8888
tokenProgram: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS,
8989
mint: input.mint,
9090
});
91-
return mintToATAInstructionPlan(
91+
return getMintToATAInstructionPlan(
9292
{
9393
...input,
9494
ata: ataAddress,

clients/js/src/transferToATA.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
InstructionPlan,
3+
sequentialInstructionPlan,
4+
Address,
5+
TransactionSigner,
6+
} from '@solana/kit';
7+
import {
8+
findAssociatedTokenPda,
9+
getCreateAssociatedTokenIdempotentInstruction,
10+
getTransferCheckedInstruction,
11+
TOKEN_PROGRAM_ADDRESS,
12+
} from './generated';
13+
14+
type TransferToATAInstructionPlanInput = {
15+
/** Funding account (must be a system account). */
16+
payer: TransactionSigner;
17+
/** The token mint to transfer. */
18+
mint: Address;
19+
/** The source account for the transfer. */
20+
source: Address;
21+
/** The source account's owner/delegate or its multisignature account. */
22+
authority: Address | TransactionSigner;
23+
/** Associated token account address to transfer to.
24+
* Will be created if it does not already exist.
25+
* Note: Use {@link getTransferToATAInstructionPlanAsync} instead to derive this automatically.
26+
* Note: Use {@link findAssociatedTokenPda} to derive the associated token account address.
27+
*/
28+
destination: Address;
29+
/** Wallet address for the destination. */
30+
recipient: Address;
31+
/** The amount of tokens to transfer. */
32+
amount: number | bigint;
33+
/** Expected number of base 10 digits to the right of the decimal place. */
34+
decimals: number;
35+
multiSigners?: Array<TransactionSigner>;
36+
};
37+
38+
type TransferToATAInstructionPlanConfig = {
39+
systemProgram?: Address;
40+
tokenProgram?: Address;
41+
associatedTokenProgram?: Address;
42+
};
43+
44+
export function getTransferToATAInstructionPlan(
45+
input: TransferToATAInstructionPlanInput,
46+
config?: TransferToATAInstructionPlanConfig
47+
): InstructionPlan {
48+
return sequentialInstructionPlan([
49+
getCreateAssociatedTokenIdempotentInstruction(
50+
{
51+
payer: input.payer,
52+
ata: input.destination,
53+
owner: input.recipient,
54+
mint: input.mint,
55+
systemProgram: config?.systemProgram,
56+
tokenProgram: config?.tokenProgram,
57+
},
58+
{
59+
programAddress: config?.associatedTokenProgram,
60+
}
61+
),
62+
getTransferCheckedInstruction(
63+
{
64+
source: input.source,
65+
mint: input.mint,
66+
destination: input.destination,
67+
authority: input.authority,
68+
amount: input.amount,
69+
decimals: input.decimals,
70+
multiSigners: input.multiSigners,
71+
},
72+
{
73+
programAddress: config?.tokenProgram,
74+
}
75+
),
76+
]);
77+
}
78+
79+
type TransferToATAInstructionPlanAsyncInput = Omit<
80+
TransferToATAInstructionPlanInput,
81+
'destination'
82+
>;
83+
84+
export async function getTransferToATAInstructionPlanAsync(
85+
input: TransferToATAInstructionPlanAsyncInput,
86+
config?: TransferToATAInstructionPlanConfig
87+
): Promise<InstructionPlan> {
88+
const [ataAddress] = await findAssociatedTokenPda({
89+
owner: input.recipient,
90+
tokenProgram: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS,
91+
mint: input.mint,
92+
});
93+
return getTransferToATAInstructionPlan(
94+
{
95+
...input,
96+
destination: ataAddress,
97+
},
98+
config
99+
);
100+
}

clients/js/test/_setup.ts

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
SolanaRpcSubscriptionsApi,
1010
TransactionMessageWithBlockhashLifetime,
1111
TransactionMessageWithFeePayer,
12-
TransactionPlanExecutor,
12+
TransactionPlan,
13+
TransactionPlanResult,
1314
TransactionPlanner,
1415
TransactionSigner,
1516
airdropFactory,
@@ -32,22 +33,47 @@ import {
3233
} from '@solana/kit';
3334
import {
3435
TOKEN_PROGRAM_ADDRESS,
36+
findAssociatedTokenPda,
3537
getInitializeAccountInstruction,
3638
getInitializeMintInstruction,
3739
getMintSize,
40+
getMintToATAInstructionPlan,
3841
getMintToInstruction,
3942
getTokenSize,
4043
} from '../src';
4144

4245
type Client = {
4346
rpc: Rpc<SolanaRpcApi>;
4447
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
48+
sendTransactionPlan: (
49+
transactionPlan: TransactionPlan
50+
) => Promise<TransactionPlanResult>;
4551
};
4652

4753
export const createDefaultSolanaClient = (): Client => {
4854
const rpc = createSolanaRpc('http://127.0.0.1:8899');
4955
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
50-
return { rpc, rpcSubscriptions };
56+
57+
const sendAndConfirm = sendAndConfirmTransactionFactory({
58+
rpc,
59+
rpcSubscriptions,
60+
});
61+
const transactionPlanExecutor = createTransactionPlanExecutor({
62+
executeTransactionMessage: async (transactionMessage) => {
63+
const signedTransaction =
64+
await signTransactionMessageWithSigners(transactionMessage);
65+
assertIsSendableTransaction(signedTransaction);
66+
assertIsTransactionWithBlockhashLifetime(signedTransaction);
67+
await sendAndConfirm(signedTransaction, { commitment: 'confirmed' });
68+
return { transaction: signedTransaction };
69+
},
70+
});
71+
72+
const sendTransactionPlan = async (transactionPlan: TransactionPlan) => {
73+
return transactionPlanExecutor(transactionPlan);
74+
};
75+
76+
return { rpc, rpcSubscriptions, sendTransactionPlan };
5177
};
5278

5379
export const generateKeyPairSignerWithSol = async (
@@ -114,24 +140,6 @@ export const createDefaultTransactionPlanner = (
114140
});
115141
};
116142

117-
export const createDefaultTransactionPlanExecutor = (
118-
client: Client,
119-
commitment: Commitment = 'confirmed'
120-
): TransactionPlanExecutor => {
121-
return createTransactionPlanExecutor({
122-
executeTransactionMessage: async (transactionMessage) => {
123-
const signedTransaction =
124-
await signTransactionMessageWithSigners(transactionMessage);
125-
assertIsSendableTransaction(signedTransaction);
126-
assertIsTransactionWithBlockhashLifetime(signedTransaction);
127-
await sendAndConfirmTransactionFactory(client)(signedTransaction, {
128-
commitment,
129-
});
130-
return { transaction: signedTransaction };
131-
},
132-
});
133-
};
134-
135143
export const getBalance = async (client: Client, address: Address) =>
136144
(await client.rpc.getBalance(address, { commitment: 'confirmed' }).send())
137145
.value;
@@ -235,3 +243,37 @@ export const createTokenWithAmount = async (
235243

236244
return token.address;
237245
};
246+
247+
export const createTokenPdaWithAmount = async (
248+
client: Client,
249+
payer: TransactionSigner,
250+
mintAuthority: TransactionSigner,
251+
mint: Address,
252+
owner: Address,
253+
amount: bigint,
254+
decimals: number
255+
): Promise<Address> => {
256+
const [token] = await findAssociatedTokenPda({
257+
owner,
258+
mint,
259+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
260+
});
261+
262+
const transactionPlan = await createDefaultTransactionPlanner(
263+
client,
264+
payer
265+
)(
266+
getMintToATAInstructionPlan({
267+
payer,
268+
ata: token,
269+
owner,
270+
mint,
271+
mintAuthority,
272+
amount,
273+
decimals,
274+
})
275+
);
276+
277+
await client.sendTransactionPlan(transactionPlan);
278+
return token;
279+
};

clients/js/test/createMint.test.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { generateKeyPairSigner, Account, some, none } from '@solana/kit';
22
import test from 'ava';
3-
import { fetchMint, Mint, createMintInstructionPlan } from '../src';
3+
import { fetchMint, Mint, getCreateMintInstructionPlan } from '../src';
44
import {
55
createDefaultSolanaClient,
66
generateKeyPairSignerWithSol,
77
createDefaultTransactionPlanner,
8-
createDefaultTransactionPlanExecutor,
98
} from './_setup';
109

1110
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) => {
1514
const mint = await generateKeyPairSigner();
1615

1716
// When we create and initialize a mint account at this address.
18-
const instructionPlan = createMintInstructionPlan({
17+
const instructionPlan = getCreateMintInstructionPlan({
1918
payer: authority,
2019
newMint: mint,
2120
decimals: 2,
@@ -24,8 +23,7 @@ test('it creates and initializes a new mint account', async (t) => {
2423

2524
const transactionPlanner = createDefaultTransactionPlanner(client, authority);
2625
const transactionPlan = await transactionPlanner(instructionPlan);
27-
const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
28-
await transactionPlanExecutor(transactionPlan);
26+
await client.sendTransactionPlan(transactionPlan);
2927

3028
// Then we expect the mint account to exist and have the following data.
3129
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) => {
5250
]);
5351

5452
// When we create and initialize a mint account at this address.
55-
const instructionPlan = createMintInstructionPlan({
53+
const instructionPlan = getCreateMintInstructionPlan({
5654
payer: payer,
5755
newMint: mint,
5856
decimals: 2,
@@ -62,8 +60,7 @@ test('it creates a new mint account with a freeze authority', async (t) => {
6260

6361
const transactionPlanner = createDefaultTransactionPlanner(client, payer);
6462
const transactionPlan = await transactionPlanner(instructionPlan);
65-
const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
66-
await transactionPlanExecutor(transactionPlan);
63+
await client.sendTransactionPlan(transactionPlan);
6764

6865
// Then we expect the mint account to exist and have the following data.
6966
const mintAccount = await fetchMint(client.rpc, mint.address);

0 commit comments

Comments
 (0)