@@ -5,13 +5,14 @@ import { SuiTransaction, SuiTransactionType, TokenTransferProgrammableTransactio
55import { Transaction } from './transaction' ;
66import { TransactionBuilder } from './transactionBuilder' ;
77import { TokenTransferTransaction } from './tokenTransferTransaction' ;
8- import { SuiObjectRef } from './mystenlab/types' ;
8+ import { normalizeSuiAddress , SuiObjectRef } from './mystenlab/types' ;
99import utils from './utils' ;
1010import {
1111 Inputs ,
1212 TransactionBlock as ProgrammingTransactionBlockBuilder ,
1313 TransactionArgument ,
1414} from './mystenlab/builder' ;
15+ import { TypeTagSerializer } from './mystenlab/txn-data-serializers/type-tag-serializer' ;
1516import BigNumber from 'bignumber.js' ;
1617
1718export class TokenTransferBuilder extends TransactionBuilder < TokenTransferProgrammableTransaction > {
@@ -25,6 +26,15 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
2526 */
2627 protected _fundsInAddressBalance : BigNumber = new BigNumber ( 0 ) ;
2728
29+ /**
30+ * Coin type recovered from a deserialized transaction (the `redeem_funds` type argument /
31+ * `BalanceWithdrawal` type). When set it takes precedence over the coin type derived from the
32+ * coin config, so re-building a transaction (e.g. to attach a TSS signature before broadcast)
33+ * preserves the exact coin type it was signed with even when the builder was constructed with
34+ * the parent chain config rather than the token config.
35+ */
36+ protected _tokenCoinTypeOverride ?: string ;
37+
2838 constructor ( _coinConfig : Readonly < CoinConfig > ) {
2939 super ( _coinConfig ) ;
3040 this . _transaction = new TokenTransferTransaction ( _coinConfig ) ;
@@ -38,6 +48,9 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
3848 * The full coin type string derived from the coin config (e.g. `0xabc::my_token::MY_TOKEN`).
3949 */
4050 private get tokenCoinType ( ) : string {
51+ if ( this . _tokenCoinTypeOverride ) {
52+ return this . _tokenCoinTypeOverride ;
53+ }
4154 const config = this . _coinConfig as SuiCoin ;
4255 return `${ config . packageId } ::${ config . module } ::${ config . symbol } ` ;
4356 }
@@ -113,6 +126,19 @@ export class TokenTransferBuilder extends TransactionBuilder<TokenTransferProgra
113126 if ( withdrawalInput ) {
114127 const bw = withdrawalInput . BalanceWithdrawal ?? withdrawalInput . value ?. BalanceWithdrawal ;
115128 this . _fundsInAddressBalance = new BigNumber ( String ( bw . reservation ?. MaxAmountU64 ?? bw . amount ) ) ;
129+ // Recover the coin type from the withdrawal's TypeTag so that re-building uses the exact
130+ // coin type the transaction was created (and signed) with. Without this, buildSuiTransaction
131+ // would re-derive the coin type from this._coinConfig, which is the parent chain config
132+ // (no packageId/module/symbol) when the builder is constructed via the chain — producing an
133+ // `undefined::undefined::undefined` coin type and a signature that fails on-chain verification.
134+ const withdrawalTypeTag = bw ?. typeArg ?. Balance ;
135+ if ( withdrawalTypeTag ) {
136+ // BCS decodes the struct address without the `0x` prefix; normalize it so the recovered
137+ // coin type matches the config-derived form (`0x<addr>::module::name`). The serialized
138+ // bytes are identical either way since parseFromStr re-normalizes the address on encode.
139+ const [ address , ...rest ] = TypeTagSerializer . tagToString ( withdrawalTypeTag ) . split ( '::' ) ;
140+ this . _tokenCoinTypeOverride = [ normalizeSuiAddress ( address ) , ...rest ] . join ( '::' ) ;
141+ }
116142 }
117143
118144 if ( txData . inputObjects && txData . inputObjects . length > 0 ) {
0 commit comments