Skip to content

Commit 4a45c72

Browse files
Merge pull request #9667 from BitGo/CHALO-1417-close-ata-flow
fix(sdk-coin-sol): support Token-2022 close ATA
2 parents b53a591 + 6267706 commit 4a45c72

6 files changed

Lines changed: 82 additions & 9 deletions

File tree

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ const MIX_API_ERROR_MESSAGE =
1414

1515
export class CloseAtaBuilder extends TransactionBuilder {
1616
// Unified storage for all close entries (single or bulk)
17-
protected _closeAtaEntries: { accountAddress: string; destinationAddress: string; authorityAddress: string }[] = [];
17+
protected _closeAtaEntries: {
18+
accountAddress: string;
19+
destinationAddress: string;
20+
authorityAddress: string;
21+
programId?: string;
22+
}[] = [];
1823

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

79+
/** Sets the SPL token program used by the close instruction. */
80+
programId(programId: string): this {
81+
this._assertSingleAtaApiUsable();
82+
validateAddress(programId, 'programId');
83+
this._apiMode = 'single';
84+
this._ensureSingleEntry();
85+
this._closeAtaEntries[0].programId = programId;
86+
return this;
87+
}
88+
7489
/**
7590
* Throws if the bulk-ATA API has already been used on this builder.
7691
*/
@@ -93,18 +108,27 @@ export class CloseAtaBuilder extends TransactionBuilder {
93108
* Add an ATA to close in this transaction (for bulk closure).
94109
* Cannot be mixed with the single-ATA API (accountAddress/destinationAddress/authorityAddress).
95110
*
96-
* @param {string} accountAddress - the ATA address to close
97-
* @param {string} destinationAddress - where rent SOL goes (root wallet address)
98-
* @param {string} authorityAddress - ATA owner who must sign
111+
* @param accountAddress - the ATA address to close
112+
* @param destinationAddress - where rent SOL goes (root wallet address)
113+
* @param authorityAddress - ATA owner who must sign
114+
* @param programId - SPL token program owning the ATA; omitted for legacy SPL
99115
*/
100-
addCloseAtaInstruction(accountAddress: string, destinationAddress: string, authorityAddress: string): this {
116+
addCloseAtaInstruction(
117+
accountAddress: string,
118+
destinationAddress: string,
119+
authorityAddress: string,
120+
programId?: string
121+
): this {
101122
if (this._apiMode === 'single') {
102123
throw new BuildTransactionError(MIX_API_ERROR_MESSAGE);
103124
}
104125

105126
validateAddress(accountAddress, 'accountAddress');
106127
validateAddress(destinationAddress, 'destinationAddress');
107128
validateAddress(authorityAddress, 'authorityAddress');
129+
if (programId) {
130+
validateAddress(programId, 'programId');
131+
}
108132

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

117141
this._apiMode = 'bulk';
118-
this._closeAtaEntries.push({ accountAddress, destinationAddress, authorityAddress });
142+
this._closeAtaEntries.push({ accountAddress, destinationAddress, authorityAddress, programId });
119143
return this;
120144
}
121145

@@ -129,6 +153,7 @@ export class CloseAtaBuilder extends TransactionBuilder {
129153
accountAddress: ataCloseInstruction.params.accountAddress,
130154
destinationAddress: ataCloseInstruction.params.destinationAddress,
131155
authorityAddress: ataCloseInstruction.params.authorityAddress,
156+
programId: ataCloseInstruction.params.programId,
132157
});
133158
}
134159
}
@@ -158,6 +183,7 @@ export class CloseAtaBuilder extends TransactionBuilder {
158183
accountAddress: entry.accountAddress,
159184
destinationAddress: entry.destinationAddress,
160185
authorityAddress: entry.authorityAddress,
186+
...(entry.programId ? { programId: entry.programId } : {}),
161187
},
162188
})
163189
);

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,13 @@ export interface AtaInit {
289289

290290
export interface AtaClose {
291291
type: InstructionBuilderTypes.CloseAssociatedTokenAccount;
292-
params: { accountAddress: string; destinationAddress: string; authorityAddress: string };
292+
params: {
293+
accountAddress: string;
294+
destinationAddress: string;
295+
authorityAddress: string;
296+
/** SPL token program owning the ATA; omitted for legacy Token Program. */
297+
programId?: string;
298+
};
293299
}
294300

295301
export interface AtaRecoverNested {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,9 @@ function parseSendInstructions(
314314
accountAddress,
315315
destinationAddress,
316316
authorityAddress,
317+
...(instruction.programId.equals(TOKEN_2022_PROGRAM_ID)
318+
? { programId: instruction.programId.toString() }
319+
: {}),
317320
},
318321
};
319322
instructionData.push(ataClose);
@@ -1201,6 +1204,9 @@ function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Arra
12011204
accountAddress: instruction.keys[ataCloseInstructionKeysIndexes.AccountAddress].pubkey.toString(),
12021205
destinationAddress: instruction.keys[ataCloseInstructionKeysIndexes.DestinationAddress].pubkey.toString(),
12031206
authorityAddress: instruction.keys[ataCloseInstructionKeysIndexes.AuthorityAddress].pubkey.toString(),
1207+
...(instruction.programId.equals(TOKEN_2022_PROGRAM_ID)
1208+
? { programId: instruction.programId.toString() }
1209+
: {}),
12041210
},
12051211
};
12061212
instructionData.push(ataClose);

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createTransferCheckedInstruction,
1010
createTransferCheckedWithFeeInstruction,
1111
TOKEN_2022_PROGRAM_ID,
12+
TOKEN_PROGRAM_ID,
1213
createApproveInstruction,
1314
} from '@solana/spl-token';
1415
import { struct, u8, s8, blob } from '@solana/buffer-layout';
@@ -608,16 +609,19 @@ function createATAInstruction(data: AtaInit): TransactionInstruction[] {
608609
*/
609610
function closeATAInstruction(data: AtaClose): TransactionInstruction[] {
610611
const {
611-
params: { accountAddress, destinationAddress, authorityAddress },
612+
params: { accountAddress, destinationAddress, authorityAddress, programId },
612613
} = data;
613614
assert(accountAddress, 'Missing accountAddress param');
614615
assert(destinationAddress, 'Missing destinationAddress param');
615616
assert(authorityAddress, 'Missing authorityAddress param');
616617

618+
const tokenProgramId = programId ? new PublicKey(programId) : TOKEN_PROGRAM_ID;
617619
const closeAssociatedTokenAccountInstruction = createCloseAccountInstruction(
618620
new PublicKey(accountAddress),
619621
new PublicKey(destinationAddress),
620-
new PublicKey(authorityAddress)
622+
new PublicKey(authorityAddress),
623+
[],
624+
tokenProgramId
621625
);
622626
return [closeAssociatedTokenAccountInstruction];
623627
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ describe('Instruction Builder Tests: ', function () {
3939
]);
4040
});
4141

