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
19 changes: 18 additions & 1 deletion modules/sdk-coin-sol/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
54 changes: 53 additions & 1 deletion modules/sdk-coin-sol/test/unit/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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);
});
});
});