Skip to content

Commit 9d03092

Browse files
committed
Add an instruction plan to transfer to an ATA
1 parent 3e0522f commit 9d03092

File tree

4 files changed

+328
-0
lines changed

4 files changed

+328
-0
lines changed

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/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 transferToATAInstructionPlanAsync} 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 transferToATAInstructionPlan(
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 transferToATAInstructionPlanAsync(
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 transferToATAInstructionPlan(
94+
{
95+
...input,
96+
destination: ataAddress,
97+
},
98+
config
99+
);
100+
}

clients/js/test/_setup.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import {
3232
} from '@solana/kit';
3333
import {
3434
TOKEN_PROGRAM_ADDRESS,
35+
findAssociatedTokenPda,
36+
getCreateAssociatedTokenInstruction,
3537
getInitializeAccountInstruction,
3638
getInitializeMintInstruction,
3739
getMintSize,
@@ -235,3 +237,37 @@ export const createTokenWithAmount = async (
235237

236238
return token.address;
237239
};
240+
241+
export const createTokenPdaWithAmount = async (
242+
client: Client,
243+
payer: TransactionSigner,
244+
mintAuthority: TransactionSigner,
245+
mint: Address,
246+
owner: Address,
247+
amount: bigint
248+
): Promise<Address> => {
249+
const [transactionMessage, token] = await Promise.all([
250+
createDefaultTransaction(client, payer),
251+
findAssociatedTokenPda({
252+
owner,
253+
mint,
254+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
255+
}).then(([address]) => address),
256+
]);
257+
const instructions = [
258+
getCreateAssociatedTokenInstruction({
259+
payer,
260+
ata: token,
261+
owner,
262+
mint,
263+
}),
264+
getMintToInstruction({ mint, token, mintAuthority, amount }),
265+
];
266+
await pipe(
267+
transactionMessage,
268+
(tx) => appendTransactionMessageInstructions(instructions, tx),
269+
(tx) => signAndSendTransaction(client, tx)
270+
);
271+
272+
return token;
273+
};
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { generateKeyPairSigner } from '@solana/kit';
2+
import test from 'ava';
3+
import {
4+
Mint,
5+
TOKEN_PROGRAM_ADDRESS,
6+
Token,
7+
fetchMint,
8+
fetchToken,
9+
findAssociatedTokenPda,
10+
transferToATAInstructionPlan,
11+
transferToATAInstructionPlanAsync,
12+
} from '../src';
13+
import {
14+
createDefaultSolanaClient,
15+
createDefaultTransactionPlanExecutor,
16+
createDefaultTransactionPlanner,
17+
createMint,
18+
createToken,
19+
createTokenPdaWithAmount,
20+
createTokenWithAmount,
21+
generateKeyPairSignerWithSol,
22+
} from './_setup';
23+
24+
test('it transfers tokens from one account to a new ATA', async (t) => {
25+
// Given a mint account, one token account with 100 tokens, and a second owner.
26+
const client = createDefaultSolanaClient();
27+
const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([
28+
generateKeyPairSignerWithSol(client),
29+
generateKeyPairSigner(),
30+
generateKeyPairSigner(),
31+
generateKeyPairSigner(),
32+
]);
33+
const decimals = 2;
34+
const mint = await createMint(client, payer, mintAuthority.address, decimals);
35+
const tokenA = await createTokenWithAmount(
36+
client,
37+
payer,
38+
mintAuthority,
39+
mint,
40+
ownerA.address,
41+
100n
42+
);
43+
44+
const [tokenB] = await findAssociatedTokenPda({
45+
owner: ownerB.address,
46+
mint,
47+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
48+
});
49+
50+
// When owner A transfers 50 tokens to owner B.
51+
const instructionPlan = transferToATAInstructionPlan({
52+
payer,
53+
mint,
54+
source: tokenA,
55+
authority: ownerA,
56+
destination: tokenB,
57+
recipient: ownerB.address,
58+
amount: 50n,
59+
decimals,
60+
});
61+
62+
const transactionPlanner = createDefaultTransactionPlanner(client, payer);
63+
const transactionPlan = await transactionPlanner(instructionPlan);
64+
const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
65+
await transactionPlanExecutor(transactionPlan);
66+
67+
// Then we expect the mint and token accounts to have the following updated data.
68+
const [{ data: mintData }, { data: tokenDataA }, { data: tokenDataB }] =
69+
await Promise.all([
70+
fetchMint(client.rpc, mint),
71+
fetchToken(client.rpc, tokenA),
72+
fetchToken(client.rpc, tokenB),
73+
]);
74+
t.like(mintData, <Mint>{ supply: 100n });
75+
t.like(tokenDataA, <Token>{ amount: 50n });
76+
t.like(tokenDataB, <Token>{ amount: 50n });
77+
});
78+
79+
test('derives a new ATA and transfers tokens to it', async (t) => {
80+
// Given a mint account, one token account with 100 tokens, and a second owner.
81+
const client = createDefaultSolanaClient();
82+
const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([
83+
generateKeyPairSignerWithSol(client),
84+
generateKeyPairSigner(),
85+
generateKeyPairSigner(),
86+
generateKeyPairSigner(),
87+
]);
88+
const decimals = 2;
89+
const mint = await createMint(client, payer, mintAuthority.address, decimals);
90+
const tokenA = await createTokenWithAmount(
91+
client,
92+
payer,
93+
mintAuthority,
94+
mint,
95+
ownerA.address,
96+
100n
97+
);
98+
99+
// When owner A transfers 50 tokens to owner B.
100+
const instructionPlan = await transferToATAInstructionPlanAsync({
101+
payer,
102+
mint,
103+
source: tokenA,
104+
authority: ownerA,
105+
recipient: ownerB.address,
106+
amount: 50n,
107+
decimals,
108+
});
109+
110+
const transactionPlanner = createDefaultTransactionPlanner(client, payer);
111+
const transactionPlan = await transactionPlanner(instructionPlan);
112+
const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
113+
await transactionPlanExecutor(transactionPlan);
114+
115+
// Then we expect the mint and token accounts to have the following updated data.
116+
const [tokenB] = await findAssociatedTokenPda({
117+
owner: ownerB.address,
118+
mint,
119+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
120+
});
121+
122+
const [{ data: mintData }, { data: tokenDataA }, { data: tokenDataB }] =
123+
await Promise.all([
124+
fetchMint(client.rpc, mint),
125+
fetchToken(client.rpc, tokenA),
126+
fetchToken(client.rpc, tokenB),
127+
]);
128+
t.like(mintData, <Mint>{ supply: 100n });
129+
t.like(tokenDataA, <Token>{ amount: 50n });
130+
t.like(tokenDataB, <Token>{ amount: 50n });
131+
});
132+
133+
test('it transfers tokens from one account to an existing ATA', async (t) => {
134+
// Given a mint account and two token accounts.
135+
// One with 90 tokens and the other with 10 tokens.
136+
const client = createDefaultSolanaClient();
137+
const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([
138+
generateKeyPairSignerWithSol(client),
139+
generateKeyPairSigner(),
140+
generateKeyPairSigner(),
141+
generateKeyPairSigner(),
142+
]);
143+
const decimals = 2;
144+
const mint = await createMint(client, payer, mintAuthority.address, decimals);
145+
const [tokenA, tokenB] = await Promise.all([
146+
createTokenWithAmount(
147+
client,
148+
payer,
149+
mintAuthority,
150+
mint,
151+
ownerA.address,
152+
90n
153+
),
154+
createTokenPdaWithAmount(
155+
client,
156+
payer,
157+
mintAuthority,
158+
mint,
159+
ownerB.address,
160+
10n
161+
),
162+
]);
163+
164+
// When owner A transfers 50 tokens to owner B.
165+
const instructionPlan = transferToATAInstructionPlan({
166+
payer,
167+
mint,
168+
source: tokenA,
169+
authority: ownerA,
170+
destination: tokenB,
171+
recipient: ownerB.address,
172+
amount: 50n,
173+
decimals,
174+
});
175+
176+
const transactionPlanner = createDefaultTransactionPlanner(client, payer);
177+
const transactionPlan = await transactionPlanner(instructionPlan);
178+
const transactionPlanExecutor = createDefaultTransactionPlanExecutor(client);
179+
await transactionPlanExecutor(transactionPlan);
180+
181+
// Then we expect the mint and token accounts to have the following updated data.
182+
const [{ data: mintData }, { data: tokenDataA }, { data: tokenDataB }] =
183+
await Promise.all([
184+
fetchMint(client.rpc, mint),
185+
fetchToken(client.rpc, tokenA),
186+
fetchToken(client.rpc, tokenB),
187+
]);
188+
t.like(mintData, <Mint>{ supply: 100n });
189+
t.like(tokenDataA, <Token>{ amount: 40n });
190+
t.like(tokenDataB, <Token>{ amount: 60n });
191+
});

0 commit comments

Comments
 (0)