feat(txm): Stellar TXM broadcast, confirm, restore, and invoker integration#111
feat(txm): Stellar TXM broadcast, confirm, restore, and invoker integration#111Krish-vemula wants to merge 10 commits into
Conversation
Soroban Contract Test Coverage92.71% line coverage — 16414 / 17704 lines hit
Per-Contract Breakdown
Full file-level coverage report |
Integration Test Coverage (Token Pool) |
Integration Test Coverage (excl. Token Pool) |
| return simResult, nil | ||
| } | ||
|
|
||
| func isRetryableSimulationError(ctx context.Context, err error) bool { |
There was a problem hiding this comment.
what is this list of errors based on?
| simResult, err := client.SimulateTransaction(ctx, protocolrpc.SimulateTransactionRequest{ | ||
| Transaction: txXDR, | ||
| }) | ||
| s.metrics.ObserveSimulationDuration(ctx, time.Since(start).Seconds()) |
There was a problem hiding this comment.
what is the use case for tracking this?
| func (s *StellarTxm) updateTransactionStatus(tx *StellarTx, status commontypes.TransactionStatus) { | ||
| s.transactionsLock.Lock() | ||
| defer s.transactionsLock.Unlock() | ||
| tx.Status = status | ||
| } | ||
|
|
||
| func (s *StellarTxm) updateTransactionHash(tx *StellarTx, hash string) { | ||
| s.transactionsLock.Lock() | ||
| defer s.transactionsLock.Unlock() | ||
| tx.TxHash = hash | ||
| } | ||
|
|
||
| func (s *StellarTxm) updateTransactionFee(tx *StellarTx, fee *big.Int) { | ||
| s.transactionsLock.Lock() | ||
| defer s.transactionsLock.Unlock() | ||
| tx.Fee = fee | ||
| } | ||
|
|
||
| func (s *StellarTxm) updateTransactionResultXDR(tx *StellarTx, resultXDR string) { | ||
| s.transactionsLock.Lock() | ||
| defer s.transactionsLock.Unlock() | ||
| tx.ResultXDR = resultXDR | ||
| } | ||
|
|
||
| func (s *StellarTxm) updateTransactionResultCode(tx *StellarTx, code string) { | ||
| s.transactionsLock.Lock() | ||
| defer s.transactionsLock.Unlock() | ||
| tx.ResultCode = code | ||
| } | ||
|
|
||
| func (s *StellarTxm) updateTransactionResultMeta(tx *StellarTx, resultMetaXDR string) { | ||
| s.transactionsLock.Lock() | ||
| defer s.transactionsLock.Unlock() | ||
| tx.ResultMetaXDR = resultMetaXDR | ||
| } | ||
|
|
||
| func (s *StellarTxm) incrementTransactionAttempt(tx *StellarTx) { | ||
| s.transactionsLock.Lock() | ||
| defer s.transactionsLock.Unlock() | ||
| tx.Attempt++ | ||
| } | ||
|
|
||
| func (s *StellarTxm) getTransactionAttempt(tx *StellarTx) uint64 { | ||
| s.transactionsLock.RLock() | ||
| defer s.transactionsLock.RUnlock() | ||
| return tx.Attempt |
There was a problem hiding this comment.
These tx field value updates are too resource intensive, this same lock is used everywhere
| getClient func() (RPCClient, error) | ||
| } | ||
| transactions map[string]*StellarTx | ||
| transactionsLock sync.RWMutex |
There was a problem hiding this comment.
This lock is used as a universal lock, please at least add a TODO here to make txm concurrent and improve how locks are handled
…op dup metric, resync logs, test ctx/logger
| resp, fetchErr := client.GetFeeStats(ctx) | ||
| if fetchErr != nil { | ||
| if t.haveData { | ||
| return t.p50, t.p90, true, nil |
There was a problem hiding this comment.
what if this call keeps failing ? do we keep returning stale data ?
There was a problem hiding this comment.
Yes, by design. Once we have a successful GetFeeStats, repeated failures keep returning that last snapshot (err == nil) so we do not drop inclusion-fee seeding to baseline on every blip. There is no max staleness today; only a failed refresh before any success yields an error and the geometric baseline. Documented on feeTracker / sorobanInclusionPercentiles. If we want a cap or extra observability after prolonged failure, we can add a follow-up (e.g. max staleness or warn logs).
We have limits for max fees
| ) | ||
|
|
||
| var _ bindings.Invoker = (*InvokerAdapter)(nil) | ||
|
|
There was a problem hiding this comment.
InvokerAdapter implements bindings.Invoker by sending writes through StellarTxm (EnqueueAndWait → full simulate/sign/send/confirm path) and simulations through Simulate; GetEvents goes straight to RPC (no sequence).
| // InvokerAdapter bridges generated bindings clients to the TXM. State-changing | ||
| // calls go through EnqueueAndWait; read-only simulations go through Simulate; | ||
| // event reads delegate directly to the shared RPC client. | ||
| type InvokerAdapter struct { |
There was a problem hiding this comment.
Does each binding use the InvokerAdapter class ? Is this a cll concept ?
There was a problem hiding this comment.
No. Each generated client takes a bindings.Invoker. InvokerAdapter is one implementation you can pass in when you want TXM-backed invocations. Elsewhere we use other Invokers (e.g. deployment.Deployer as the shared invoker in ccv/chain devenv wiring). invoker_adapter_test.go has tests that might be helpful. This is optional code for later
| }, | ||
| } | ||
|
|
||
| sourceAccount := txnbuild.NewSimpleAccount(tx.FromAddress, seq-1) |
There was a problem hiding this comment.
can explain why do -1 here ?
i see that we send in GetNextSequence().
then we do -1 here and then we do IncrementSequenceNum=true.
is this intentional ?
There was a problem hiding this comment.
Intentional - GetNextSequence() returns the sequence the restore tx will consume on-chain; txnbuild wants last-used seq in SimpleAccount, so we pass seq-1 and set IncrementSequenceNum: true to get seq on the wire (same as buildPreliminaryTx). After restore we resync and call GetNextSequence() again for the user invoke. Documented in restore.go.
|
Code coverage report:
|
Summary
Implements the Stellar/Soroban transaction manager end-to-end: transactions move through enqueue, simulation, optional restore when state is expired, assembly, signing, submit, and confirmation, with background broadcast and confirm loops, fee handling, retries, pruning, and metrics. Adds
InvokerAdapterso generated contract bindings can route state-changing work throughEnqueueAndWaitand read paths through simulation. Builds on the chain wiring from the interface PR (this branch is a superset ofmainfor the same chain/mocks files).What changed
relayer/chainNetworkPassphrasefrom chain ID (txm.NetworkPassphrase) and constructing the real TXM withtxm.Config{}and passphrase (no new required TOML passphrase field; passphrases come from public/testnet mapping in code).relayer/txmtxm.go—StellarTxmservice: start/stop, broadcast + confirm goroutines, enqueue / wait APIs, account store, integration with config, fees, store, and metrics.broadcast.go— build / simulate / sign / send path and shared helpers used by tests.restore.go— restore footprint flow from simulation, submit, retries, metrics.invoker_adapter.go—bindings.Invoker-style adapter over TXM enqueue + simulate with ledger bounds / source options.config.go,fee.go,metrics.go,failed_result.go,tx.go,txstore.go,utils.go,network.go— defaults, fee strategy, metrics, failure modeling, in-memory tx model + store, helpers, passphrase resolution bychainID.config_test,fee_test,failed_result_test,txstore_test,txm_test,broadcast_test,handle_send_test,invoker_adapter_test.internal/mocks— aligned with expandedChain/RPCClient(including methods the full TXM path uses).