Skip to content

Commit

Permalink
token js: modify offchain extra metas helper
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec committed Jan 8, 2024
1 parent 6b915a7 commit 0b70d77
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 26 deletions.
102 changes: 80 additions & 22 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,109 @@ function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMe
return accountMeta;
}

function createExecuteInstructionFromTransfer(
transferInstruction: TransactionInstruction,
validateStatePubkey: PublicKey,
transferHookProgramId: PublicKey,
amount: bigint
): TransactionInstruction {
if (transferInstruction.keys.length < 4) {
throw new Error('Not a valid transfer instruction');
}

const keys = [
transferInstruction.keys[0].pubkey,
transferInstruction.keys[1].pubkey,
transferInstruction.keys[2].pubkey,
transferInstruction.keys[3].pubkey,
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(amount, 8);

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

/**
* Add extra accounts needed for transfer hook to an instruction
*
* Note that this offchain helper will build a new `Execute` instruction,
* resolve the extra account metas, and then add them to the transfer
* instruction. This is because the extra account metas are configured
* specifically for the `Execute` instruction, which requires five accounts
* (source, mint, destination, authority, and validation state), wheras the
* transfer instruction only requires four (source, mint, destination, and
* authority) in addition to `n` number of multisig authorities.
*
* @param connection Connection to use
* @param instruction The transferChecked instruction to add accounts to
* @param mint Mint being transferred
* @param amount Amount being transferred
* @param commitment Commitment to use
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export async function addExtraAccountsToInstruction(
export async function addExtraAccountsToTransferInstruction(
connection: Connection,
instruction: TransactionInstruction,
mint: PublicKey,
amount: bigint,
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
tokenProgramId = TOKEN_PROGRAM_ID
): Promise<TransactionInstruction> {
if (!programSupportsExtensions(programId)) {
if (!programSupportsExtensions(tokenProgramId)) {
throw new TokenUnsupportedInstructionError();
}

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

const extraAccountsAccount = getExtraAccountMetaAddress(mint, transferHook.programId);
const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, commitment);
if (extraAccountsInfo == null) {
// Convert the transfer instruction into an `Execute` instruction,
// then resolve the extra account metas as configured in the validation
// account data, then finally add the extra account metas to the original
// transfer instruction.
const validateStatePubkey = getExtraAccountMetaAddress(mint, transferHook.programId);
const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment);
if (validateStateAccount == null) {
return instruction;
}

const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo);

const accountMetas = instruction.keys;
const executeIx = createExecuteInstructionFromTransfer(
instruction,
validateStatePubkey,
transferHook.programId,
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 });

instruction.keys.push(...executeIx.keys.slice(5));

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

/**
Expand Down Expand Up @@ -229,10 +285,11 @@ export async function createTransferCheckedWithTransferHookInstruction(
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
const hydratedInstruction = await addExtraAccountsToTransferInstruction(
connection,
rawInstruction,
mint,
amount,
commitment,
programId
);
Expand Down Expand Up @@ -282,10 +339,11 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction(
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
const hydratedInstruction = await addExtraAccountsToTransferInstruction(
connection,
rawInstruction,
mint,
amount,
commitment,
programId
);
Expand Down
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
6 changes: 3 additions & 3 deletions token/js/test/unit/transferHook.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getExtraAccountMetas, resolveExtraAccountMeta } from '../../src';
import { getExtraAccountMetaList, resolveExtraAccountMeta } from '../../src';
import { expect } from 'chai';
import type { Connection } from '@solana/web3.js';
import { PublicKey } from '@solana/web3.js';
Expand Down Expand Up @@ -100,14 +100,14 @@ describe('transferHookExtraAccounts', () => {
});
});

it('getExtraAccountMetas', () => {
it('getExtraAccountMetaList', () => {
const accountInfo = {
data: extraAccountList,
owner: PublicKey.default,
executable: false,
lamports: 0,
};
const parsedExtraAccounts = getExtraAccountMetas(accountInfo);
const parsedExtraAccounts = getExtraAccountMetaList(accountInfo);
expect(parsedExtraAccounts).to.not.be.null;
if (parsedExtraAccounts == null) {
return;
Expand Down

0 comments on commit 0b70d77

Please sign in to comment.