Skip to content

Commit ef5482a

Browse files
Use SDK prepareTransaction and pollTransaction for transaction assembly and polling
1 parent 7121b5c commit ef5482a

2 files changed

Lines changed: 83 additions & 128 deletions

File tree

stellar-sdk/src/commonMain/kotlin/com/soneso/stellar/sdk/smartaccount/oz/OZMultiSignerManager.kt

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -500,25 +500,15 @@ class OZMultiSignerManager internal constructor(
500500
throw TransactionException.simulationFailed("Re-simulation error: ${resignedSimulation.error}")
501501
}
502502

503-
val transactionData = resignedSimulation.parseTransactionData()
504-
?: throw TransactionException.submissionFailed(
505-
"Failed to get transaction data from re-simulation"
506-
)
507-
508-
val minResourceFee = resignedSimulation.minResourceFee
509-
?: throw TransactionException.submissionFailed(
510-
"Failed to get min resource fee from re-simulation"
511-
)
512-
513-
// STEP 8: Submit via the same Mode 1 / Mode 2 routing as single-signer transfer.
503+
// STEP 8: Assemble and submit via the same Mode 1 / Mode 2 routing as single-signer.
514504
// Mode 1 (default): relayer receives hostFunction + authEntries and builds the envelope.
515505
// Mode 2 (fallback): used only when source_account auth entries are present.
506+
// prepareTransaction applies resource fees, footprint, and soroban data from simulation.
516507
return kit.transactionOperations.submitMultiSignerTransaction(
517508
hostFunction = hostFunction,
518509
signedAuthEntries = signedAuthEntries,
519-
transactionData = transactionData,
520-
minResourceFee = minResourceFee,
521-
deployerAccount = refetchedDeployerAccount
510+
signedTransaction = signedTransaction,
511+
simulation = resignedSimulation
522512
)
523513
}
524514

stellar-sdk/src/commonMain/kotlin/com/soneso/stellar/sdk/smartaccount/oz/OZTransactionOperations.kt

Lines changed: 79 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import com.soneso.stellar.sdk.FriendBot
1818
import com.soneso.stellar.sdk.Network
1919
import com.soneso.stellar.sdk.Transaction
2020
import com.soneso.stellar.sdk.TransactionBuilder
21-
import com.soneso.stellar.sdk.TransactionBuilderAccount
2221
import com.soneso.stellar.sdk.rpc.responses.GetTransactionStatus
22+
import com.soneso.stellar.sdk.rpc.responses.SendTransactionStatus
23+
import com.soneso.stellar.sdk.rpc.responses.SimulateTransactionResponse
2324
import com.soneso.stellar.sdk.xdr.HostFunctionXdr
2425
import com.soneso.stellar.sdk.xdr.Int64Xdr
2526
import com.soneso.stellar.sdk.xdr.InvokeContractArgsXdr
@@ -28,7 +29,6 @@ import com.soneso.stellar.sdk.xdr.SCValXdr
2829
import com.soneso.stellar.sdk.xdr.SorobanAddressCredentialsXdr
2930
import com.soneso.stellar.sdk.xdr.SorobanAuthorizationEntryXdr
3031
import com.soneso.stellar.sdk.xdr.SorobanCredentialsXdr
31-
import com.soneso.stellar.sdk.xdr.SorobanTransactionDataXdr
3232
import com.soneso.stellar.sdk.xdr.Uint32Xdr
3333
import com.soneso.stellar.sdk.xdr.XdrReader
3434
import com.soneso.stellar.sdk.xdr.XdrWriter
@@ -463,10 +463,14 @@ class OZTransactionOperations internal constructor(
463463
)
464464
}
465465

466-
// (d) Resolve key data from storage or on-chain context rules
466+
// (d) Resolve key data from storage or on-chain context rules.
467+
// Storage lookup is an optimization; fall through to on-chain lookup on any failure.
467468
val keyData: ByteArray
468-
val storage = kit.getStorage()
469-
val stored = storage.get(credentialId)
469+
val stored = try {
470+
kit.getStorage().get(credentialId)
471+
} catch (_: Exception) {
472+
null
473+
}
470474
if (stored != null) {
471475
keyData = stored.publicKey + credIdBytes
472476
} else {
@@ -532,9 +536,12 @@ class OZTransactionOperations internal constructor(
532536
)
533537
)
534538

