Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit c79c727

Browse files
authored
token-js: added extra account resolution for transfer hook extension (#5112)
* Added extra account resolution for transfer hook extension * A few of tweaks and improvements * A few minor improvements and tweaks * Ported over fix for tlv parsing of the ExtraMetaAccount account * Split transfer fee with hook up into instruction creators and actions * Minor improvements and tweaks
1 parent fd67d99 commit c79c727

File tree

8 files changed

+643
-8
lines changed

8 files changed

+643
-8
lines changed

token/js/examples/transferHook.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
getMintLen,
1717
TOKEN_2022_PROGRAM_ID,
1818
updateTransferHook,
19+
transferCheckedWithHook,
20+
getAssociatedTokenAddressSync,
21+
ASSOCIATED_TOKEN_PROGRAM_ID,
1922
} from '../src';
2023

2124
(async () => {
@@ -25,6 +28,9 @@ import {
2528
const mintKeypair = Keypair.generate();
2629
const mint = mintKeypair.publicKey;
2730

31+
const sender = Keypair.generate();
32+
const recipient = Keypair.generate();
33+
2834
const extensions = [ExtensionType.TransferHook];
2935
const mintLen = getMintLen(extensions);
3036
const decimals = 9;
@@ -60,4 +66,33 @@ import {
6066
undefined,
6167
TOKEN_2022_PROGRAM_ID
6268
);
69+
70+
const senderAta = getAssociatedTokenAddressSync(
71+
mint,
72+
sender.publicKey,
73+
false,
74+
TOKEN_2022_PROGRAM_ID,
75+
ASSOCIATED_TOKEN_PROGRAM_ID
76+
);
77+
const recipientAta = getAssociatedTokenAddressSync(
78+
mint,
79+
recipient.publicKey,
80+
false,
81+
TOKEN_2022_PROGRAM_ID,
82+
ASSOCIATED_TOKEN_PROGRAM_ID
83+
);
84+
85+
await transferCheckedWithHook(
86+
connection,
87+
payer,
88+
senderAta,
89+
mint,
90+
recipientAta,
91+
sender,
92+
BigInt(1000000000),
93+
9,
94+
[],
95+
undefined,
96+
TOKEN_2022_PROGRAM_ID
97+
);
6398
})();

token/js/src/errors.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export class TokenInvalidAccountError extends TokenError {
1515
name = 'TokenInvalidAccountError';
1616
}
1717

18+
/** Thrown if a program state account does not contain valid data */
19+
export class TokenInvalidAccountDataError extends TokenError {
20+
name = 'TokenInvalidAccountDataError';
21+
}
22+
1823
/** Thrown if a program state account is not owned by the expected token program */
1924
export class TokenInvalidAccountOwnerError extends TokenError {
2025
name = 'TokenInvalidAccountOwnerError';
@@ -64,3 +69,13 @@ export class TokenInvalidInstructionTypeError extends TokenError {
6469
export class TokenUnsupportedInstructionError extends TokenError {
6570
name = 'TokenUnsupportedInstructionError';
6671
}
72+
73+
/** Thrown if the transfer hook extra accounts contains an invalid account index */
74+
export class TokenTransferHookAccountNotFound extends TokenError {
75+
name = 'TokenTransferHookAccountNotFound';
76+
}
77+
78+
/** Thrown if the transfer hook extra accounts contains an invalid seed */
79+
export class TokenTransferHookInvalidSeed extends TokenError {
80+
name = 'TokenTransferHookInvalidSeed';
81+
}

token/js/src/extensions/transferHook/actions.ts

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js';
1+
import type { ConfirmOptions, Connection, Signer, TransactionSignature } from '@solana/web3.js';
2+
import type { PublicKey } from '@solana/web3.js';
23
import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js';
34
import { getSigners } from '../../actions/internal.js';
4-
import { TOKEN_2022_PROGRAM_ID } from '../../constants.js';
5-
import { createInitializeTransferHookInstruction, createUpdateTransferHookInstruction } from './instructions.js';
5+
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../../constants.js';
6+
import {
7+
createInitializeTransferHookInstruction,
8+
createTransferCheckedWithFeeAndTransferHookInstruction,
9+
createTransferCheckedWithTransferHookInstruction,
10+
createUpdateTransferHookInstruction,
11+
} from './instructions.js';
612

713
/**
814
* Initialize a transfer hook on a mint
@@ -65,3 +71,106 @@ export async function updateTransferHook(
6571

6672
return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
6773
}
74+
75+
/**
76+
* Transfer tokens from one account to another, asserting the token mint, and decimals
77+
*
78+
* @param connection Connection to use
79+
* @param payer Payer of the transaction fees
80+
* @param source Source account
81+
* @param mint Mint for the account
82+
* @param destination Destination account
83+
* @param authority Authority of the source account
84+
* @param amount Number of tokens to transfer
85+
* @param decimals Number of decimals in transfer amount
86+
* @param multiSigners Signing accounts if `owner` is a multisig
87+
* @param confirmOptions Options for confirming the transaction
88+
* @param programId SPL Token program account
89+
*
90+
* @return Signature of the confirmed transaction
91+
*/
92+
export async function transferCheckedWithTransferHook(
93+
connection: Connection,
94+
payer: Signer,
95+
source: PublicKey,
96+
mint: PublicKey,
97+
destination: PublicKey,
98+
authority: Signer | PublicKey,
99+
amount: bigint,
100+
decimals: number,
101+
multiSigners: Signer[] = [],
102+
confirmOptions?: ConfirmOptions,
103+
programId = TOKEN_PROGRAM_ID
104+
): Promise<TransactionSignature> {
105+
const [authorityPublicKey, signers] = getSigners(authority, multiSigners);
106+
107+
const transaction = new Transaction().add(
108+
await createTransferCheckedWithTransferHookInstruction(
109+
connection,
110+
source,
111+
mint,
112+
destination,
113+
authorityPublicKey,
114+
amount,
115+
decimals,
116+
signers,
117+
confirmOptions?.commitment,
118+
programId
119+
)
120+
);
121+
122+
return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
123+
}
124+
125+
/**
126+
* Transfer tokens from one account to another, asserting the transfer fee, token mint, and decimals
127+
*
128+
* @param connection Connection to use
129+
* @param payer Payer of the transaction fees
130+
* @param source Source account
131+
* @param mint Mint for the account
132+
* @param destination Destination account
133+
* @param authority Authority of the source account
134+
* @param amount Number of tokens to transfer
135+
* @param decimals Number of decimals in transfer amount
136+
* @param fee The calculated fee for the transfer fee extension
137+
* @param multiSigners Signing accounts if `owner` is a multisig
138+
* @param confirmOptions Options for confirming the transaction
139+
* @param programId SPL Token program account
140+
*
141+
* @return Signature of the confirmed transaction
142+
*/
143+
export async function transferCheckedWithFeeAndTransferHook(
144+
connection: Connection,
145+
payer: Signer,
146+
source: PublicKey,
147+
mint: PublicKey,
148+
destination: PublicKey,
149+
authority: Signer | PublicKey,
150+
amount: bigint,
151+
decimals: number,
152+
fee: bigint,
153+
multiSigners: Signer[] = [],
154+
confirmOptions?: ConfirmOptions,
155+
programId = TOKEN_PROGRAM_ID
156+
): Promise<TransactionSignature> {
157+
const [authorityPublicKey, signers] = getSigners(authority, multiSigners);
158+
159+
const transaction = new Transaction().add(
160+
await createTransferCheckedWithFeeAndTransferHookInstruction(
161+
connection,
162+
source,
163+
mint,
164+
destination,
165+
authorityPublicKey,
166+
amount,
167+
decimals,
168+
fee,
169+
signers,
170+
confirmOptions?.commitment,
171+
programId
172+
)
173+
);
174+
175+
return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
176+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './actions.js';
22
export * from './instructions.js';
3+
export * from './seeds.js';
34
export * from './state.js';

token/js/src/extensions/transferHook/instructions.ts

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { struct, u8 } from '@solana/buffer-layout';
2-
import type { PublicKey, Signer } from '@solana/web3.js';
2+
import type { Commitment, Connection, PublicKey, Signer } from '@solana/web3.js';
33
import { TransactionInstruction } from '@solana/web3.js';
4-
import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js';
4+
import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../../constants.js';
55
import { TokenUnsupportedInstructionError } from '../../errors.js';
66
import { addSigners } from '../../instructions/internal.js';
77
import { TokenInstruction } from '../../instructions/types.js';
88
import { publicKey } from '@solana/buffer-layout-utils';
9+
import { createTransferCheckedInstruction } from '../../instructions/transferChecked.js';
10+
import { createTransferCheckedWithFeeInstruction } from '../transferFee/instructions.js';
11+
import { getMint } from '../../state/mint.js';
12+
import { getExtraAccountMetaAccount, getExtraAccountMetas, getTransferHook, resolveExtraAccountMeta } from './state.js';
913

1014
export enum TransferHookInstruction {
1115
Initialize = 0,
@@ -112,3 +116,158 @@ export function createUpdateTransferHookInstruction(
112116

113117
return new TransactionInstruction({ keys, programId, data });
114118
}
119+
120+
/**
121+
* Add extra accounts needed for transfer hook to an instruction
122+
*
123+
* @param connection Connection to use
124+
* @param instruction The transferChecked instruction to add accounts to
125+
* @param commitment Commitment to use
126+
* @param programId SPL Token program account
127+
*
128+
* @return Instruction to add to a transaction
129+
*/
130+
export async function addExtraAccountsToInstruction(
131+
connection: Connection,
132+
instruction: TransactionInstruction,
133+
mint: PublicKey,
134+
commitment?: Commitment,
135+
programId = TOKEN_PROGRAM_ID
136+
): Promise<TransactionInstruction> {
137+
if (!programSupportsExtensions(programId)) {
138+
throw new TokenUnsupportedInstructionError();
139+
}
140+
141+
const mintInfo = await getMint(connection, mint, commitment, programId);
142+
const transferHook = getTransferHook(mintInfo);
143+
if (transferHook == null) {
144+
return instruction;
145+
}
146+
147+
const extraAccountsAccount = getExtraAccountMetaAccount(transferHook.programId, mint);
148+
const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, commitment);
149+
if (extraAccountsInfo == null) {
150+
return instruction;
151+
}
152+
153+
const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo);
154+
155+
const accountMetas = instruction.keys;
156+
accountMetas.push({ pubkey: extraAccountsAccount, isSigner: false, isWritable: false });
157+
158+
for (const extraAccountMeta of extraAccountMetas) {
159+
const accountMeta = resolveExtraAccountMeta(
160+
extraAccountMeta,
161+
accountMetas,
162+
instruction.data,
163+
transferHook.programId
164+
);
165+
accountMetas.push(accountMeta);
166+
}
167+
accountMetas.push({ pubkey: transferHook.programId, isSigner: false, isWritable: false });
168+
169+
return new TransactionInstruction({ keys: accountMetas, programId, data: instruction.data });
170+
}
171+
172+
/**
173+
* Construct an transferChecked instruction with extra accounts for transfer hook
174+
*
175+
* @param connection Connection to use
176+
* @param source Source account
177+
* @param mint Mint to update
178+
* @param destination Destination account
179+
* @param authority The mint's transfer hook authority
180+
* @param amount The amount of tokens to transfer
181+
* @param decimals Number of decimals in transfer amount
182+
* @param multiSigners The signer account(s) for a multisig
183+
* @param commitment Commitment to use
184+
* @param programId SPL Token program account
185+
*
186+
* @return Instruction to add to a transaction
187+
*/
188+
export async function createTransferCheckedWithTransferHookInstruction(
189+
connection: Connection,
190+
source: PublicKey,
191+
mint: PublicKey,
192+
destination: PublicKey,
193+
authority: PublicKey,
194+
amount: bigint,
195+
decimals: number,
196+
multiSigners: (Signer | PublicKey)[] = [],
197+
commitment?: Commitment,
198+
programId = TOKEN_PROGRAM_ID
199+
) {
200+
const rawInstruction = createTransferCheckedInstruction(
201+
source,
202+
mint,
203+
destination,
204+
authority,
205+
amount,
206+
decimals,
207+
multiSigners,
208+
programId
209+
);
210+
211+
const hydratedInstruction = await addExtraAccountsToInstruction(
212+
connection,
213+
rawInstruction,
214+
mint,
215+
commitment,
216+
programId
217+
);
218+
219+
return hydratedInstruction;
220+
}
221+
222+
/**
223+
* Construct an transferChecked instruction with extra accounts for transfer hook
224+
*
225+
* @param connection Connection to use
226+
* @param source Source account
227+
* @param mint Mint to update
228+
* @param destination Destination account
229+
* @param authority The mint's transfer hook authority
230+
* @param amount The amount of tokens to transfer
231+
* @param decimals Number of decimals in transfer amount
232+
* @param fee The calculated fee for the transfer fee extension
233+
* @param multiSigners The signer account(s) for a multisig
234+
* @param commitment Commitment to use
235+
* @param programId SPL Token program account
236+
*
237+
* @return Instruction to add to a transaction
238+
*/
239+
export async function createTransferCheckedWithFeeAndTransferHookInstruction(
240+
connection: Connection,
241+
source: PublicKey,
242+
mint: PublicKey,
243+
destination: PublicKey,
244+
authority: PublicKey,
245+
amount: bigint,
246+
decimals: number,
247+
fee: bigint,
248+
multiSigners: (Signer | PublicKey)[] = [],
249+
commitment?: Commitment,
250+
programId = TOKEN_PROGRAM_ID
251+
) {
252+
const rawInstruction = createTransferCheckedWithFeeInstruction(
253+
source,
254+
mint,
255+
destination,
256+
authority,
257+
amount,
258+
decimals,
259+
fee,
260+
multiSigners,
261+
programId
262+
);
263+
264+
const hydratedInstruction = await addExtraAccountsToInstruction(
265+
connection,
266+
rawInstruction,
267+
mint,
268+
commitment,
269+
programId
270+
);
271+
272+
return hydratedInstruction;
273+
}

0 commit comments

Comments
 (0)