From 2b41681b280d7852cf8b547ba3759137cb31dd30 Mon Sep 17 00:00:00 2001 From: damodarnaik699 Date: Thu, 6 Nov 2025 15:06:12 +0530 Subject: [PATCH] feat(sdk-coin-sol): accepting idempotent ATA instructions Ticket: TMS-1502 --- modules/sdk-coin-sol/src/lib/utils.ts | 19 ++++++++- modules/sdk-coin-sol/test/unit/utils.ts | 54 ++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/modules/sdk-coin-sol/src/lib/utils.ts b/modules/sdk-coin-sol/src/lib/utils.ts index 90e19f2cc2..6c499f7b70 100644 --- a/modules/sdk-coin-sol/src/lib/utils.ts +++ b/modules/sdk-coin-sol/src/lib/utils.ts @@ -63,6 +63,21 @@ const DECODED_SIGNATURE_LENGTH = 64; // https://docs.solana.com/terminology#sign const BASE_58_ENCONDING_REGEX = '[1-9A-HJ-NP-Za-km-z]'; const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111'; +/** + * Checks if an ATA instruction is idempotent (discriminator byte = 1). + * + * Idempotent ATA instructions use Buffer.from([1]) as instruction data, created by + * @solana/spl-token's createAssociatedTokenAccountIdempotentInstruction() (v0.4.1+). + * An idempotent ATA instruction succeed even if account exists, + * preventing race condition errors in concurrent scenarios. + * + * @param {TransactionInstruction} instruction - The instruction to validate + * @returns {boolean} True if instruction data is a single byte with value 1 + */ +export function isIdempotentAtaInstruction(instruction: TransactionInstruction): boolean { + return instruction.data.length === 1 && instruction.data[0] === 1; +} + /** @inheritdoc */ export function isValidAddress(address: string): boolean { return isValidPublicKey(address); @@ -395,7 +410,9 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn return StakeInstruction.decodeInstructionType(instruction); case ASSOCIATED_TOKEN_PROGRAM_ID.toString(): // TODO: change this when @spl-token supports decoding associated token instructions - if (instruction.data.length === 0) { + // Support both legacy ATA creation (data.length === 0) and idempotent ATA creation (discriminator = 1) + // Both instruction types are treated as 'InitializeAssociatedTokenAccount' for compatibility + if (instruction.data.length === 0 || isIdempotentAtaInstruction(instruction)) { return 'InitializeAssociatedTokenAccount'; } else { throw new NotSupported( diff --git a/modules/sdk-coin-sol/test/unit/utils.ts b/modules/sdk-coin-sol/test/unit/utils.ts index 95cc835e8d..31bd27891c 100644 --- a/modules/sdk-coin-sol/test/unit/utils.ts +++ b/modules/sdk-coin-sol/test/unit/utils.ts @@ -9,7 +9,7 @@ import { stakingWithdrawInstructionsIndexes, } from '../../src/lib/constants'; import BigNumber from 'bignumber.js'; -import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; +import { TOKEN_2022_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'; describe('SOL util library', function () { describe('isValidAddress', function () { @@ -547,4 +547,56 @@ describe('SOL util library', function () { }); }); }); + + describe('isIdempotentAtaInstruction', function () { + const mockKeys = [ + { pubkey: new PublicKey('9i8CSiz2un7rfuNvTMt1tTXbHSaMkPZyJ4MexY1yeZBD'), isSigner: true, isWritable: true }, + { pubkey: new PublicKey('3F7X7ifwMR29Z3t1YamFg6yzCcsSkjAZpZF8yU1kWURh'), isSigner: false, isWritable: true }, + ]; + + it('should return true for idempotent ATA instruction with discriminator byte 1', function () { + const instruction = new TransactionInstruction({ + programId: ASSOCIATED_TOKEN_PROGRAM_ID, + keys: mockKeys, + data: Buffer.from([1]), + }); + Utils.isIdempotentAtaInstruction(instruction).should.equal(true); + }); + + it('should return true for idempotent ATA instruction from base64 "AQ=="', function () { + const instruction = new TransactionInstruction({ + programId: ASSOCIATED_TOKEN_PROGRAM_ID, + keys: mockKeys, + data: Buffer.from('AQ==', 'base64'), + }); + Utils.isIdempotentAtaInstruction(instruction).should.equal(true); + }); + + it('should return false for legacy ATA instruction with empty data', function () { + const instruction = new TransactionInstruction({ + programId: ASSOCIATED_TOKEN_PROGRAM_ID, + keys: mockKeys, + data: Buffer.from([]), + }); + Utils.isIdempotentAtaInstruction(instruction).should.equal(false); + }); + + it('should return false for instruction with wrong discriminator', function () { + const instruction = new TransactionInstruction({ + programId: ASSOCIATED_TOKEN_PROGRAM_ID, + keys: mockKeys, + data: Buffer.from([2]), + }); + Utils.isIdempotentAtaInstruction(instruction).should.equal(false); + }); + + it('should return false for instruction with multiple bytes', function () { + const instruction = new TransactionInstruction({ + programId: ASSOCIATED_TOKEN_PROGRAM_ID, + keys: mockKeys, + data: Buffer.from([1, 2]), + }); + Utils.isIdempotentAtaInstruction(instruction).should.equal(false); + }); + }); });