diff --git a/modules/sdk-coin-sol/src/lib/explainTransactionWasm.ts b/modules/sdk-coin-sol/src/lib/explainTransactionWasm.ts index fbe3b90a91..560cdff615 100644 --- a/modules/sdk-coin-sol/src/lib/explainTransactionWasm.ts +++ b/modules/sdk-coin-sol/src/lib/explainTransactionWasm.ts @@ -5,6 +5,7 @@ import { ZK_ELGAMAL_PROOF_PROGRAM_ID, CT_EXT_DISCRIMINATOR, CT_SUB_DISCRIMINATORS, + TOKEN_ACL_PROGRAM_ID, } from './constants'; import { StakingAuthorizeParams, TransactionExplanation as SolLibTransactionExplanation } from './iface'; import { findTokenName } from './instructionParamsFactory'; @@ -117,6 +118,18 @@ function isWasmConfidentialTransferInstruction(instr: InstructionParams): boolea return false; } +/** + * Returns true if a WASM-parsed Unknown instruction is a Token ACL permissionless + * thaw instruction (sRFC-37 Token ACL program). + * + * The wasm parser does not know the Token ACL program, so ThawPermissionlessIdempotent + * instructions parse as Unknown. Mirrors the JS-side `getInstructionType` case for + * TOKEN_ACL_PROGRAM_ID in lib/utils.ts. + */ +function isWasmTokenAclThawInstruction(instr: InstructionParams): boolean { + return instr.type === 'Unknown' && instr.programId === TOKEN_ACL_PROGRAM_ID; +} + function deriveTransactionType( instructions: InstructionParams[], combined: CombinedPattern | null, @@ -131,8 +144,8 @@ function deriveTransactionType( if (instructions.some((i) => i.type === 'StakePoolDepositSol')) return TransactionType.StakingActivate; if (instructions.some((i) => i.type === 'StakePoolWithdrawStake')) return TransactionType.StakingDeactivate; - // ATA-only transactions (ignoring boilerplate) - const meaningful = instructions.filter((i) => !BOILERPLATE_TYPES.has(i.type)); + // ATA-only transactions (ignoring boilerplate and Token ACL permissionless thaw) + const meaningful = instructions.filter((i) => !BOILERPLATE_TYPES.has(i.type) && !isWasmTokenAclThawInstruction(i)); if (meaningful.length > 0 && meaningful.every((i) => i.type === 'CreateAssociatedTokenAccount')) { return TransactionType.AssociatedTokenAccountInitialization; } @@ -145,7 +158,8 @@ function deriveTransactionType( if (instructions.some((i) => i.type === 'Unknown' && isWasmConfidentialTransferInstruction(i))) { return TransactionType.ConfidentialTransfer; } - if (instructions.some((i) => i.type === 'Unknown')) return TransactionType.CustomTx; + if (instructions.some((i) => i.type === 'Unknown' && !isWasmTokenAclThawInstruction(i))) + return TransactionType.CustomTx; // Send requires an explicit Transfer or TokenTransfer instruction. // Everything else is a custom/unrecognized transaction. diff --git a/modules/sdk-coin-sol/test/unit/explainTransactionWasm.ts b/modules/sdk-coin-sol/test/unit/explainTransactionWasm.ts index 7b4a644408..a0bd07bdb6 100644 --- a/modules/sdk-coin-sol/test/unit/explainTransactionWasm.ts +++ b/modules/sdk-coin-sol/test/unit/explainTransactionWasm.ts @@ -23,5 +23,42 @@ describe('explainTransactionWasm', function () { explained.outputs.length.should.equal(0); (typeof explained.memo).should.equal('string'); }); + + it('should classify token enablement (ATA init + Token ACL permissionless thaw) as AssociatedTokenAccountInitialization', function () { + // Token-2022 mint (TransferHook + DefaultAccountState frozen, sRFC-37 Token ACL gating with + // permissionless thaw enabled). The enablement prebuild appends the Token ACL + // ThawPermissionlessIdempotent instruction (program TACLkU6..., discriminator 9) after the + // idempotent ATA-init. The wasm parser does not know the Token ACL program, so it parses as + // Unknown — this previously misclassified the enablement as CustomTx and failed + // verifyTxType ("Invalid transaction type on token enablement"). + const ATA_INIT_WITH_TOKEN_ACL_THAW_BASE64 = + 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAYJUhCF9LHo7KiG7HDDtCQbbIAJG2uinMlIg5Jby7LiGCUk9dBaCEmT0ePnPYh4E/3Lb7jS7cZGyoPKxxPGdAbPtQZ6KIKRB9he5+4YMC6z6SnI4p2UviExxtQ63M7yyNzKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoxrewX5VA2FlHpnxuxejiSESVZI1yzGOHB4uV5TmCPCTxZ+GHLPVbt3R+ul84ehxVAgqM3rmztfFLW8m/o/41jJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkGs3+acPQG6jbTKfWUpRYFlg/y4TcXWy7aAGbKMjprzgbd9uHudY/eGEJdvORszdq2GvxNg7kNJ/69+SjYoYv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBgYAAgAFAwgBAQcGAAIFAAEEAQk='; + + const explained = explainSolTransaction({ + txBase64: ATA_INIT_WITH_TOKEN_ACL_THAW_BASE64, + feeInfo: { fee: '5000' }, + coinName: 'tsol', + }); + + explained.type.should.equal('AssociatedTokenAccountInitialization'); + explained.tokenEnablements?.length.should.equal(1); + explained.tokenEnablements?.[0].tokenName.should.equal('3VDBJWgzRUscjQzzAp52na1dDquExZmD1PkCvH9svGF6'); + }); + + it('should classify token transfer with Token ACL permissionless thaw as Send', function () { + // TokenTransfer (transferChecked, Token-2022) + Token ACL ThawPermissionlessIdempotent for + // the destination ATA. The Unknown thaw instruction previously triggered the CustomTx + // fallback before the Send check. + const TRANSFER_WITH_TOKEN_ACL_THAW_BASE64 = + 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAQIUhCF9LHo7KiG7HDDtCQbbIAJG2uinMlIg5Jby7LiGCVlN8xCFuLWWqDUa7zdMscRzHJShFmQ4avYUk5r95qhqrMUSZcan5PQzSooMXH6hoZngse42Tou1uN7aFDVDLStBnoogpEH2F7n7hgwLrPpKcjinZS+ITHG1DrczvLI3MooxrewX5VA2FlHpnxuxejiSESVZI1yzGOHB4uV5TmCPCTxZ+GHLPVbt3R+ul84ehxVAgqM3rmztfFLW8m/o/41BrN/mnD0Buo20yn1lKUWBZYP8uE3F1su2gBmyjI6a84G3fbh7nWP3hhCXbzkbM3athr8TYO5DSf+vfko2KGL/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgcEAwUBAAoMAQAAAAAAAAAJBgYAAQUAAgQBCQ=='; + + const explained = explainSolTransaction({ + txBase64: TRANSFER_WITH_TOKEN_ACL_THAW_BASE64, + feeInfo: { fee: '5000' }, + coinName: 'tsol', + }); + + explained.type.should.equal('Send'); + }); }); });