Skip to content

Allow Fallback for TokenTransferHookAccountDataNotFound in SPL Token JS #212

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
52 changes: 29 additions & 23 deletions clients/js-legacy/src/extensions/transferHook/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,18 @@ export async function updateTransferHook(
/**
* Transfer tokens from one account to another, asserting the token mint, and decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Source account
* @param mint Mint for the account
* @param destination Destination account
* @param authority Authority of the source account
* @param amount Number of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Source account
* @param mint Mint for the account
* @param destination Destination account
* @param authority Authority of the source account
* @param amount Number of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
* @param allowAccountDataFallback Fallback to zeroed pubkey when resolveExtraAccountMeta fails with TokenTransferHookAccountDataNotFound
*
* @return Signature of the confirmed transaction
*/
Expand All @@ -101,6 +102,7 @@ export async function transferCheckedWithTransferHook(
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_PROGRAM_ID,
allowAccountDataFallback = false,
): Promise<TransactionSignature> {
const [authorityPublicKey, signers] = getSigners(authority, multiSigners);

Expand All @@ -116,6 +118,7 @@ export async function transferCheckedWithTransferHook(
signers,
confirmOptions?.commitment,
programId,
allowAccountDataFallback,
),
);

Expand All @@ -125,18 +128,19 @@ export async function transferCheckedWithTransferHook(
/**
* Transfer tokens from one account to another, asserting the transfer fee, token mint, and decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Source account
* @param mint Mint for the account
* @param destination Destination account
* @param authority Authority of the source account
* @param amount Number of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param fee The calculated fee for the transfer fee extension
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Source account
* @param mint Mint for the account
* @param destination Destination account
* @param authority Authority of the source account
* @param amount Number of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param fee The calculated fee for the transfer fee extension
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
* @param allowAccountDataFallback Fallback to zeroed pubkey when resolveExtraAccountMeta fails with TokenTransferHookAccountDataNotFound
*
* @return Signature of the confirmed transaction
*/
Expand All @@ -153,6 +157,7 @@ export async function transferCheckedWithFeeAndTransferHook(
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_PROGRAM_ID,
allowAccountDataFallback = false,
): Promise<TransactionSignature> {
const [authorityPublicKey, signers] = getSigners(authority, multiSigners);

Expand All @@ -169,6 +174,7 @@ export async function transferCheckedWithFeeAndTransferHook(
signers,
confirmOptions?.commitment,
programId,
allowAccountDataFallback,
),
);

Expand Down
69 changes: 39 additions & 30 deletions clients/js-legacy/src/extensions/transferHook/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,16 @@ export function createExecuteInstruction(
*
* 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
* @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
* @param allowAccountDataFallback Fallback to zeroed pubkey when resolveExtraAccountMeta fails with TokenTransferHookAccountDataNotFound
*/
export async function addExtraAccountMetasForExecute(
connection: Connection,
Expand All @@ -196,6 +197,7 @@ export async function addExtraAccountMetasForExecute(
owner: PublicKey,
amount: number | bigint,
commitment?: Commitment,
allowAccountDataFallback = false,
) {
const validateStatePubkey = getExtraAccountMetaAddress(mint, programId);
const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment);
Expand Down Expand Up @@ -228,6 +230,7 @@ export async function addExtraAccountMetasForExecute(
executeInstruction.keys,
executeInstruction.data,
executeInstruction.programId,
allowAccountDataFallback,
),
executeInstruction.keys,
),
Expand All @@ -245,16 +248,17 @@ export async function addExtraAccountMetasForExecute(
/**
* 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 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
* @param commitment Commitment to use
* @param programId SPL Token program account
* @param connection Connection to use
* @param source Source account
* @param mint Mint to update
* @param destination Destination account
* @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
* @param commitment Commitment to use
* @param programId SPL Token program account
* @param allowAccountDataFallback Fallback to zeroed pubkey when resolveExtraAccountMeta fails with TokenTransferHookAccountDataNotFound
*
* @return Instruction to add to a transaction
*/
Expand All @@ -269,6 +273,7 @@ export async function createTransferCheckedWithTransferHookInstruction(
multiSigners: (Signer | PublicKey)[] = [],
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID,
allowAccountDataFallback = false,
) {
const instruction = createTransferCheckedInstruction(
source,
Expand All @@ -295,6 +300,7 @@ export async function createTransferCheckedWithTransferHookInstruction(
owner,
amount,
commitment,
allowAccountDataFallback,
);
}

Expand All @@ -304,17 +310,18 @@ export async function createTransferCheckedWithTransferHookInstruction(
/**
* 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 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
* @param multiSigners The signer account(s) for a multisig
* @param commitment Commitment to use
* @param programId SPL Token program account
* @param connection Connection to use
* @param source Source account
* @param mint Mint to update
* @param destination Destination account
* @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
* @param multiSigners The signer account(s) for a multisig
* @param commitment Commitment to use
* @param programId SPL Token program account
* @param allowAccountDataFallback Fallback to zeroed pubkey when resolveExtraAccountMeta fails with TokenTransferHookAccountDataNotFound
*
* @return Instruction to add to a transaction
*/
Expand All @@ -330,6 +337,7 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction(
multiSigners: (Signer | PublicKey)[] = [],
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID,
allowAccountDataFallback = false,
) {
const instruction = createTransferCheckedWithFeeInstruction(
source,
Expand Down Expand Up @@ -357,6 +365,7 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction(
owner,
amount,
commitment,
allowAccountDataFallback,
);
}

Expand Down
16 changes: 12 additions & 4 deletions clients/js-legacy/src/extensions/transferHook/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { AccountInfo, AccountMeta, Connection } from '@solana/web3.js';
import { PublicKey } from '@solana/web3.js';
import { bool, publicKey, u64 } from '@solana/buffer-layout-utils';
import type { Account } from '../../state/account.js';
import { TokenTransferHookAccountNotFound } from '../../errors.js';
import { TokenTransferHookAccountDataNotFound, TokenTransferHookAccountNotFound } from '../../errors.js';
import { unpackSeeds } from './seeds.js';

/** TransferHook as stored by the program */
Expand Down Expand Up @@ -112,6 +112,7 @@ export async function resolveExtraAccountMeta(
previousMetas: AccountMeta[],
instructionData: Buffer,
transferHookProgramId: PublicKey,
allowAccountDataFallback = false,
): Promise<AccountMeta> {
if (extraMeta.discriminator === 0) {
return {
Expand All @@ -133,8 +134,15 @@ export async function resolveExtraAccountMeta(
programId = previousMetas[accountIndex].pubkey;
}

const seeds = await unpackSeeds(extraMeta.addressConfig, previousMetas, instructionData, connection);
const pubkey = PublicKey.findProgramAddressSync(seeds, programId)[0];
try {
const seeds = await unpackSeeds(extraMeta.addressConfig, previousMetas, instructionData, connection);
const pubkey = PublicKey.findProgramAddressSync(seeds, programId)[0];

return { pubkey, isSigner: extraMeta.isSigner, isWritable: extraMeta.isWritable };
return { pubkey, isSigner: extraMeta.isSigner, isWritable: extraMeta.isWritable };
} catch (error) {
if (allowAccountDataFallback && error instanceof TokenTransferHookAccountDataNotFound) {
return { pubkey: PublicKey.default, isSigner: false, isWritable: false };
}
throw error;
}
}
69 changes: 68 additions & 1 deletion clients/js-legacy/test/unit/transferHook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,10 @@ describe('transferHook', () => {
arbitraryProgramId = Keypair.generate().publicKey;
});

function createMockFetchAccountDataFn(extraAccounts: ExtraAccountMeta[]) {
function createMockFetchAccountDataFn(
extraAccounts: ExtraAccountMeta[],
notExistingAccounts: PublicKey[] = [],
) {
return async function mockFetchAccountDataFn(
publicKey: PublicKey,
_commitmentOrConfig?: Parameters<Connection['getAccountInfo']>[1],
Expand Down Expand Up @@ -300,6 +303,9 @@ describe('transferHook', () => {
};
}

if (notExistingAccounts.some(notExistingAccount => notExistingAccount.equals(publicKey))) {
return null;
}
return {
data: Buffer.from([]),
owner: PublicKey.default,
Expand Down Expand Up @@ -438,6 +444,67 @@ describe('transferHook', () => {
expect(instruction.keys).to.eql(checkMetas);
});

it('can fallback to zeroed pubkey for fallbackAccountDataNotFound', async () => {
// prettier-ignore
connection.getAccountInfo = createMockFetchAccountDataFn([
pda([
3, 0, // First seed: Account key at index 0 (2)
3, 2, // Second seed: Account key at index 2 (2)
], false, false),
pda([
1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8)
4, 2, 32, 64, // Second seed: Account data 32..64 at index 2 (destination account) (4)
], false, false),
], [
destinationPubkey, // mock as destination account is not created yet
]);

const instruction = new TransactionInstruction({
keys: [
{ pubkey: sourcePubkey, isSigner: false, isWritable: true },
{ pubkey: mintPubkey, isSigner: false, isWritable: false },
{ pubkey: destinationPubkey, isSigner: false, isWritable: true },
{ pubkey: authorityPubkey, isSigner: true, isWritable: false },
],
programId: transferHookProgramId,
});

await addExtraAccountMetasForExecute(
connection,
instruction,
transferHookProgramId,
sourcePubkey,
mintPubkey,
destinationPubkey,
authorityPubkey,
amount,
undefined,
true,
);

const extraMeta1Pubkey = PublicKey.findProgramAddressSync(
[
sourcePubkey.toBuffer(), // Account key at index 0
destinationPubkey.toBuffer(), // Account key at index 2
],
transferHookProgramId,
)[0];
const extraMeta2Pubkey = PublicKey.default;

const checkMetas = [
{ pubkey: sourcePubkey, isSigner: false, isWritable: true },
{ pubkey: mintPubkey, isSigner: false, isWritable: false },
{ pubkey: destinationPubkey, isSigner: false, isWritable: true },
{ pubkey: authorityPubkey, isSigner: true, isWritable: false },
{ pubkey: extraMeta1Pubkey, isSigner: false, isWritable: false },
{ pubkey: extraMeta2Pubkey, isSigner: false, isWritable: false },
{ pubkey: transferHookProgramId, isSigner: false, isWritable: false },
{ pubkey: validateStatePubkey, isSigner: false, isWritable: false },
];

expect(instruction.keys).to.eql(checkMetas);
});

it('can create a transfer instruction with extra metas', async () => {
// prettier-ignore
connection.getAccountInfo = createMockFetchAccountDataFn([
Expand Down