11import assert from 'assert' ;
22import { TransactionType , Recipient , BuildTransactionError , BaseKey } from '@bitgo/sdk-core' ;
3- import { BaseCoin as CoinConfig } from '@bitgo/statics' ;
3+ import { BaseCoin as CoinConfig , SuiCoin } from '@bitgo/statics' ;
44import { SuiTransaction , SuiTransactionType , TokenTransferProgrammableTransaction } from './iface' ;
55import { Transaction } from './transaction' ;
66import { TransactionBuilder } from './transactionBuilder' ;
@@ -12,10 +12,19 @@ import {
1212 TransactionBlock as ProgrammingTransactionBlockBuilder ,
1313 TransactionArgument ,
1414} from './mystenlab/builder' ;
15+ import BigNumber from 'bignumber.js' ;
1516
1617export class TokenTransferBuilder extends TransactionBuilder < TokenTransferProgrammableTransaction > {
1718 protected _recipients : Recipient [ ] ;
1819 protected _inputObjects : SuiObjectRef [ ] ;
20+ /**
21+ * Balance held in the address balance system for the token being transferred.
22+ * When set, this amount is included in the total available balance.
23+ * At execution time, tx.withdrawal() + 0x2::coin::redeem_funds converts it
24+ * to a Coin<T> that is merged with any coin objects before splitting.
25+ */
26+ protected _fundsInAddressBalance : BigNumber = new BigNumber ( 0 ) ;
27+
1928 constructor ( _coinConfig : Readonly < CoinConfig > ) {
2029 super ( _coinConfig ) ;
2130 this . _transaction = new TokenTransferTransaction ( _coinConfig ) ;
@@ -25,6 +34,14 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
2534 return TransactionType . Send ;
2635 }
2736
37+ /**
38+ * The full coin type string derived from the coin config (e.g. `0xabc::my_token::MY_TOKEN`).
39+ */
40+ private get tokenCoinType ( ) : string {
41+ const config = this . _coinConfig as SuiCoin ;
42+ return `${ config . packageId } ::${ config . module } ::${ config . symbol } ` ;
43+ }
44+
2845 /** @inheritdoc */
2946 validateTransaction ( transaction : TokenTransferTransaction ) : void {
3047 if ( ! transaction . suiTransaction ) {
@@ -78,8 +95,23 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
7895 this . gasData ( txData . gasData ) ;
7996 const recipients = utils . getRecipients ( tx . suiTransaction ) ;
8097 this . send ( recipients ) ;
81- assert ( txData . inputObjects ) ;
82- this . inputObjects ( txData . inputObjects ) ;
98+
99+ // Reconstruct fundsInAddressBalance from BalanceWithdrawal input if present.
100+ // After BCS deserialization inputs are CallArg format: { BalanceWithdrawal: {...} }
101+ // During building they are TransactionBlockInput format: { kind:'Input', value: { BalanceWithdrawal: {...} } }
102+ const withdrawalInput = ( tx . suiTransaction ?. tx ?. inputs as any [ ] ) ?. find (
103+ ( input : any ) =>
104+ ( input !== null && typeof input === 'object' && 'BalanceWithdrawal' in input ) ||
105+ ( input ?. value !== null && typeof input ?. value === 'object' && 'BalanceWithdrawal' in ( input . value ?? { } ) )
106+ ) ;
107+ if ( withdrawalInput ) {
108+ const bw = withdrawalInput . BalanceWithdrawal ?? withdrawalInput . value ?. BalanceWithdrawal ;
109+ this . _fundsInAddressBalance = new BigNumber ( String ( bw . amount ) ) ;
110+ }
111+
112+ if ( txData . inputObjects && txData . inputObjects . length > 0 ) {
113+ this . inputObjects ( txData . inputObjects ) ;
114+ }
83115 }
84116
85117 send ( recipients : Recipient [ ] ) : this {
@@ -89,11 +121,21 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
89121 }
90122
91123 inputObjects ( inputObject : SuiObjectRef [ ] ) : this {
92- this . validateInputObjects ( inputObject ) ;
124+ this . validateInputObjectRefs ( inputObject ) ;
93125 this . _inputObjects = inputObject ;
94126 return this ;
95127 }
96128
129+ /**
130+ * Set the amount of token funds held in the Sui address balance system for this sender.
131+ *
132+ * @param {string } amount - amount in base units held in address balance
133+ */
134+ fundsInAddressBalance ( amount : string ) : this {
135+ this . _fundsInAddressBalance = new BigNumber ( amount ) ;
136+ return this ;
137+ }
138+
97139 /**
98140 * Validates all fields are defined correctly
99141 */
@@ -106,21 +148,37 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
106148 ) ;
107149 assert ( this . _gasData , new BuildTransactionError ( 'gasData is required before building' ) ) ;
108150 this . validateGasData ( this . _gasData ) ;
109- this . validateInputObjects ( this . _inputObjects ) ;
110- }
111151
112- private validateInputObjects ( inputObjects : SuiObjectRef [ ] ) : void {
152+ // Must have at least coin objects OR address balance
113153 assert (
114- inputObjects && inputObjects . length > 0 ,
115- new BuildTransactionError ( 'input objects required before building' )
154+ ( this . _inputObjects && this . _inputObjects . length > 0 ) || this . _fundsInAddressBalance . gt ( 0 ) ,
155+ new BuildTransactionError ( 'input objects or fundsInAddressBalance required before building' )
116156 ) ;
117- inputObjects . forEach ( ( inputObject ) => {
118- this . validateSuiObjectRef ( inputObject , 'input object' ) ;
119- } ) ;
157+ if ( this . _inputObjects && this . _inputObjects . length > 0 ) {
158+ this . validateInputObjectRefs ( this . _inputObjects ) ;
159+ }
160+ }
161+
162+ /** Validates the individual object refs (does not require non-empty array). */
163+ private validateInputObjectRefs ( inputObjects : SuiObjectRef [ ] ) : void {
164+ if ( inputObjects ) {
165+ inputObjects . forEach ( ( inputObject ) => {
166+ this . validateSuiObjectRef ( inputObject , 'input object' ) ;
167+ } ) ;
168+ }
120169 }
121170
122171 /**
123- * Build SuiTransaction
172+ * Build SuiTransaction.
173+ *
174+ * Two build paths:
175+ *
176+ * Path A — coin objects only (fundsInAddressBalance = 0):
177+ * MergeCoins(inputObject[0], [inputObject[1..]]) → SplitCoins → TransferObjects
178+ *
179+ * Path B — coin objects + address balance (or address balance only):
180+ * MoveCall(0x2::coin::redeem_funds, [withdrawal(amount, coinType)]) → Coin<T>
181+ * MergeCoins(inputObject[0] | addrCoin, [rest...]) → SplitCoins → TransferObjects
124182 *
125183 * @return {SuiTransaction<TokenTransferProgrammableTransaction> }
126184 * @protected
@@ -130,9 +188,27 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
130188
131189 const programmableTxBuilder = new ProgrammingTransactionBlockBuilder ( ) ;
132190
133- const inputObjects = this . _inputObjects . map ( ( object ) => programmableTxBuilder . object ( Inputs . ObjectRef ( object ) ) ) ;
134- const mergedObject = inputObjects . shift ( ) as TransactionArgument ;
191+ const inputObjects : TransactionArgument [ ] = ( this . _inputObjects ?? [ ] ) . map ( ( object ) =>
192+ programmableTxBuilder . object ( Inputs . ObjectRef ( object ) )
193+ ) ;
194+
195+ // If address balance is available, withdraw it as Coin<T> and add to the pool
196+ if ( this . _fundsInAddressBalance . gt ( 0 ) ) {
197+ const coinType = this . tokenCoinType ;
198+ const [ addrCoin ] = programmableTxBuilder . moveCall ( {
199+ target : '0x2::coin::redeem_funds' ,
200+ typeArguments : [ coinType ] ,
201+ arguments : [
202+ programmableTxBuilder . withdrawal ( {
203+ amount : BigInt ( this . _fundsInAddressBalance . toFixed ( ) ) ,
204+ type : coinType ,
205+ } ) ,
206+ ] ,
207+ } ) ;
208+ inputObjects . push ( addrCoin ) ;
209+ }
135210
211+ const mergedObject = inputObjects . shift ( ) as TransactionArgument ;
136212 if ( inputObjects . length > 0 ) {
137213 programmableTxBuilder . mergeCoins ( mergedObject , inputObjects ) ;
138214 }
0 commit comments