Skip to content

Commit 40e14ea

Browse files
authored
chore(js): Add plugin & instruction planner defaults (#128)
1 parent 4b06df8 commit 40e14ea

8 files changed

Lines changed: 279 additions & 32 deletions

File tree

clients/js/src/createMint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export type CreateMintInstructionPlanInput = {
2727
mintAccountLamports?: number;
2828
};
2929

30-
type CreateMintInstructionPlanConfig = {
30+
export type CreateMintInstructionPlanConfig = {
3131
systemProgram?: Address;
3232
tokenProgram?: Address;
3333
};

clients/js/src/mintToATA.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getMintToCheckedInstruction,
66
TOKEN_PROGRAM_ADDRESS,
77
} from './generated';
8+
import { MakeOptional } from './types';
89

910
export type MintToATAInstructionPlanInput = {
1011
/** Funding account (must be a system account). */
@@ -28,7 +29,7 @@ export type MintToATAInstructionPlanInput = {
2829
multiSigners?: Array<TransactionSigner>;
2930
};
3031

31-
type MintToATAInstructionPlanConfig = {
32+
export type MintToATAInstructionPlanConfig = {
3233
systemProgram?: Address;
3334
tokenProgram?: Address;
3435
associatedTokenProgram?: Address;
@@ -69,21 +70,25 @@ export function getMintToATAInstructionPlan(
6970
]);
7071
}
7172

72-
type MintToATAInstructionPlanAsyncInput = Omit<MintToATAInstructionPlanInput, 'ata'>;
73+
export type MintToATAInstructionPlanAsyncInput = MakeOptional<MintToATAInstructionPlanInput, 'ata'>;
7374

7475
export async function getMintToATAInstructionPlanAsync(
7576
input: MintToATAInstructionPlanAsyncInput,
7677
config?: MintToATAInstructionPlanConfig,
7778
): Promise<InstructionPlan> {
78-
const [ataAddress] = await findAssociatedTokenPda({
79-
owner: input.owner,
80-
tokenProgram: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS,
81-
mint: input.mint,
82-
});
79+
const tokenProgram = config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS;
80+
let ata = input.ata;
81+
if (!ata) {
82+
[ata] = await findAssociatedTokenPda({
83+
owner: input.owner,
84+
tokenProgram,
85+
mint: input.mint,
86+
});
87+
}
8388
return getMintToATAInstructionPlan(
8489
{
8590
...input,
86-
ata: ataAddress,
91+
ata,
8792
},
8893
config,
8994
);

clients/js/src/plugin.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,49 @@
11
import { ClientWithPayer, pipe } from '@solana/kit';
22
import { addSelfPlanAndSendFunctions, SelfPlanAndSendFunctions } from '@solana/kit/program-client-core';
33

4-
import { CreateMintInstructionPlanInput, getCreateMintInstructionPlan } from './createMint';
4+
import {
5+
CreateMintInstructionPlanConfig,
6+
CreateMintInstructionPlanInput,
7+
getCreateMintInstructionPlan,
8+
} from './createMint';
59
import {
610
TokenPlugin as GeneratedTokenPlugin,
711
TokenPluginInstructions as GeneratedTokenPluginInstructions,
812
TokenPluginRequirements as GeneratedTokenPluginRequirements,
913
tokenProgram as generatedTokenProgram,
1014
} from './generated';
11-
import { getMintToATAInstructionPlan, MintToATAInstructionPlanInput } from './mintToATA';
12-
import { getTransferToATAInstructionPlan, TransferToATAInstructionPlanInput } from './transferToATA';
15+
import {
16+
getMintToATAInstructionPlanAsync,
17+
MintToATAInstructionPlanAsyncInput,
18+
MintToATAInstructionPlanConfig,
19+
} from './mintToATA';
20+
import {
21+
getTransferToATAInstructionPlanAsync,
22+
TransferToATAInstructionPlanAsyncInput,
23+
TransferToATAInstructionPlanConfig,
24+
} from './transferToATA';
25+
import { MakeOptional } from './types';
1326

1427
export type TokenPluginRequirements = GeneratedTokenPluginRequirements & ClientWithPayer;
1528

1629
export type TokenPlugin = Omit<GeneratedTokenPlugin, 'instructions'> & { instructions: TokenPluginInstructions };
1730

1831
export type TokenPluginInstructions = GeneratedTokenPluginInstructions & {
32+
/** Create a new token mint. */
1933
createMint: (
2034
input: MakeOptional<CreateMintInstructionPlanInput, 'payer'>,
35+
config?: CreateMintInstructionPlanConfig,
2136
) => ReturnType<typeof getCreateMintInstructionPlan> & SelfPlanAndSendFunctions;
37+
/** Mint tokens to an owner's ATA (created if needed). */
2238
mintToATA: (
23-
input: MakeOptional<MintToATAInstructionPlanInput, 'payer'>,
24-
) => ReturnType<typeof getMintToATAInstructionPlan> & SelfPlanAndSendFunctions;
39+
input: MakeOptional<MintToATAInstructionPlanAsyncInput, 'payer'>,
40+
config?: MintToATAInstructionPlanConfig,
41+
) => ReturnType<typeof getMintToATAInstructionPlanAsync> & SelfPlanAndSendFunctions;
42+
/** Transfer tokens to a recipient's ATA (created if needed). */
2543
transferToATA: (
26-
input: MakeOptional<TransferToATAInstructionPlanInput, 'payer'>,
27-
) => ReturnType<typeof getTransferToATAInstructionPlan> & SelfPlanAndSendFunctions;
44+
input: MakeOptional<TransferToATAInstructionPlanAsyncInput, 'payer'>,
45+
config?: TransferToATAInstructionPlanConfig,
46+
) => ReturnType<typeof getTransferToATAInstructionPlanAsync> & SelfPlanAndSendFunctions;
2847
};
2948

3049
export function tokenProgram() {
@@ -35,25 +54,41 @@ export function tokenProgram() {
3554
...c.token,
3655
instructions: {
3756
...c.token.instructions,
38-
createMint: input =>
57+
createMint: (input, config) =>
3958
addSelfPlanAndSendFunctions(
4059
client,
41-
getCreateMintInstructionPlan({ ...input, payer: input.payer ?? client.payer }),
60+
getCreateMintInstructionPlan(
61+
{
62+
...input,
63+
payer: input.payer ?? client.payer,
64+
},
65+
config,
66+
),
4267
),
43-
mintToATA: input =>
68+
mintToATA: (input, config) =>
4469
addSelfPlanAndSendFunctions(
4570
client,
46-
getMintToATAInstructionPlan({ ...input, payer: input.payer ?? client.payer }),
71+
getMintToATAInstructionPlanAsync(
72+
{
73+
...input,
74+
payer: input.payer ?? client.payer,
75+
},
76+
config,
77+
),
4778
),
48-
transferToATA: input =>
79+
transferToATA: (input, config) =>
4980
addSelfPlanAndSendFunctions(
5081
client,
51-
getTransferToATAInstructionPlan({ ...input, payer: input.payer ?? client.payer }),
82+
getTransferToATAInstructionPlanAsync(
83+
{
84+
...input,
85+
payer: input.payer ?? client.payer,
86+
},
87+
config,
88+
),
5289
),
5390
},
5491
},
5592
}));
5693
};
5794
}
58-
59-
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

