Skip to content

Commit 5e6e733

Browse files
Construct Soroban auth and footprint natively, simulate only for fee
Auth entries and ledger footprint are now constructed deterministically in the builder — no untrusted data from external RPC is embedded in the signed transaction. Only the resource fee estimate comes from Soroban simulation (when secondary_url is configured), with a conservative fallback of 100k stroops if unavailable. SorobanResourceFee is exposed in TxInput so it participates in GetFeeLimit() and SetGasFeePriority() correctly.
1 parent 22396e7 commit 5e6e733

4 files changed

Lines changed: 168 additions & 186 deletions

File tree

chain/xlm/builder/builder.go

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
223223
func (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

chain/xlm/client/client.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/cordialsys/crosschain/client/errors"
2525
txinfo "github.com/cordialsys/crosschain/client/tx_info"
2626
xctypes "github.com/cordialsys/crosschain/client/types"
27+
"github.com/sirupsen/logrus"
2728
"github.com/stellar/go-stellar-sdk/xdr"
2829
)
2930

@@ -108,6 +109,14 @@ func (client *Client) FetchTransferInput(ctx context.Context, args xcbuilder.Tra
108109
// Contract addresses require Soroban SAC invocation — skip account existence check.
109110
// The builder will use InvokeHostFunction instead of Payment.
110111
txInput.DestinationFunded = true
112+
// Conservative default resource fee for Soroban SAC transfers.
113+
txInput.SorobanResourceFee = 100_000
114+
// If Soroban RPC is configured, simulate to get an accurate resource fee.
115+
if sorobanUrl := config.SecondaryURL; sorobanUrl != "" {
116+
if err := client.estimateSorobanResourceFee(sorobanUrl, args, txInput); err != nil {
117+
logrus.WithError(err).Warn("soroban simulation failed, using default resource fee")
118+
}
119+
}
111120
} else {
112121
// Check if destination account exists on the network.
113122
// Stellar requires CreateAccount for new accounts instead of Payment.
@@ -173,19 +182,6 @@ func (client *Client) FetchTransferInput(ctx context.Context, args xcbuilder.Tra
173182
}
174183

175184
txInput.TransactionActiveTime = config.TransactionActiveTime
176-
177-
// For Soroban SAC transfers (C-address destinations), simulate the transaction
178-
// to get resource fees and authorization entries.
179-
if common.IsContractAddress(args.GetTo()) {
180-
sorobanUrl := config.SecondaryURL
181-
if sorobanUrl == "" {
182-
return nil, fmt.Errorf("soroban rpc url is required for contract address transfers (set secondary_url in chain config)")
183-
}
184-
if err := client.simulateSorobanTransfer(sorobanUrl, args, txInput); err != nil {
185-
return nil, fmt.Errorf("soroban simulation failed: %w", err)
186-
}
187-
}
188-
189185
return txInput, nil
190186
}
191187

0 commit comments

Comments
 (0)