Skip to content

Commit 1321a91

Browse files
authored
Merge pull request #7439 from BitGo/TMS-1502
feat(sdk-coin-sol): accepting idempotent ATA instructions
2 parents 2adcba1 + 2b41681 commit 1321a91

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

modules/sdk-coin-sol/src/lib/utils.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ const DECODED_SIGNATURE_LENGTH = 64; // https://docs.solana.com/terminology#sign
6363
const BASE_58_ENCONDING_REGEX = '[1-9A-HJ-NP-Za-km-z]';
6464
const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111';
6565

66+
/**
67+
* Checks if an ATA instruction is idempotent (discriminator byte = 1).
68+
*
69+
* Idempotent ATA instructions use Buffer.from([1]) as instruction data, created by
70+
* @solana/spl-token's createAssociatedTokenAccountIdempotentInstruction() (v0.4.1+).
71+
* An idempotent ATA instruction succeed even if account exists,
72+
* preventing race condition errors in concurrent scenarios.
73+
*
74+
* @param {TransactionInstruction} instruction - The instruction to validate
75+
* @returns {boolean} True if instruction data is a single byte with value 1
76+
*/
77+
export function isIdempotentAtaInstruction(instruction: TransactionInstruction): boolean {
78+
return instruction.data.length === 1 && instruction.data[0] === 1;
79+
}
80+
6681
/** @inheritdoc */
6782
export function isValidAddress(address: string): boolean {
6883
return isValidPublicKey(address);
@@ -395,7 +410,9 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn
395410
return StakeInstruction.decodeInstructionType(instruction);
396411
case ASSOCIATED_TOKEN_PROGRAM_ID.toString():
397412
// TODO: change this when @spl-token supports decoding associated token instructions
398-
if (instruction.data.length === 0) {
413+
// Support both legacy ATA creation (data.length === 0) and idempotent ATA creation (discriminator = 1)
414+
// Both instruction types are treated as 'InitializeAssociatedTokenAccount' for compatibility
415+
if (instruction.data.length === 0 || isIdempotentAtaInstruction(instruction)) {
399416
return 'InitializeAssociatedTokenAccount';
400417
} else {
401418
throw new NotSupported(

modules/sdk-coin-sol/test/unit/utils.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
stakingWithdrawInstructionsIndexes,
1010
} from '../../src/lib/constants';
1111
import BigNumber from 'bignumber.js';
12-
import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
12+
import { TOKEN_2022_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token';
1313

1414
describe('SOL util library', function () {
1515
describe('isValidAddress', function () {
@@ -547,4 +547,56 @@ describe('SOL util library', function () {
547547
});
548548
});
549549
});
550+
551+
describe('isIdempotentAtaInstruction', function () {
552+
const mockKeys = [
553+
{ pubkey: new PublicKey('9i8CSiz2un7rfuNvTMt1tTXbHSaMkPZyJ4MexY1yeZBD'), isSigner: true, isWritable: true },
554+
{ pubkey: new PublicKey('3F7X7ifwMR29Z3t1YamFg6yzCcsSkjAZpZF8yU1kWURh'), isSigner: false, isWritable: true },
555+
];
556+
557+
it('should return true for idempotent ATA instruction with discriminator byte 1', function () {
558+
const instruction = new TransactionInstruction({
559+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
560+
keys: mockKeys,
561+
data: Buffer.from([1]),
562+
});
563+
Utils.isIdempotentAtaInstruction(instruction).should.equal(true);
564+
});
565+
566+
it('should return true for idempotent ATA instruction from base64 "AQ=="', function () {
567+
const instruction = new TransactionInstruction({
568+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
569+
keys: mockKeys,
570+
data: Buffer.from('AQ==', 'base64'),
571+
});
572+
Utils.isIdempotentAtaInstruction(instruction).should.equal(true);
573+
});
574+
575+
it('should return false for legacy ATA instruction with empty data', function () {
576+
const instruction = new TransactionInstruction({
577+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
578+
keys: mockKeys,
579+
data: Buffer.from([]),
580+
});
581+
Utils.isIdempotentAtaInstruction(instruction).should.equal(false);
582+
});
583+
584+
it('should return false for instruction with wrong discriminator', function () {
585+
const instruction = new TransactionInstruction({
586+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
587+
keys: mockKeys,
588+
data: Buffer.from([2]),
589+
});
590+
Utils.isIdempotentAtaInstruction(instruction).should.equal(false);
591+
});
592+
593+
it('should return false for instruction with multiple bytes', function () {
594+
const instruction = new TransactionInstruction({
595+
programId: ASSOCIATED_TOKEN_PROGRAM_ID,
596+
keys: mockKeys,
597+
data: Buffer.from([1, 2]),
598+
});
599+
Utils.isIdempotentAtaInstruction(instruction).should.equal(false);
600+
});
601+
});
550602
});

0 commit comments

Comments
 (0)