Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

token js: create new offchain helper #6108

Merged
merged 5 commits into from
Jan 11, 2024
Merged
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
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