From 7333fd469e565ed211257ba14ed38822b5585d47 Mon Sep 17 00:00:00 2001 From: Sachu Shaji Abraham Date: Thu, 23 Jul 2026 18:31:25 +0530 Subject: [PATCH] fix(sdk-coin-sol): fall back to custom instruction on decode failure decodeInstruction from @solana/spl-token cannot decode Token-2022 extension-family instructions (Pause, PermissionedBurn, transfer fee ops, ...) at any published version and throws TokenInvalidInstructionTypeError, crashing customTx validation client- and server-side. Wrap the call in try/catch and return CustomInstruction, the same path already used for decodable-but-unmapped instructions. Ticket: SCAAS-10578 --- modules/sdk-coin-sol/src/lib/utils.ts | 14 +++++++++++++- modules/sdk-coin-sol/test/unit/utils.ts | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/sdk-coin-sol/src/lib/utils.ts b/modules/sdk-coin-sol/src/lib/utils.ts index 30158ed17d..0daa89a849 100644 --- a/modules/sdk-coin-sol/src/lib/utils.ts +++ b/modules/sdk-coin-sol/src/lib/utils.ts @@ -15,6 +15,7 @@ import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, decodeInstruction, + DecodedInstruction, TokenInstruction, TransferFeeInstruction, } from '@solana/spl-token'; @@ -414,7 +415,18 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn ) { return 'TokenTransfer'; } - const decodedInstruction = decodeInstruction(instruction, instruction.programId); + let decodedInstruction: DecodedInstruction; + try { + decodedInstruction = decodeInstruction(instruction, instruction.programId); + } catch (e) { + // decodeInstruction only supports the base token instruction set — + // extension-family instructions (Pause, PermissionedBurn, transfer + // fee ops, ...) throw TokenInvalidInstructionTypeError at EVERY + // @solana/spl-token version. Those must not crash classification: + // fall back to CustomInstruction, the same path used below for + // decodable-but-unmapped instruction types. + return 'CustomInstruction'; + } const instructionTypeMap: Map = new Map(); instructionTypeMap.set(TokenInstruction.CloseAccount, 'CloseAssociatedTokenAccount'); instructionTypeMap.set(TokenInstruction.Burn, 'Burn'); diff --git a/modules/sdk-coin-sol/test/unit/utils.ts b/modules/sdk-coin-sol/test/unit/utils.ts index 1a3d32c7d9..d496e6c7b2 100644 --- a/modules/sdk-coin-sol/test/unit/utils.ts +++ b/modules/sdk-coin-sol/test/unit/utils.ts @@ -255,6 +255,25 @@ describe('SOL util library', function () { }); Utils.getInstructionType(invalidInstruction).should.equal('CustomInstruction'); }); + it('should fallback to customInstruction for token instructions that cannot be decoded', function () { + // decodeInstruction only covers the base token instruction set; + // extension-family instructions (e.g. PermissionedBurnExtension = 46, + // PausableExtension = 44) throw TokenInvalidInstructionTypeError at + // every @solana/spl-token version. They must classify as + // CustomInstruction instead of crashing customTx validation. + const permissionedBurnChecked = new TransactionInstruction({ + keys: [ + { pubkey: new PublicKey(testData.authAccount.pub), isSigner: false, isWritable: true }, + { pubkey: new PublicKey(testData.nonceAccount.pub), isSigner: false, isWritable: true }, + { pubkey: new PublicKey(testData.authAccount.pub), isSigner: true, isWritable: false }, + { pubkey: new PublicKey(testData.authAccount.pub), isSigner: true, isWritable: false }, + ], + programId: TOKEN_2022_PROGRAM_ID, + // [46 = PermissionedBurnExtension, 2 = BurnChecked, u64 amount, u8 decimals] + data: Buffer.from([46, 2, 0, 202, 154, 59, 0, 0, 0, 0, 9]), + }); + Utils.getInstructionType(permissionedBurnChecked).should.equal('CustomInstruction'); + }); it('should succeed for ComputeBudget SetComputeUnitLimit instruction', function () { const setComputeUnitLimitInstruction = ComputeBudgetProgram.setComputeUnitLimit({ units: 300000,