clients/js/src/transferToATA.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getTransferCheckedInstruction,
66
TOKEN_PROGRAM_ADDRESS,
77
} from './generated';
8+
import { MakeOptional } from './types';
89

910
export type TransferToATAInstructionPlanInput = {
1011
/** Funding account (must be a system account). */
@@ -30,7 +31,7 @@ export type TransferToATAInstructionPlanInput = {
3031
multiSigners?: Array<TransactionSigner>;
3132
};
3233

33-
type TransferToATAInstructionPlanConfig = {
34+
export type TransferToATAInstructionPlanConfig = {
3435
systemProgram?: Address;
3536
tokenProgram?: Address;
3637
associatedTokenProgram?: Address;
@@ -71,21 +72,43 @@ export function getTransferToATAInstructionPlan(
7172
]);
7273
}
7374

74-
type TransferToATAInstructionPlanAsyncInput = Omit<TransferToATAInstructionPlanInput, 'destination'>;
75+
export type TransferToATAInstructionPlanAsyncInput = MakeOptional<
76+
TransferToATAInstructionPlanInput,
77+
'source' | 'destination'
78+
>;
7579

7680
export async function getTransferToATAInstructionPlanAsync(
7781
input: TransferToATAInstructionPlanAsyncInput,
7882
config?: TransferToATAInstructionPlanConfig,
7983
): Promise<InstructionPlan> {
80-
const [ataAddress] = await findAssociatedTokenPda({
81-
owner: input.recipient,
82-
tokenProgram: config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS,
83-
mint: input.mint,
84-
});
84+
const tokenProgram = config?.tokenProgram ?? TOKEN_PROGRAM_ADDRESS;
85+
86+
let destination = input.destination;
87+
if (!destination) {
88+
[destination] = await findAssociatedTokenPda({
89+
owner: input.recipient,
90+
tokenProgram,
91+
mint: input.mint,
92+
});
93+
}
94+
95+
let source = input.source;
96+
if (!source) {
97+
const authorityAddress: Address =
98+
typeof input.authority === 'string' ? input.authority : input.authority.address;
99+
const [sourceAta] = await findAssociatedTokenPda({
100+
owner: authorityAddress,
101+
tokenProgram,
102+
mint: input.mint,
103+
});
104+
source = sourceAta;
105+
}
106+
85107
return getTransferToATAInstructionPlan(
86108
{
87109
...input,
88-
destination: ataAddress,
110+
source,
111+
destination,
89112
},
90113
config,
91114
);

clients/js/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

clients/js/test/mintToATA.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,49 @@ test('it derives a new associated token account with an initial balance', async
110110
});
111111
});
112112