42+
it('Close ATA uses Token-2022 program when specified', () => {
43+
const result = solInstructionFactory({
44+
type: InstructionBuilderTypes.CloseAssociatedTokenAccount,
45+
params: {
46+
accountAddress: testData.authAccount.pub,
47+
destinationAddress: testData.authAccount2.pub,
48+
authorityAddress: testData.authAccount.pub,
49+
programId: TOKEN_2022_PROGRAM_ID.toString(),
50+
},
51+
});
52+
53+
result[0].programId.equals(TOKEN_2022_PROGRAM_ID).should.be.true();
54+
});
55+
4256
it('Transfer', () => {
4357
const fromAddress = testData.authAccount.pub;
4458
const toAddress = testData.nonceAccount.pub;

modules/sdk-coin-sol/test/unit/transactionBuilder/closeAtaBuilder.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,23 @@ describe('Sol Close ATA Builder', () => {
220220
instruction.params.destinationAddress.should.equal(destinationAddress);
221221
}
222222
});
223+
224+
it('builds mixed legacy SPL and Token-2022 close instructions', async () => {
225+
const txBuilder = closeAtaBuilder();
226+
txBuilder.addCloseAtaInstruction(ataAddress1, destinationAddress, account.pub);
227+
txBuilder.addCloseAtaInstruction(
228+
ataAddress2,
229+
destinationAddress,
230+
account.pub,
231+
'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
232+
);
233+
234+
const tx = await txBuilder.build();
235+
const instructions = tx.toJson().instructionsData;
236+
instructions.length.should.equal(2);
237+
should.not.exist(instructions[0].params.programId);
238+
instructions[1].params.programId.should.equal('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
239+
});
223240
});
224241

225242
describe('Fail', () => {

0 commit comments

Comments
 (0)