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
38 changes: 32 additions & 6 deletions modules/sdk-coin-sol/src/lib/closeAtaBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ const MIX_API_ERROR_MESSAGE =

export class CloseAtaBuilder extends TransactionBuilder {
// Unified storage for all close entries (single or bulk)
protected _closeAtaEntries: { accountAddress: string; destinationAddress: string; authorityAddress: string }[] = [];
protected _closeAtaEntries: {
accountAddress: string;
destinationAddress: string;
authorityAddress: string;
programId?: string;
}[] = [];

// Which API has been used on this builder instance. Locks in on first call so we can
// reject attempts to mix the legacy single-ATA setters with the bulk addCloseAtaInstruction().
Expand Down Expand Up @@ -71,6 +76,16 @@ export class CloseAtaBuilder extends TransactionBuilder {
return this;
}

/** Sets the SPL token program used by the close instruction. */
programId(programId: string): this {
this._assertSingleAtaApiUsable();
validateAddress(programId, 'programId');
this._apiMode = 'single';
this._ensureSingleEntry();
this._closeAtaEntries[0].programId = programId;
return this;
}

/**
* Throws if the bulk-ATA API has already been used on this builder.
*/
Expand All @@ -93,18 +108,27 @@ export class CloseAtaBuilder extends TransactionBuilder {
* Add an ATA to close in this transaction (for bulk closure).
* Cannot be mixed with the single-ATA API (accountAddress/destinationAddress/authorityAddress).
*
* @param {string} accountAddress - the ATA address to close
* @param {string} destinationAddress - where rent SOL goes (root wallet address)
* @param {string} authorityAddress - ATA owner who must sign
* @param accountAddress - the ATA address to close
* @param destinationAddress - where rent SOL goes (root wallet address)
* @param authorityAddress - ATA owner who must sign
* @param programId - SPL token program owning the ATA; omitted for legacy SPL
*/
addCloseAtaInstruction(accountAddress: string, destinationAddress: string, authorityAddress: string): this {
addCloseAtaInstruction(
accountAddress: string,
destinationAddress: string,
authorityAddress: string,
programId?: string
): this {
if (this._apiMode === 'single') {
throw new BuildTransactionError(MIX_API_ERROR_MESSAGE);
}

validateAddress(accountAddress, 'accountAddress');
validateAddress(destinationAddress, 'destinationAddress');
validateAddress(authorityAddress, 'authorityAddress');
if (programId) {
validateAddress(programId, 'programId');
}

if (accountAddress === destinationAddress) {
throw new BuildTransactionError('Account address to close cannot be the same as the destination address');
Expand All @@ -115,7 +139,7 @@ export class CloseAtaBuilder extends TransactionBuilder {
}

this._apiMode = 'bulk';
this._closeAtaEntries.push({ accountAddress, destinationAddress, authorityAddress });
this._closeAtaEntries.push({ accountAddress, destinationAddress, authorityAddress, programId });
return this;
}

Expand All @@ -129,6 +153,7 @@ export class CloseAtaBuilder extends TransactionBuilder {
accountAddress: ataCloseInstruction.params.accountAddress,
destinationAddress: ataCloseInstruction.params.destinationAddress,
authorityAddress: ataCloseInstruction.params.authorityAddress,
programId: ataCloseInstruction.params.programId,
});
}
}
Expand Down Expand Up @@ -158,6 +183,7 @@ export class CloseAtaBuilder extends TransactionBuilder {
accountAddress: entry.accountAddress,
destinationAddress: entry.destinationAddress,
authorityAddress: entry.authorityAddress,
...(entry.programId ? { programId: entry.programId } : {}),
},
})
);
Expand Down
8 changes: 7 additions & 1 deletion modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,13 @@ export interface AtaInit {

export interface AtaClose {
type: InstructionBuilderTypes.CloseAssociatedTokenAccount;
params: { accountAddress: string; destinationAddress: string; authorityAddress: string };
params: {
accountAddress: string;
destinationAddress: string;
authorityAddress: string;
/** SPL token program owning the ATA; omitted for legacy Token Program. */
programId?: string;
};
}

export interface AtaRecoverNested {
Expand Down
6 changes: 6 additions & 0 deletions modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ function parseSendInstructions(
accountAddress,
destinationAddress,
authorityAddress,
...(instruction.programId.equals(TOKEN_2022_PROGRAM_ID)
? { programId: instruction.programId.toString() }
: {}),
},
};
instructionData.push(ataClose);
Expand Down Expand Up @@ -1176,6 +1179,9 @@ function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Arra
accountAddress: instruction.keys[ataCloseInstructionKeysIndexes.AccountAddress].pubkey.toString(),
destinationAddress: instruction.keys[ataCloseInstructionKeysIndexes.DestinationAddress].pubkey.toString(),
authorityAddress: instruction.keys[ataCloseInstructionKeysIndexes.AuthorityAddress].pubkey.toString(),
...(instruction.programId.equals(TOKEN_2022_PROGRAM_ID)
? { programId: instruction.programId.toString() }
: {}),
},
};
instructionData.push(ataClose);
Expand Down
8 changes: 6 additions & 2 deletions modules/sdk-coin-sol/src/lib/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createTransferCheckedInstruction,
createTransferCheckedWithFeeInstruction,
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
createApproveInstruction,
} from '@solana/spl-token';
import {
Expand Down Expand Up @@ -574,16 +575,19 @@ function createATAInstruction(data: AtaInit): TransactionInstruction[] {
*/
function closeATAInstruction(data: AtaClose): TransactionInstruction[] {
const {
params: { accountAddress, destinationAddress, authorityAddress },
params: { accountAddress, destinationAddress, authorityAddress, programId },
} = data;
assert(accountAddress, 'Missing accountAddress param');
assert(destinationAddress, 'Missing destinationAddress param');
assert(authorityAddress, 'Missing authorityAddress param');

const tokenProgramId = programId ? new PublicKey(programId) : TOKEN_PROGRAM_ID;
const closeAssociatedTokenAccountInstruction = createCloseAccountInstruction(
new PublicKey(accountAddress),
new PublicKey(destinationAddress),
new PublicKey(authorityAddress)
new PublicKey(authorityAddress),
[],
tokenProgramId
);
return [closeAssociatedTokenAccountInstruction];
}
Expand Down
14 changes: 14 additions & 0 deletions modules/sdk-coin-sol/test/unit/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ describe('Instruction Builder Tests: ', function () {
]);
});