113+
test('it uses an explicit ATA when provided to the async variant', async t => {
114+
// Given a mint account, its mint authority, a token owner and a pre-derived ATA.
115+
const client = createDefaultSolanaClient();
116+
const [payer, mintAuthority, owner] = await Promise.all([
117+
generateKeyPairSignerWithSol(client),
118+
generateKeyPairSigner(),
119+
generateKeyPairSigner(),
120+
]);
121+
const decimals = 2;
122+
const mint = await createMint(client, payer, mintAuthority.address, decimals);
123+
const [ata] = await findAssociatedTokenPda({
124+
mint,
125+
owner: owner.address,
126+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
127+
});
128+
129+
// When we mint via the async variant with an explicit ATA.
130+
const instructionPlan = await getMintToATAInstructionPlanAsync({
131+
payer,
132+
ata,
133+
mint,
134+
owner: owner.address,
135+
mintAuthority,
136+
amount: 1_000n,
137+
decimals,
138+
});
139+
140+
const transactionPlanner = createDefaultTransactionPlanner(client, payer);
141+
const transactionPlan = await transactionPlanner(instructionPlan);
142+
await client.sendTransactionPlan(transactionPlan);
143+
144+
// Then the explicit ATA should hold the minted tokens.
145+
t.like(await fetchToken(client.rpc, ata), <Account<Token>>{
146+
address: ata,
147+
data: {
148+
mint,
149+
owner: owner.address,
150+
amount: 1000n,
151+
state: AccountState.Initialized,
152+
},
153+
});
154+
});
155+
113156
test('it also mints to an existing associated token account', async t => {
114157
// Given a mint account, its mint authority, a token owner and the ATA.
115158
const client = createDefaultSolanaClient();

clients/js/test/plugin.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Account, generateKeyPairSigner } from '@solana/kit';
2+
import { createDefaultLocalhostRpcClient } from '@solana/kit-plugins';
3+
import test from 'ava';
4+
import { AccountState, fetchToken, findAssociatedTokenPda, Token, TOKEN_PROGRAM_ADDRESS, tokenProgram } from '../src';
5+
import {
6+
createMint,
7+
createTokenPdaWithAmount,
8+
generateKeyPairSignerWithSol,
9+
createDefaultSolanaClient,
10+
} from './_setup';
11+
12+
test('plugin mintToATA defaults payer and auto-derives ATA', async t => {
13+
// Given a mint account, its mint authority and a token owner.
14+
const client = await createDefaultLocalhostRpcClient().use(tokenProgram());
15+
const mintAuthority = await generateKeyPairSigner();
16+
const owner = await generateKeyPairSigner();
17+
const mint = await generateKeyPairSigner();
18+
19+
// And a mint created via the plugin.
20+
await client.token.instructions
21+
.createMint({ newMint: mint, decimals: 2, mintAuthority: mintAuthority.address })
22+
.sendTransaction();
23+
24+
// When we mint to the owner via the plugin (payer defaulted, ATA derived).
25+
await client.token.instructions
26+
.mintToATA({
27+
mint: mint.address,
28+
owner: owner.address,
29+
mintAuthority,
30+
amount: 1_000n,
31+
decimals: 2,
32+
})
33+
.sendTransaction();
34+
35+
// Then we expect the derived ATA to exist with the correct balance.
36+
const [ata] = await findAssociatedTokenPda({
37+
mint: mint.address,
38+
owner: owner.address,
39+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
40+
});
41+
42+
t.like(await fetchToken(client.rpc, ata), <Account<Token>>{
43+
address: ata,
44+
data: {
45+
mint: mint.address,
46+
owner: owner.address,
47+
amount: 1000n,
48+
state: AccountState.Initialized,
49+
},
50+
});
51+
});
52+
53+
test('plugin transferToATA defaults payer and auto-derives source + destination', async t => {
54+
// Given a mint account and ownerA's ATA with 100 tokens.
55+
const baseClient = createDefaultSolanaClient();
56+
const [payer, mintAuthority, ownerA, ownerB] = await Promise.all([
57+
generateKeyPairSignerWithSol(baseClient),
58+
generateKeyPairSigner(),
59+
generateKeyPairSigner(),
60+
generateKeyPairSigner(),
61+
]);
62+
const decimals = 2;
63+
const mint = await createMint(baseClient, payer, mintAuthority.address, decimals);
64+
await createTokenPdaWithAmount(baseClient, payer, mintAuthority, mint, ownerA.address, 100n, decimals);
65+
66+
// When ownerA transfers 50 tokens to ownerB via the plugin (payer defaulted, source + destination derived).
67+
const client = await createDefaultLocalhostRpcClient().use(tokenProgram());
68+
await client.token.instructions
69+
.transferToATA({
70+
mint,
71+
authority: ownerA,
72+
recipient: ownerB.address,
73+
amount: 50n,
74+
decimals,
75+
})
76+
.sendTransaction();
77+
78+
// Then we expect both ATAs to have the correct balances.
79+
const [sourceAta] = await findAssociatedTokenPda({
80+
owner: ownerA.address,
81+
mint,
82+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
83+
});
84+
const [destAta] = await findAssociatedTokenPda({
85+
owner: ownerB.address,
86+
mint,
87+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
88+
});
89+
90+
t.like(await fetchToken(client.rpc, sourceAta), <Account<Token>>{
91+
data: { amount: 50n },
92+
});
93+
t.like(await fetchToken(client.rpc, destAta), <Account<Token>>{
94+
data: { amount: 50n },
95+
});
96+
});

0 commit comments

Comments
 (0)