@@ -219,7 +219,7 @@ func (builder TxBuilder) Transfer(args xcbuilder.TransferArgs, input xc.TxInput)
219219
220220// buildSACTransfer builds an InvokeHostFunction operation that calls the SAC transfer function.
221221// The SAC transfer signature is: transfer(from: Address, to: Address, amount: i128)
222- // The SAC contract is derived from the token asset code + issuer .
222+ // All Soroban data (auth, footprint, resources) is constructed natively — no simulation RPC needed .
223223func (builder TxBuilder ) buildSACTransfer (args xcbuilder.TransferArgs , txInput * TxInput , from xc.Address , to xc.Address ) (* xdr.OperationBody , * xdr.SorobanTransactionData , error ) {
224224 // Derive the SAC contract address from the token asset
225225 contract , ok := args .GetContract ()
@@ -266,32 +266,99 @@ func (builder TxBuilder) buildSACTransfer(args xcbuilder.TransferArgs, txInput *
266266 return nil , nil , fmt .Errorf ("failed to create host function: %w" , err )
267267 }
268268
269- // Parse Soroban auth entries from simulation
270- var authEntries []xdr.SorobanAuthorizationEntry
271- for _ , authBytes := range txInput .SorobanAuth {
272- var entry xdr.SorobanAuthorizationEntry
273- if err := entry .UnmarshalBinary (authBytes ); err != nil {
274- return nil , nil , fmt .Errorf ("failed to unmarshal soroban auth entry: %w" , err )
275- }
276- authEntries = append (authEntries , entry )
269+ // Construct auth entry natively.
270+ // For SAC transfer where sender = transaction source, credentials are SOURCE_ACCOUNT
271+ // and the invocation is the transfer call with no sub-invocations.
272+ contractFn , err := xdr .NewSorobanAuthorizedFunction (
273+ xdr .SorobanAuthorizedFunctionTypeSorobanAuthorizedFunctionTypeContractFn ,
274+ invokeArgs ,
275+ )
276+ if err != nil {
277+ return nil , nil , fmt .Errorf ("failed to create authorized function: %w" , err )
278+ }
279+ authEntry := xdr.SorobanAuthorizationEntry {
280+ Credentials : xdr.SorobanCredentials {
281+ Type : xdr .SorobanCredentialsTypeSorobanCredentialsSourceAccount ,
282+ },
283+ RootInvocation : xdr.SorobanAuthorizedInvocation {
284+ Function : contractFn ,
285+ SubInvocations : nil ,
286+ },
277287 }
278288
279289 invokeOp := xdr.InvokeHostFunctionOp {
280290 HostFunction : hostFn ,
281- Auth : authEntries ,
291+ Auth : []xdr. SorobanAuthorizationEntry { authEntry } ,
282292 }
283293
284294 opBody , err := xdr .NewOperationBody (xdr .OperationTypeInvokeHostFunction , invokeOp )
285295 if err != nil {
286296 return nil , nil , fmt .Errorf ("failed to create invoke host function operation: %w" , err )
287297 }
288298
289- // Parse Soroban transaction data from simulation
290- var sorobanData xdr.SorobanTransactionData
291- if len (txInput .SorobanData ) > 0 {
292- if err := sorobanData .UnmarshalBinary (txInput .SorobanData ); err != nil {
293- return nil , nil , fmt .Errorf ("failed to unmarshal soroban transaction data: %w" , err )
294- }
299+ // Construct the ledger footprint natively.
300+ // SAC transfer touches:
301+ // ReadOnly: SAC contract instance
302+ // ReadWrite: sender's trustline (G-addr balance), receiver's contract data (C-addr balance)
303+ fromAccountId , err := xdr .AddressToAccountId (string (from ))
304+ if err != nil {
305+ return nil , nil , fmt .Errorf ("failed to parse from account: %w" , err )
306+ }
307+ toContractAddr , err := common .ScAddressFromString (string (to ))
308+ if err != nil {
309+ return nil , nil , fmt .Errorf ("failed to parse to contract address: %w" , err )
310+ }
311+
312+ // SAC contract instance key
313+ sacInstanceKey := xdr.LedgerKey {
314+ Type : xdr .LedgerEntryTypeContractData ,
315+ ContractData : & xdr.LedgerKeyContractData {
316+ Contract : contractAddr ,
317+ Key : xdr.ScVal {Type : xdr .ScValTypeScvLedgerKeyContractInstance },
318+ Durability : xdr .ContractDataDurabilityPersistent ,
319+ },
320+ }
321+
322+ // Sender's classic trustline (SAC uses TrustLine entries for G-address balances)
323+ senderTrustlineKey := xdr.LedgerKey {
324+ Type : xdr .LedgerEntryTypeTrustline ,
325+ TrustLine : & xdr.LedgerKeyTrustLine {
326+ AccountId : fromAccountId ,
327+ Asset : xdrAsset .ToTrustLineAsset (),
328+ },
329+ }
330+
331+ // Receiver's contract balance (SAC uses ContractData for C-address balances)
332+ // Key format: Vec[Symbol("Balance"), Address(to)]
333+ balanceSym := common .ScValSymbol ("Balance" )
334+ toAddrScVal := xdr.ScVal {Type : xdr .ScValTypeScvAddress , Address : & toContractAddr }
335+ balanceVec := xdr.ScVec {balanceSym , toAddrScVal }
336+ balanceVecPtr := & balanceVec
337+ receiverBalanceKey := xdr.LedgerKey {
338+ Type : xdr .LedgerEntryTypeContractData ,
339+ ContractData : & xdr.LedgerKeyContractData {
340+ Contract : contractAddr ,
341+ Key : xdr.ScVal {
342+ Type : xdr .ScValTypeScvVec ,
343+ Vec : & balanceVecPtr ,
344+ },
345+ Durability : xdr .ContractDataDurabilityPersistent ,
346+ },
347+ }
348+
349+ // Conservative resource limits for SAC transfer.
350+ // These are upper bounds — unused portions of refundable fees are returned.
351+ sorobanData := xdr.SorobanTransactionData {
352+ Resources : xdr.SorobanResources {
353+ Footprint : xdr.LedgerFootprint {
354+ ReadOnly : []xdr.LedgerKey {sacInstanceKey },
355+ ReadWrite : []xdr.LedgerKey {senderTrustlineKey , receiverBalanceKey },
356+ },
357+ Instructions : 500_000 ,
358+ DiskReadBytes : 2000 ,
359+ WriteBytes : 500 ,
360+ },
361+ ResourceFee : xdr .Int64 (txInput .SorobanResourceFee ),
295362 }
296363
297364 return & opBody , & sorobanData , nil
0 commit comments