Skip to content
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
20 changes: 17 additions & 3 deletions modules/sdk-coin-sol/src/lib/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions modules/sdk-coin-sol/test/unit/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
Loading