Skip to content

Commit

Permalink
token js: create new offchain helper
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Joe C authored Jan 11, 2024
1 parent e988e6f commit de2e356
Show file tree
Hide file tree
Showing 2 changed files with 674 additions and 183 deletions.
174 changes: 150 additions & 24 deletions token/js/src/extensions/transferHook/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMe
}

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

/**
* Construct an `ExecuteInstruction` for a transfer hook program, without the
* additional accounts
*
* @param programId The program ID of the transfer hook program
* @param source The source account
* @param mint The mint account
* @param destination The destination account
* @param owner Owner of the source account
* @param validateStatePubkey The validate state pubkey
* @param amount The amount of tokens to transfer
* @returns Instruction to add to a transaction
*/
export function createExecuteInstruction(
programId: PublicKey,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
owner: PublicKey,
validateStatePubkey: PublicKey,
amount: bigint
): TransactionInstruction {
const keys = [source, mint, destination, owner, validateStatePubkey].map((pubkey) => ({
pubkey,
isSigner: false,
isWritable: false,
}));

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

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

/**
* Adds all the extra accounts needed for a transfer hook to an instruction.
*
* Note this will modify the instruction passed in.
*
* @param connection Connection to use
* @param instruction The instruction to add accounts to
* @param programId Transfer hook program ID
* @param source The source account
* @param mint The mint account
* @param destination The destination account
* @param owner Owner of the source account
* @param amount The amount of tokens to transfer
* @param commitment Commitment to use
*/
export async function addExtraAccountMetasForExecute(
connection: Connection,
instruction: TransactionInstruction,
programId: PublicKey,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
owner: PublicKey,
amount: number | bigint,
commitment?: Commitment
) {
const validateStatePubkey = getExtraAccountMetaAddress(mint, programId);
const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment);
if (validateStateAccount == null) {
return instruction;
}
const validateStateData = getExtraAccountMetas(validateStateAccount);

// Check to make sure the provided keys are in the instruction
if (![source, mint, destination, owner].every((key) => instruction.keys.some((meta) => meta.pubkey === key))) {
throw new Error('Missing required account in instruction');
}

const executeInstruction = createExecuteInstruction(
programId,
source,
mint,
destination,
owner,
validateStatePubkey,
BigInt(amount)
);

for (const extraAccountMeta of validateStateData) {
executeInstruction.keys.push(
deEscalateAccountMeta(
await resolveExtraAccountMeta(
connection,
extraAccountMeta,
executeInstruction.keys,
executeInstruction.data,
executeInstruction.programId
),
executeInstruction.keys
)
);
}

// Add only the extra accounts resolved from the validation state
instruction.keys.push(...executeInstruction.keys.slice(5));

// Add the transfer hook program ID and the validation state account
instruction.keys.push({ pubkey: programId, isSigner: false, isWritable: false });
instruction.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false });
}

/**
* Construct an transferChecked instruction with extra accounts for transfer hook
*
* @param connection Connection to use
* @param source Source account
* @param mint Mint to update
* @param destination Destination account
* @param authority The mint's transfer hook authority
* @param owner Owner of the source account
* @param amount The amount of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param multiSigners The signer account(s) for a multisig
Expand All @@ -211,33 +319,42 @@ export async function createTransferCheckedWithTransferHookInstruction(
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: PublicKey,
owner: PublicKey,
amount: bigint,
decimals: number,
multiSigners: (Signer | PublicKey)[] = [],
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const rawInstruction = createTransferCheckedInstruction(
const instruction = createTransferCheckedInstruction(
source,
mint,
destination,
authority,
owner,
amount,
decimals,
multiSigners,
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
connection,
rawInstruction,
mint,
commitment,
programId
);
const mintInfo = await getMint(connection, mint, commitment, programId);
const transferHook = getTransferHook(mintInfo);

if (transferHook) {
await addExtraAccountMetasForExecute(
connection,
instruction,
transferHook.programId,
source,
mint,
destination,
owner,
amount,
commitment
);
}

return hydratedInstruction;
return instruction;
}

/**
Expand All @@ -247,7 +364,7 @@ export async function createTransferCheckedWithTransferHookInstruction(
* @param source Source account
* @param mint Mint to update
* @param destination Destination account
* @param authority The mint's transfer hook authority
* @param owner Owner of the source account
* @param amount The amount of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param fee The calculated fee for the transfer fee extension
Expand All @@ -262,33 +379,42 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction(
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: PublicKey,
owner: PublicKey,
amount: bigint,
decimals: number,
fee: bigint,
multiSigners: (Signer | PublicKey)[] = [],
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const rawInstruction = createTransferCheckedWithFeeInstruction(
const instruction = createTransferCheckedWithFeeInstruction(
source,
mint,
destination,
authority,
owner,
amount,
decimals,
fee,
multiSigners,
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
connection,
rawInstruction,
mint,
commitment,
programId
);
const mintInfo = await getMint(connection, mint, commitment, programId);
const transferHook = getTransferHook(mintInfo);

if (transferHook) {
await addExtraAccountMetasForExecute(
connection,
instruction,
transferHook.programId,
source,
mint,
destination,
owner,
amount,
commitment
);
}

return hydratedInstruction;
return instruction;
}
Loading

0 comments on commit de2e356

Please sign in to comment.