@@ -18,8 +18,9 @@ import com.soneso.stellar.sdk.FriendBot
1818import com.soneso.stellar.sdk.Network
1919import com.soneso.stellar.sdk.Transaction
2020import com.soneso.stellar.sdk.TransactionBuilder
21- import com.soneso.stellar.sdk.TransactionBuilderAccount
2221import 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
2324import com.soneso.stellar.sdk.xdr.HostFunctionXdr
2425import com.soneso.stellar.sdk.xdr.Int64Xdr
2526import com.soneso.stellar.sdk.xdr.InvokeContractArgsXdr
@@ -28,7 +29,6 @@ import com.soneso.stellar.sdk.xdr.SCValXdr
2829import com.soneso.stellar.sdk.xdr.SorobanAddressCredentialsXdr
2930import com.soneso.stellar.sdk.xdr.SorobanAuthorizationEntryXdr
3031import com.soneso.stellar.sdk.xdr.SorobanCredentialsXdr
31- import com.soneso.stellar.sdk.xdr.SorobanTransactionDataXdr
3232import com.soneso.stellar.sdk.xdr.Uint32Xdr
3333import com.soneso.stellar.sdk.xdr.XdrReader
3434import 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