535-
// STEP 9: Rebuild transaction with signed auth entries
539+
// STEP 9: Rebuild transaction with signed auth entries.
540+
// Re-fetch deployer account to get the correct on-chain sequence number.
541+
// The initial fetch (step 2) was consumed by TransactionBuilder.build() in step 3.
542+
val refreshedDeployerAccount = kit.sorobanServer.getAccount(deployer.getAccountId())
536543
val signedOperation = InvokeHostFunctionOperation(hostFunction, signedAuthEntries)
537-
val signedTransaction = TransactionBuilder(deployerAccount, Network(kit.config.networkPassphrase))
544+
val signedTransaction = TransactionBuilder(refreshedDeployerAccount, Network(kit.config.networkPassphrase))
538545
.setBaseFee(100)
539546
.addOperation(signedOperation)
540547
.addMemo(MemoNone)
@@ -548,25 +555,9 @@ class OZTransactionOperations internal constructor(
548555
throw TransactionException.simulationFailed("Re-simulation error: ${reSimulation.error}")
549556
}
550557

551-
// STEP 11: Assemble transaction from re-simulation
552-
val transactionData = reSimulation.parseTransactionData()
553-
?: throw TransactionException.submissionFailed(
554-
"Failed to get transaction data from re-simulation"
555-
)
556-
557-
val minResourceFee = reSimulation.minResourceFee
558-
?: throw TransactionException.submissionFailed(
559-
"Failed to get min resource fee from re-simulation"
560-
)
561-
562-
// Rebuild transaction with Soroban data and resource fee
563-
val finalTransaction = TransactionBuilder(deployerAccount, Network(kit.config.networkPassphrase))
564-
.setBaseFee(100 + minResourceFee)
565-
.addOperation(signedOperation)
566-
.addMemo(MemoNone)
567-
.setTimeout(300)
568-
.setSorobanData(transactionData)
569-
.build()
558+
// STEP 11: Assemble transaction from re-simulation.
559+
// prepareTransaction applies resource fees, footprint, and soroban data from simulation.
560+
val finalTransaction = kit.sorobanServer.prepareTransaction(signedTransaction, reSimulation)
570561

