@@ -7,10 +7,20 @@ import { Transaction } from './transaction';
77import { TransactionBuilder } from './transactionBuilder' ;
88import { validateAddress } from './utils' ;
99
10+ type CloseAtaApiMode = 'single' | 'bulk' ;
11+
12+ const MIX_API_ERROR_MESSAGE =
13+ 'Cannot mix single-ATA API (accountAddress/destinationAddress/authorityAddress) with bulk-ATA API (addCloseAtaInstruction)' ;
14+
1015export class CloseAtaBuilder extends TransactionBuilder {
11- protected _accountAddress : string ;
12- protected _destinationAddress : string ;
13- protected _authorityAddress : string ;
16+ // Unified storage for all close entries (single or bulk)
17+ protected _closeAtaEntries : { accountAddress : string ; destinationAddress : string ; authorityAddress : string } [ ] = [ ] ;
18+
19+ // Which API has been used on this builder instance. Locks in on first call so we can
20+ // reject attempts to mix the legacy single-ATA setters with the bulk addCloseAtaInstruction().
21+ // Remains undefined after initBuilder() so a parsed transaction can still be modified via
22+ // either API path.
23+ private _apiMode : CloseAtaApiMode | undefined = undefined ;
1424
1525 constructor ( _coinConfig : Readonly < CoinConfig > ) {
1626 super ( _coinConfig ) ;
@@ -21,21 +31,90 @@ export class CloseAtaBuilder extends TransactionBuilder {
2131 return TransactionType . CloseAssociatedTokenAccount ;
2232 }
2333
34+ /**
35+ * Sets the ATA account address to close (single-ATA API, backward compatible).
36+ * Cannot be mixed with addCloseAtaInstruction().
37+ */
2438 accountAddress ( accountAddress : string ) : this {
39+ this . _assertSingleAtaApiUsable ( ) ;
2540 validateAddress ( accountAddress , 'accountAddress' ) ;
26- this . _accountAddress = accountAddress ;
41+ this . _apiMode = 'single' ;
42+ this . _ensureSingleEntry ( ) ;
43+ this . _closeAtaEntries [ 0 ] . accountAddress = accountAddress ;
2744 return this ;
2845 }
2946
47+ /**
48+ * Sets the destination address for rent SOL (single-ATA API, backward compatible).
49+ * Cannot be mixed with addCloseAtaInstruction().
50+ */
3051 destinationAddress ( destinationAddress : string ) : this {
52+ this . _assertSingleAtaApiUsable ( ) ;
3153 validateAddress ( destinationAddress , 'destinationAddress' ) ;
32- this . _destinationAddress = destinationAddress ;
54+ this . _apiMode = 'single' ;
55+ this . _ensureSingleEntry ( ) ;
56+ this . _closeAtaEntries [ 0 ] . destinationAddress = destinationAddress ;
3357 return this ;
3458 }
3559
60+ /**
61+ * Sets the authority address / ATA owner (single-ATA API, backward compatible).
62+ * Cannot be mixed with addCloseAtaInstruction().
63+ */
3664 authorityAddress ( authorityAddress : string ) : this {
65+ this . _assertSingleAtaApiUsable ( ) ;
3766 validateAddress ( authorityAddress , 'authorityAddress' ) ;
38- this . _authorityAddress = authorityAddress ;
67+ this . _apiMode = 'single' ;
68+ this . _ensureSingleEntry ( ) ;
69+ this . _closeAtaEntries [ 0 ] . authorityAddress = authorityAddress ;
70+ return this ;
71+ }
72+
73+ /**
74+ * Throws if the bulk-ATA API has already been used on this builder.
75+ */
76+ private _assertSingleAtaApiUsable ( ) : void {
77+ if ( this . _apiMode === 'bulk' ) {
78+ throw new BuildTransactionError ( MIX_API_ERROR_MESSAGE ) ;
79+ }
80+ }
81+
82+ /**
83+ * Ensures a single entry exists in _closeAtaEntries for the legacy API.
84+ */
85+ private _ensureSingleEntry ( ) : void {
86+ if ( this . _closeAtaEntries . length === 0 ) {
87+ this . _closeAtaEntries . push ( { accountAddress : '' , destinationAddress : '' , authorityAddress : '' } ) ;
88+ }
89+ }
90+
91+ /**
92+ * Add an ATA to close in this transaction (for bulk closure).
93+ * Cannot be mixed with the single-ATA API (accountAddress/destinationAddress/authorityAddress).
94+ *
95+ * @param {string } accountAddress - the ATA address to close
96+ * @param {string } destinationAddress - where rent SOL goes (root wallet address)
97+ * @param {string } authorityAddress - ATA owner who must sign
98+ */
99+ addCloseAtaInstruction ( accountAddress : string , destinationAddress : string , authorityAddress : string ) : this {
100+ if ( this . _apiMode === 'single' ) {
101+ throw new BuildTransactionError ( MIX_API_ERROR_MESSAGE ) ;
102+ }
103+
104+ validateAddress ( accountAddress , 'accountAddress' ) ;
105+ validateAddress ( destinationAddress , 'destinationAddress' ) ;
106+ validateAddress ( authorityAddress , 'authorityAddress' ) ;
107+
108+ if ( accountAddress === destinationAddress ) {
109+ throw new BuildTransactionError ( 'Account address to close cannot be the same as the destination address' ) ;
110+ }
111+
112+ if ( this . _closeAtaEntries . some ( ( entry ) => entry . accountAddress === accountAddress ) ) {
113+ throw new BuildTransactionError ( 'Duplicate ATA address: ' + accountAddress ) ;
114+ }
115+
116+ this . _apiMode = 'bulk' ;
117+ this . _closeAtaEntries . push ( { accountAddress, destinationAddress, authorityAddress } ) ;
39118 return this ;
40119 }
41120
@@ -45,33 +124,39 @@ export class CloseAtaBuilder extends TransactionBuilder {
45124 for ( const instruction of this . _instructionsData ) {
46125 if ( instruction . type === InstructionBuilderTypes . CloseAssociatedTokenAccount ) {
47126 const ataCloseInstruction : AtaClose = instruction ;
48- this . accountAddress ( ataCloseInstruction . params . accountAddress ) ;
49- this . destinationAddress ( ataCloseInstruction . params . destinationAddress ) ;
50- this . authorityAddress ( ataCloseInstruction . params . authorityAddress ) ;
127+ this . _closeAtaEntries . push ( {
128+ accountAddress : ataCloseInstruction . params . accountAddress ,
129+ destinationAddress : ataCloseInstruction . params . destinationAddress ,
130+ authorityAddress : ataCloseInstruction . params . authorityAddress ,
131+ } ) ;
51132 }
52133 }
53134 }
54135
55136 /** @inheritdoc */
56137 protected async buildImplementation ( ) : Promise < Transaction > {
57- assert ( this . _accountAddress , 'Account Address must be set before building the transaction' ) ;
58- assert ( this . _destinationAddress , 'Destination Address must be set before building the transaction' ) ;
59- assert ( this . _authorityAddress , 'Authority Address must be set before building the transaction' ) ;
138+ assert ( this . _closeAtaEntries . length > 0 , 'At least one ATA must be specified before building the transaction' ) ;
60139
61- if ( this . _accountAddress === this . _destinationAddress ) {
62- throw new BuildTransactionError ( 'Account address to close cannot be the same as the destination address' ) ;
63- }
140+ for ( const entry of this . _closeAtaEntries ) {
141+ assert ( entry . accountAddress , 'Account Address must be set before building the transaction' ) ;
142+ assert ( entry . destinationAddress , 'Destination Address must be set before building the transaction' ) ;
143+ assert ( entry . authorityAddress , 'Authority Address must be set before building the transaction' ) ;
64144
65- const closeAssociatedTokenAccountData : AtaClose = {
66- type : InstructionBuilderTypes . CloseAssociatedTokenAccount ,
67- params : {
68- accountAddress : this . _accountAddress ,
69- destinationAddress : this . _destinationAddress ,
70- authorityAddress : this . _authorityAddress ,
71- } ,
72- } ;
145+ if ( entry . accountAddress === entry . destinationAddress ) {
146+ throw new BuildTransactionError ( 'Account address to close cannot be the same as the destination address' ) ;
147+ }
148+ }
73149
74- this . _instructionsData = [ closeAssociatedTokenAccountData ] ;
150+ this . _instructionsData = this . _closeAtaEntries . map (
151+ ( entry ) : AtaClose => ( {
152+ type : InstructionBuilderTypes . CloseAssociatedTokenAccount ,
153+ params : {
154+ accountAddress : entry . accountAddress ,
155+ destinationAddress : entry . destinationAddress ,
156+ authorityAddress : entry . authorityAddress ,
157+ } ,
158+ } )
159+ ) ;
75160
76161 return await super . buildImplementation ( ) ;
77162 }
0 commit comments