@@ -72,6 +72,8 @@ import (
7272
7373 "github.com/ethereum/go-ethereum/crypto"
7474 "github.com/gagliardetto/solana-go"
75+ addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table"
76+ "github.com/gagliardetto/solana-go/rpc"
7577 "github.com/rs/zerolog"
7678
7779 "github.com/pushchain/push-chain-node/universalClient/chains/common"
@@ -102,6 +104,8 @@ type TxBuilder struct {
102104 gatewayAddress solana.PublicKey
103105 nodeHome string
104106 logger zerolog.Logger
107+ protocolALT solana.PublicKey // Protocol ALT pubkey (zero if not configured)
108+ tokenALTs map [solana.PublicKey ]solana.PublicKey // mint pubkey → token ALT pubkey
105109}
106110
107111// NewTxBuilder creates a new Solana transaction builder.
@@ -113,6 +117,7 @@ func NewTxBuilder(
113117 gatewayAddress string ,
114118 nodeHome string ,
115119 logger zerolog.Logger ,
120+ chainConfig * config.ChainSpecificConfig ,
116121) (* TxBuilder , error ) {
117122 if rpcClient == nil {
118123 return nil , fmt .Errorf ("rpcClient is required" )
@@ -129,13 +134,45 @@ func NewTxBuilder(
129134 return nil , fmt .Errorf ("invalid gateway address: %w" , err )
130135 }
131136
132- return & TxBuilder {
137+ tb := & TxBuilder {
133138 rpcClient : rpcClient ,
134139 chainID : chainID ,
135140 gatewayAddress : addr ,
136141 nodeHome : nodeHome ,
137142 logger : logger .With ().Str ("component" , "svm_tx_builder" ).Str ("chain" , chainID ).Logger (),
138- }, nil
143+ tokenALTs : make (map [solana.PublicKey ]solana.PublicKey ),
144+ }
145+
146+ // Parse ALT config if provided
147+ if chainConfig != nil {
148+ if chainConfig .ProtocolALT != "" {
149+ protocolALT , err := solana .PublicKeyFromBase58 (chainConfig .ProtocolALT )
150+ if err != nil {
151+ tb .logger .Warn ().Err (err ).Str ("address" , chainConfig .ProtocolALT ).Msg ("invalid protocol ALT address, skipping" )
152+ } else {
153+ tb .protocolALT = protocolALT
154+ tb .logger .Info ().Str ("protocol_alt" , chainConfig .ProtocolALT ).Msg ("configured protocol ALT" )
155+ }
156+ }
157+ for mint , altAddr := range chainConfig .TokenALTs {
158+ mintPubkey , err := solana .PublicKeyFromBase58 (mint )
159+ if err != nil {
160+ tb .logger .Warn ().Err (err ).Str ("mint" , mint ).Msg ("invalid token ALT mint address, skipping" )
161+ continue
162+ }
163+ altPubkey , err := solana .PublicKeyFromBase58 (altAddr )
164+ if err != nil {
165+ tb .logger .Warn ().Err (err ).Str ("alt" , altAddr ).Msg ("invalid token ALT address, skipping" )
166+ continue
167+ }
168+ tb .tokenALTs [mintPubkey ] = altPubkey
169+ }
170+ if len (tb .tokenALTs ) > 0 {
171+ tb .logger .Info ().Int ("count" , len (tb .tokenALTs )).Msg ("configured token ALTs" )
172+ }
173+ }
174+
175+ return tb , nil
139176}
140177
141178// =============================================================================
@@ -464,6 +501,49 @@ func (tb *TxBuilder) BroadcastOutboundSigningRequest(
464501 return txHash , nil
465502}
466503
504+ // fetchAddressTables fetches Address Lookup Table state for V0 transactions.
505+ // Always includes the protocol ALT (if configured). For SPL tokens, also includes
506+ // the token-specific ALT for the given mint (if configured).
507+ // Returns an empty map if no ALTs are configured (transaction falls back to legacy format).
508+ func (tb * TxBuilder ) fetchAddressTables (ctx context.Context , mintPubkey solana.PublicKey , isNative bool ) (map [solana.PublicKey ]solana.PublicKeySlice , error ) {
509+ addressTables := make (map [solana.PublicKey ]solana.PublicKeySlice )
510+
511+ // Collect ALT pubkeys to fetch
512+ var altsToFetch []solana.PublicKey
513+
514+ if ! tb .protocolALT .IsZero () {
515+ altsToFetch = append (altsToFetch , tb .protocolALT )
516+ }
517+
518+ if ! isNative {
519+ if tokenALT , ok := tb .tokenALTs [mintPubkey ]; ok {
520+ altsToFetch = append (altsToFetch , tokenALT )
521+ }
522+ }
523+
524+ if len (altsToFetch ) == 0 {
525+ return addressTables , nil
526+ }
527+
528+ // Fetch each ALT state from chain using the RPC client's failover mechanism
529+ for _ , altPubkey := range altsToFetch {
530+ altKey := altPubkey // capture for closure
531+ var altState * addresslookuptable.AddressLookupTableState
532+ err := tb .rpcClient .executeWithFailover (ctx , "getAddressLookupTable" , func (client * rpc.Client ) error {
533+ var fetchErr error
534+ altState , fetchErr = addresslookuptable .GetAddressLookupTable (ctx , client , altKey )
535+ return fetchErr
536+ })
537+ if err != nil {
538+ return nil , fmt .Errorf ("failed to fetch ALT %s: %w" , altKey .String (), err )
539+ }
540+ addressTables [altKey ] = altState .Addresses
541+ }
542+
543+ tb .logger .Debug ().Int ("alt_count" , len (addressTables )).Msg ("fetched address lookup tables for V0 transaction" )
544+ return addressTables , nil
545+ }
546+
467547// BuildOutboundTransaction assembles a complete signed Solana transaction from the
468548// TSS signature and event data, without broadcasting. Returns the transaction and
469549// the instruction ID for logging. Use this for simulation or inspection.
@@ -763,11 +843,14 @@ func (tb *TxBuilder) BuildOutboundTransaction(
763843 return nil , 0 , fmt .Errorf ("failed to get recent blockhash: %w" , err )
764844 }
765845
766- tx , err := solana .NewTransaction (
767- instructions ,
768- recentBlockhash ,
769- solana .TransactionPayer (relayerKeypair .PublicKey ()),
770- )
846+ opts := []solana.TransactionOption {solana .TransactionPayer (relayerKeypair .PublicKey ())}
847+ addressTables , err := tb .fetchAddressTables (ctx , mintPubkey , isNative )
848+ if err != nil {
849+ tb .logger .Warn ().Err (err ).Msg ("failed to fetch ALTs, falling back to legacy tx" )
850+ } else if len (addressTables ) > 0 {
851+ opts = append (opts , solana .TransactionAddressTables (addressTables ))
852+ }
853+ tx , err := solana .NewTransaction (instructions , recentBlockhash , opts ... )
771854 if err != nil {
772855 return nil , 0 , fmt .Errorf ("failed to create transaction: %w" , err )
773856 }
0 commit comments