571562
// STEP 12: Determine submission method and submit
572563
val submissionMethod = getSubmissionMethod(forceMethod)
@@ -596,33 +587,22 @@ class OZTransactionOperations internal constructor(
596587
*
597588
* @param hostFunction The host function for the token transfer
598589
* @param signedAuthEntries Auth entries with all collected signatures
599-
* @param transactionData Soroban transaction data from re-simulation
600-
* @param minResourceFee Minimum resource fee from re-simulation
601-
* @param deployerAccount The deployer account (already fetched by the caller to avoid
602-
* a redundant network round-trip and potential sequence number drift)
590+
* @param signedTransaction The transaction with signed auth entries (pre-assembly)
591+
* @param simulation The re-simulation response for transaction assembly
603592
* @return TransactionResult with submission outcome
604593
* @throws SmartAccountException if submission fails
605594
*/
606595
internal suspend fun submitMultiSignerTransaction(
607596
hostFunction: HostFunctionXdr,
608597
signedAuthEntries: List<SorobanAuthorizationEntryXdr>,
609-
transactionData: SorobanTransactionDataXdr,
610-
minResourceFee: Long,
611-
deployerAccount: TransactionBuilderAccount
598+
signedTransaction: Transaction,
599+
simulation: SimulateTransactionResponse
612600
): TransactionResult {
613601
val deployer = kit.getDeployer()
614602

615-
val signedOperation = InvokeHostFunctionOperation(hostFunction, signedAuthEntries)
616-
val finalTransaction = TransactionBuilder(
617-
deployerAccount,
618-
Network(kit.config.networkPassphrase)
619-
)
620-
.setBaseFee(100 + minResourceFee)
621-
.addOperation(signedOperation)
622-
.addMemo(MemoNone)
623-
.setTimeout(300)
624-
.setSorobanData(transactionData)
625-
.build()
603+
// Assemble the transaction using the SDK's prepareTransaction.
604+
// This correctly applies resource fees, footprint, and soroban data from simulation.
605+
val finalTransaction = kit.sorobanServer.prepareTransaction(signedTransaction, simulation)
626606

627607
// submitMultiSignerTransaction intentionally uses relayerClient presence directly
628608
// rather than getSubmissionMethod(), because multi-signer transfers do not support
@@ -798,8 +778,8 @@ class OZTransactionOperations internal constructor(
798778
// Extract auth entries from simulation
799779
val simulatedAuthEntries = simulation.results?.firstOrNull()?.parseAuth() ?: emptyList()
800780

801-
// STEP 8: Convert source_account auth entries to Address credentials
802-
// This allows the Relayer to use its own channel accounts for fee sponsoring
781+
// STEP 8: Convert source_account auth entries to Address credentials.
782+
// This allows the Relayer to use its own channel accounts for fee sponsoring.
803783
val latestLedger = kit.sorobanServer.getLatestLedger()
804784
val expirationLedger = latestLedger.sequence.toUInt() + OZConstants.AUTH_ENTRY_EXPIRATION_BUFFER.toUInt()
805785

@@ -809,10 +789,10 @@ class OZTransactionOperations internal constructor(
809789
expirationLedger = expirationLedger
810790
)
811791

812-
// STEP 9: Refresh temp account for re-simulation
792+
// STEP 9: Rebuild transaction with signed auth entries and re-simulate.
793+
// Use a fresh account fetch to avoid sequence number issues.
813794
val tempAccountRefresh = kit.sorobanServer.getAccount(tempKeypair.getAccountId())
814795

815-
// Build transaction with signed auth entries
816796
val signedOperation = InvokeHostFunctionOperation(hostFunction, signedAuthEntries)
817797
val signedTransaction = TransactionBuilder(tempAccountRefresh, Network(kit.config.networkPassphrase))
818798
.setBaseFee(100)
@@ -821,40 +801,22 @@ class OZTransactionOperations internal constructor(
821801
.setTimeout(300)
822802
.build()
823803

824-
// STEP 10: Re-simulate with signed auth entries to get correct resource estimates
825804
val reSimulation = kit.sorobanServer.simulateTransaction(signedTransaction)
826805

827806
if (reSimulation.error != null) {
828807
throw TransactionException.simulationFailed("Re-simulation error: ${reSimulation.error}")
829808
}
830809

831-
// Assemble transaction from re-simulation
832-
val transactionData = reSimulation.parseTransactionData()
833-
?: throw TransactionException.submissionFailed(
834-
"Failed to get transaction data from re-simulation"
835-
)
836-
837-
val minResourceFee = reSimulation.minResourceFee
838-
?: throw TransactionException.submissionFailed(
839-
"Failed to get min resource fee from re-simulation"
840-
)
841-
842-
// Rebuild transaction with signed auth entries and Soroban data
843-
val finalOperation = InvokeHostFunctionOperation(hostFunction, signedAuthEntries)
844-
val finalTransaction = TransactionBuilder(tempAccountRefresh, Network(kit.config.networkPassphrase))
845-
.setBaseFee(100 + minResourceFee)
846-
.addOperation(finalOperation)
847-
.addMemo(MemoNone)
848-
.setTimeout(300)
849-
.setSorobanData(transactionData)
850-
.build()
810+
// STEP 10: Assemble the transaction using the SDK's prepareTransaction.
811+
// This correctly applies resource fees, footprint, and soroban data from simulation.
812+
val preparedTransaction = kit.sorobanServer.prepareTransaction(signedTransaction, reSimulation)
851813

852814
// STEP 11: Determine submission method and submit
853815
val submissionMethod = getSubmissionMethod(forceMethod)
854816
val useRelayer = submissionMethod == SubmissionMethod.RELAYER
855817

856818
val result = submitOrRelay(
857-
transaction = finalTransaction,
819+
transaction = preparedTransaction,
858820
hostFunction = hostFunction,
859821
signedAuthEntries = signedAuthEntries,
860822
signer = tempKeypair,
@@ -868,7 +830,7 @@ class OZTransactionOperations internal constructor(
868830
)
869831
}
870832

871-
// STEP 13: Return funded amount as XLM string
833+
// STEP 12: Return funded amount as XLM string
872834
val xlmWhole = transferStroops / BigInteger.fromLong(Util.STROOPS_PER_XLM)
873835
val xlmFraction = transferStroops % BigInteger.fromLong(Util.STROOPS_PER_XLM)
874836
return if (xlmFraction == BigInteger.ZERO) {
@@ -1170,9 +1132,29 @@ class OZTransactionOperations internal constructor(
11701132
// Submit via RPC
11711133
val sendResult = kit.sorobanServer.sendTransaction(transaction)
11721134

1135+
when (sendResult.status) {
1136+
SendTransactionStatus.ERROR -> {
1137+
return TransactionResult(
1138+
success = false,
1139+
hash = sendResult.hash ?: "",
1140+
error = sendResult.errorResultXdr ?: "Transaction rejected by network"
1141+
)
1142+
}
1143+
SendTransactionStatus.TRY_AGAIN_LATER -> {
1144+
return TransactionResult(
1145+
success = false,
1146+
hash = sendResult.hash ?: "",
1147+
error = "Network is congested. Try again later."
1148+
)
1149+
}
1150+
else -> {
1151+
// PENDING or DUPLICATE — poll for confirmation
1152+
}
1153+
}
1154+
11731155
val hash = sendResult.hash
11741156
?: throw TransactionException.submissionFailed(
1175-
"Failed to get transaction hash from send result: ${sendResult.errorResultXdr ?: "unknown error"}"
1157+
"No transaction hash returned from send result"
11761158
)
11771159

11781160
if (emitEvents) {
@@ -1189,55 +1171,38 @@ class OZTransactionOperations internal constructor(
11891171
}
11901172

11911173
/**
1192-
* Polls for transaction confirmation.
1174+
* Polls for transaction confirmation using the SDK's [SorobanServer.pollTransaction].
11931175
*
1194-
* Repeatedly checks the transaction status on Soroban RPC until it is confirmed,
1195-
* fails, or times out. Uses exponential backoff between attempts.
1176+
* Uses 30 attempts with 3-second intervals (~90 seconds total) to account for
1177+
* testnet ledger close times and potential congestion.
11961178
*
11971179
* @param hash The transaction hash to poll
11981180
* @return TransactionResult indicating success or failure
1199-
* @throws SmartAccountException if polling times out
12001181
*/
12011182
private suspend fun pollForConfirmation(hash: String): TransactionResult {
1202-
val maxAttempts = 10
1203-
val sleepDurationMs = 2000L
1204-
1205-
repeat(maxAttempts) { attempt ->
1206-
val txStatus = kit.sorobanServer.getTransaction(hash)
1207-
1208-
when (txStatus.status) {
1209-
GetTransactionStatus.SUCCESS -> return TransactionResult(
1210-
success = true,
1211-
hash = hash,
1212-
ledger = txStatus.latestLedger?.toUInt()
1213-
)
1214-
1215-
GetTransactionStatus.FAILED -> {
1216-
val errorMessage = txStatus.resultXdr ?: "Transaction failed on-chain"
1217-
return TransactionResult(
1218-
success = false,
1219-
hash = hash,
1220-
ledger = txStatus.latestLedger?.toUInt(),
1221-
error = errorMessage
1222-
)
1223-
}
1183+
val txResponse = kit.sorobanServer.pollTransaction(
1184+
hash = hash,
1185+
maxAttempts = 30,
1186+
sleepStrategy = { 3000L }
1187+
)
12241188

1225-
GetTransactionStatus.NOT_FOUND -> {
1226-
// Transaction not yet confirmed, retry
1227-
if (attempt < maxAttempts - 1) {
1228-
delay(sleepDurationMs)
1229-
} else {
1230-
return TransactionResult(
1231-
success = false,
1232-
hash = hash,
1233-
error = "Transaction timed out after $maxAttempts attempts"
1234-
)
1235-
}
1236-
}
1237-
}
1189+
return when (txResponse.status) {
1190+
GetTransactionStatus.SUCCESS -> TransactionResult(
1191+
success = true,
1192+
hash = hash,
1193+
ledger = txResponse.latestLedger?.toUInt()
1194+
)
1195+
GetTransactionStatus.FAILED -> TransactionResult(
1196+
success = false,
1197+
hash = hash,
1198+
ledger = txResponse.latestLedger?.toUInt(),
1199+
error = txResponse.resultXdr ?: "Transaction failed on-chain"
1200+
)
1201+
GetTransactionStatus.NOT_FOUND -> TransactionResult(
1202+
success = false,
1203+
hash = hash,
1204+
error = "Transaction not confirmed after 30 polling attempts"
1205+
)
12381206
}
1239-
1240-
// Should not reach here, but for safety
1241-
throw TransactionException.timeout("Transaction polling timed out after $maxAttempts attempts")
12421207
}
12431208
}

0 commit comments

Comments
 (0)