it('Close ATA uses Token-2022 program when specified', () => {
const result = solInstructionFactory({
type: InstructionBuilderTypes.CloseAssociatedTokenAccount,
params: {
accountAddress: testData.authAccount.pub,
destinationAddress: testData.authAccount2.pub,
authorityAddress: testData.authAccount.pub,
programId: TOKEN_2022_PROGRAM_ID.toString(),
},
});

result[0].programId.equals(TOKEN_2022_PROGRAM_ID).should.be.true();
});

it('Transfer', () => {
const fromAddress = testData.authAccount.pub;
const toAddress = testData.nonceAccount.pub;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,23 @@ describe('Sol Close ATA Builder', () => {
instruction.params.destinationAddress.should.equal(destinationAddress);
}
});

it('builds mixed legacy SPL and Token-2022 close instructions', async () => {
const txBuilder = closeAtaBuilder();
txBuilder.addCloseAtaInstruction(ataAddress1, destinationAddress, account.pub);
txBuilder.addCloseAtaInstruction(
ataAddress2,
destinationAddress,
account.pub,
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
);

const tx = await txBuilder.build();
const instructions = tx.toJson().instructionsData;
instructions.length.should.equal(2);
should.not.exist(instructions[0].params.programId);
instructions[1].params.programId.should.equal('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
});
});

describe('Fail', () => {
Expand Down
Loading