Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

token js: repair offchain extra metas helper #6090

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 91 additions & 55 deletions token/js/src/extensions/transferHook/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { publicKey } from '@solana/buffer-layout-utils';
import { createTransferCheckedInstruction } from '../../instructions/transferChecked.js';
import { createTransferCheckedWithFeeInstruction } from '../transferFee/instructions.js';
import { getMint } from '../../state/mint.js';
import { getExtraAccountMetaAddress, getExtraAccountMetas, getTransferHook, resolveExtraAccountMeta } from './state.js';
import {
getExtraAccountMetaAddress,
getExtraAccountMetaList,
getTransferHook,
resolveExtraAccountMeta,
} from './state.js';

export enum TransferHookInstruction {
Initialize = 0,
Expand Down Expand Up @@ -136,58 +141,79 @@ function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMe
return accountMeta;
}

/**
* Add extra accounts needed for transfer hook to an instruction
*
* @param connection Connection to use
* @param instruction The transferChecked instruction to add accounts to
* @param commitment Commitment to use
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export async function addExtraAccountsToInstruction(
function createExecuteInstruction(
transferHookProgramId: PublicKey,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: PublicKey,
validateStatePubkey: PublicKey,
amount: number | bigint
): TransactionInstruction {
const keys = [source, mint, destination, authority, validateStatePubkey].map((pubkey) => ({
pubkey,
isSigner: false,
isWritable: false,
}));

const programId = transferHookProgramId;

const data = Buffer.alloc(16);
data.set(Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]), 0); // `Execute` discriminator
data.writeBigUInt64LE(BigInt(amount), 8);

return new TransactionInstruction({ keys, programId, data });
}

async function resolveExtraAccountMetasForTransfer(
connection: Connection,
instruction: TransactionInstruction,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: PublicKey,
amount: bigint,
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
): Promise<TransactionInstruction> {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}

const mintInfo = await getMint(connection, mint, commitment, programId);
tokenProgramId = TOKEN_PROGRAM_ID
) {
const mintInfo = await getMint(connection, mint, commitment, tokenProgramId);
const transferHook = getTransferHook(mintInfo);
if (transferHook == null) {
return instruction;
return [];
}

const extraAccountsAccount = getExtraAccountMetaAddress(mint, transferHook.programId);
const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, commitment);
if (extraAccountsInfo == null) {
return instruction;
const validateStatePubkey = getExtraAccountMetaAddress(mint, transferHook.programId);
const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment);
if (validateStateAccount == null) {
return [];
}

const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo);

const accountMetas = instruction.keys;
// Create an `Execute` instruction, then resolve the extra account metas
// as configured in the validation account data.
const executeIx = createExecuteInstruction(
transferHook.programId,
source,
mint,
destination,
authority,
validateStatePubkey,
amount
);

for (const extraAccountMeta of extraAccountMetas) {
for (const extraAccountMeta of getExtraAccountMetaList(validateStateAccount)) {
const accountMetaUnchecked = await resolveExtraAccountMeta(
connection,
extraAccountMeta,
accountMetas,
instruction.data,
transferHook.programId
executeIx.keys,
executeIx.data,
executeIx.programId
);
const accountMeta = deEscalateAccountMeta(accountMetaUnchecked, accountMetas);
accountMetas.push(accountMeta);
const accountMeta = deEscalateAccountMeta(accountMetaUnchecked, executeIx.keys);
executeIx.keys.push(accountMeta);
}
accountMetas.push({ pubkey: transferHook.programId, isSigner: false, isWritable: false });
accountMetas.push({ pubkey: extraAccountsAccount, isSigner: false, isWritable: false });
executeIx.keys.push({ pubkey: transferHook.programId, isSigner: false, isWritable: false });
executeIx.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false });

return new TransactionInstruction({ keys: accountMetas, programId, data: instruction.data });
return executeIx.keys.slice(5);
}

/**
Expand All @@ -206,7 +232,7 @@ export async function addExtraAccountsToInstruction(
*
* @return Instruction to add to a transaction
*/
export async function createTransferCheckedWithTransferHookInstruction(
export async function createTransferCheckedInstructionWithExtraMetas(
connection: Connection,
source: PublicKey,
mint: PublicKey,
Expand All @@ -218,7 +244,7 @@ export async function createTransferCheckedWithTransferHookInstruction(
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const rawInstruction = createTransferCheckedInstruction(
const transferCheckedIx = createTransferCheckedInstruction(
source,
mint,
destination,
Expand All @@ -229,15 +255,20 @@ export async function createTransferCheckedWithTransferHookInstruction(
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
connection,
rawInstruction,
mint,
commitment,
programId
transferCheckedIx.keys.push(
...(await resolveExtraAccountMetasForTransfer(
connection,
source,
mint,
destination,
authority,
amount,
commitment,
programId
))
);

return hydratedInstruction;
return transferCheckedIx;
}

/**
Expand All @@ -257,7 +288,7 @@ export async function createTransferCheckedWithTransferHookInstruction(
*
* @return Instruction to add to a transaction
*/
export async function createTransferCheckedWithFeeAndTransferHookInstruction(
export async function createTransferCheckedWithFeeInstructionWithExtraMetas(
connection: Connection,
source: PublicKey,
mint: PublicKey,
Expand All @@ -270,7 +301,7 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction(
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const rawInstruction = createTransferCheckedWithFeeInstruction(
const transferCheckedWithFeeIx = createTransferCheckedWithFeeInstruction(
source,
mint,
destination,
Expand All @@ -282,13 +313,18 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction(
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
connection,
rawInstruction,
mint,
commitment,
programId
transferCheckedWithFeeIx.keys.push(
...(await resolveExtraAccountMetasForTransfer(
connection,
source,
mint,
destination,
authority,
amount,
commitment,
programId
))
);

return hydratedInstruction;
return transferCheckedWithFeeIx;
}
2 changes: 1 addition & 1 deletion token/js/src/extensions/transferHook/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const ExtraAccountMetaAccountDataLayout = struct<ExtraAccountMetaAccountD
]);

/** Unpack an extra account metas account and parse the data into a list of ExtraAccountMetas */
export function getExtraAccountMetas(account: AccountInfo<Buffer>): ExtraAccountMeta[] {
export function getExtraAccountMetaList(account: AccountInfo<Buffer>): ExtraAccountMeta[] {
const extraAccountsList = ExtraAccountMetaAccountDataLayout.decode(account.data).extraAccountsList;
return extraAccountsList.extraAccounts.slice(0, extraAccountsList.count);
}
Expand Down
Loading
Loading