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

Commit de2e356

Browse files
author
Joe C
authored
token js: create new offchain helper
This is the final PR to round off the changes required to fix #6064. Previously, the offchain helpers for adding extra metas to instructions have been replaced with new ones in the SPL Transfer Hook interface and Token2022. This PR follows suit and adds a new helper to SPL Token JS. The new helper, `addExtraAccountMetasForExecute(..)`, mirrors the Rust helper in SPL Transfer Hook interface, requiring the parameters for an `ExecuteInstruction` to be passed into the function directly. This change also adds a public function for creating an `ExecuteInstruction`, in case developers wish to create such an instruction for directly sending instructions to their transfer hook program. These existing functions have been updated to use the new helper: - `createTransferCheckedWithTransferHookInstruction(..)` - `createTransferCheckedWithFeeAndTransferHookInstruction(..)` Closes #6064
1 parent e988e6f commit de2e356

File tree

2 files changed

+674
-183
lines changed

2 files changed

+674
-183
lines changed

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

Lines changed: 150 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMe
137137
}
138138

139139
/**
140+
* @deprecated Deprecated since v0.3.12. Please use {@link addExtraAccountMetasForExecute} instead.
141+
*
140142
* Add extra accounts needed for transfer hook to an instruction
141143
*
142144
* @param connection Connection to use
@@ -190,14 +192,120 @@ export async function addExtraAccountsToInstruction(
190192
return new TransactionInstruction({ keys: accountMetas, programId, data: instruction.data });
191193
}
192194

195+
/**
196+
* Construct an `ExecuteInstruction` for a transfer hook program, without the
197+
* additional accounts
198+
*
199+
* @param programId The program ID of the transfer hook program
200+
* @param source The source account
201+
* @param mint The mint account
202+
* @param destination The destination account
203+
* @param owner Owner of the source account
204+
* @param validateStatePubkey The validate state pubkey
205+
* @param amount The amount of tokens to transfer
206+
* @returns Instruction to add to a transaction
207+
*/
208+
export function createExecuteInstruction(
209+
programId: PublicKey,
210+
source: PublicKey,
211+
mint: PublicKey,
212+
destination: PublicKey,
213+
owner: PublicKey,
214+
validateStatePubkey: PublicKey,
215+
amount: bigint
216+
): TransactionInstruction {
217+
const keys = [source, mint, destination, owner, validateStatePubkey].map((pubkey) => ({
218+
pubkey,
219+
isSigner: false,
220+
isWritable: false,
221+
}));
222+
223+
const data = Buffer.alloc(16);
224+
data.set(Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]), 0); // `ExecuteInstruction` discriminator
225+
data.writeBigUInt64LE(BigInt(amount), 8);
226+
227+
return new TransactionInstruction({ keys, programId, data });
228+
}
229+
230+
/**
231+
* Adds all the extra accounts needed for a transfer hook to an instruction.
232+
*
233+
* Note this will modify the instruction passed in.
234+
*
235+
* @param connection Connection to use
236+
* @param instruction The instruction to add accounts to
237+
* @param programId Transfer hook program ID
238+
* @param source The source account
239+
* @param mint The mint account
240+
* @param destination The destination account
241+
* @param owner Owner of the source account
242+
* @param amount The amount of tokens to transfer
243+
* @param commitment Commitment to use
244+
*/
245+
export async function addExtraAccountMetasForExecute(
246+
connection: Connection,
247+
instruction: TransactionInstruction,
248+
programId: PublicKey,
249+
source: PublicKey,
250+
mint: PublicKey,
251+
destination: PublicKey,
252+
owner: PublicKey,
253+
amount: number | bigint,
254+
commitment?: Commitment
255+
) {
256+
const validateStatePubkey = getExtraAccountMetaAddress(mint, programId);
257+
const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment);
258+
if (validateStateAccount == null) {
259+
return instruction;
260+
}
261+
const validateStateData = getExtraAccountMetas(validateStateAccount);
262+
263+
// Check to make sure the provided keys are in the instruction
264+
if (![source, mint, destination, owner].every((key) => instruction.keys.some((meta) => meta.pubkey === key))) {
265+
throw new Error('Missing required account in instruction');
266+
}
267+
268+
const executeInstruction = createExecuteInstruction(
269+
programId,
270+
source,
271+
mint,
272+
destination,
273+
owner,
274+
validateStatePubkey,
275+
BigInt(amount)
276+
);
277+
278+
for (const extraAccountMeta of validateStateData) {
279+
executeInstruction.keys.push(
280+
deEscalateAccountMeta(
281+
await resolveExtraAccountMeta(
282+
connection,
283+
extraAccountMeta,
284+
executeInstruction.keys,
285+
executeInstruction.data,
286+
executeInstruction.programId
287+
),
288+
executeInstruction.keys
289+
)
290+
);
291+
}
292+
293+
// Add only the extra accounts resolved from the validation state
294+
instruction.keys.push(...executeInstruction.keys.slice(5));
295+
296+
// Add the transfer hook program ID and the validation state account
297+
instruction.keys.push({ pubkey: programId, isSigner: false, isWritable: false });
298+
instruction.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false });
299+
}
300+
193301
/**
194302
* Construct an transferChecked instruction with extra accounts for transfer hook
195303
*
196304
* @param connection Connection to use
197305
* @param source Source account
198306
* @param mint Mint to update
199307
* @param destination Destination account
200-
* @param authority The mint's transfer hook authority
308+
* @param owner Owner of the source account
201309
* @param amount The amount of tokens to transfer
202310
* @param decimals Number of decimals in transfer amount
203311
* @param multiSigners The signer account(s) for a multisig
@@ -211,33 +319,42 @@ export async function createTransferCheckedWithTransferHookInstruction(
211319
source: PublicKey,
212320
mint: PublicKey,
213321
destination: PublicKey,
214-
authority: PublicKey,
322+
owner: PublicKey,
215323
amount: bigint,
216324
decimals: number,
217325
multiSigners: (Signer | PublicKey)[] = [],
218326
commitment?: Commitment,
219327
programId = TOKEN_PROGRAM_ID
220328
) {
221-
const rawInstruction = createTransferCheckedInstruction(
329+
const instruction = createTransferCheckedInstruction(
222330
source,
223331
mint,
224332
destination,
225-
authority,
333+
owner,
226334
amount,
227335
decimals,
228336
multiSigners,
229337
programId
230338
);
231339

232-
const hydratedInstruction = await addExtraAccountsToInstruction(
233-
connection,
234-
rawInstruction,
235-
mint,
236-
commitment,
237-
programId
238-
);
340+
const mintInfo = await getMint(connection, mint, commitment, programId);
341+
const transferHook = getTransferHook(mintInfo);
342+
343+
if (transferHook) {
344+
await addExtraAccountMetasForExecute(
345+
connection,
346+
instruction,
347+
transferHook.programId,
348+
source,
349+
mint,
350+
destination,
351+
owner,
352+
amount,
353+
commitment
354+
);
355+
}
239356

240-
return hydratedInstruction;
357+
return instruction;
241358
}
242359

243360
/**
@@ -247,7 +364,7 @@ export async function createTransferCheckedWithTransferHookInstruction(
247364
* @param source Source account
248365
* @param mint Mint to update
249366
* @param destination Destination account
250-
* @param authority The mint's transfer hook authority
367+
* @param owner Owner of the source account
251368
* @param amount The amount of tokens to transfer
252369
* @param decimals Number of decimals in transfer amount
253370
* @param fee The calculated fee for the transfer fee extension
@@ -262,33 +379,42 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction(
262379
source: PublicKey,
263380
mint: PublicKey,
264381
destination: PublicKey,
265-
authority: PublicKey,
382+
owner: PublicKey,
266383
amount: bigint,
267384
decimals: number,
268385
fee: bigint,
269386
multiSigners: (Signer | PublicKey)[] = [],
270387
commitment?: Commitment,
271388
programId = TOKEN_PROGRAM_ID
272389
) {
273-
const rawInstruction = createTransferCheckedWithFeeInstruction(
390+
const instruction = createTransferCheckedWithFeeInstruction(
274391
source,
275392
mint,
276393
destination,
277-
authority,
394+
owner,
278395
amount,
279396
decimals,
280397
fee,
281398
multiSigners,
282399
programId
283400
);
284401

285-
const hydratedInstruction = await addExtraAccountsToInstruction(
286-
connection,
287-
rawInstruction,
288-
mint,
289-
commitment,
290-
programId
291-
);
402+
const mintInfo = await getMint(connection, mint, commitment, programId);
403+
const transferHook = getTransferHook(mintInfo);
404+
405+
if (transferHook) {
406+
await addExtraAccountMetasForExecute(
407+
connection,
408+
instruction,
409+
transferHook.programId,
410+
source,
411+
mint,
412+
destination,
413+
owner,
414+
amount,
415+
commitment
416+
);
417+
}
292418

293-
return hydratedInstruction;
419+
return instruction;
294420
}

0 commit comments

Comments
 (0)