mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -0,0 +1,357 @@
|
||||
// Package tx provides transaction broadcasting utilities for the Sonr client SDK.
|
||||
package tx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/types/tx"
|
||||
|
||||
"github.com/sonr-io/sonr/client/config"
|
||||
"github.com/sonr-io/sonr/client/errors"
|
||||
)
|
||||
|
||||
// BroadcastMode defines different transaction broadcasting modes.
|
||||
type BroadcastMode string
|
||||
|
||||
const (
|
||||
// BroadcastModeSync waits for the transaction to be included in a block and returns the result.
|
||||
BroadcastModeSync BroadcastMode = "sync"
|
||||
|
||||
// BroadcastModeAsync submits the transaction and returns immediately without waiting.
|
||||
BroadcastModeAsync BroadcastMode = "async"
|
||||
|
||||
// BroadcastModeBlock waits for the transaction to be committed and returns the full result.
|
||||
BroadcastModeBlock BroadcastMode = "block"
|
||||
)
|
||||
|
||||
// Broadcaster provides an interface for broadcasting transactions with different modes and retry logic.
|
||||
type Broadcaster interface {
|
||||
// Broadcasting operations
|
||||
Broadcast(ctx context.Context, txBytes []byte, mode BroadcastMode) (*BroadcastResult, error)
|
||||
BroadcastSync(ctx context.Context, txBytes []byte) (*BroadcastResult, error)
|
||||
BroadcastAsync(ctx context.Context, txBytes []byte) (*BroadcastResult, error)
|
||||
BroadcastBlock(ctx context.Context, txBytes []byte) (*BroadcastResult, error)
|
||||
|
||||
// Retry and monitoring
|
||||
BroadcastWithRetry(ctx context.Context, txBytes []byte, mode BroadcastMode, maxRetries int) (*BroadcastResult, error)
|
||||
WaitForConfirmation(ctx context.Context, txHash string, timeout time.Duration) (*TxConfirmation, error)
|
||||
|
||||
// Configuration
|
||||
WithRetryConfig(config RetryConfig) Broadcaster
|
||||
WithTimeout(timeout time.Duration) Broadcaster
|
||||
}
|
||||
|
||||
// TxConfirmation contains information about a confirmed transaction.
|
||||
type TxConfirmation struct {
|
||||
TxHash string
|
||||
BlockHeight int64
|
||||
BlockTime time.Time
|
||||
Code uint32
|
||||
Log string
|
||||
GasWanted int64
|
||||
GasUsed int64
|
||||
Events []Event
|
||||
}
|
||||
|
||||
// RetryConfig defines retry behavior for failed broadcasts.
|
||||
type RetryConfig struct {
|
||||
MaxRetries int
|
||||
InitialDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
BackoffFactor float64
|
||||
}
|
||||
|
||||
// broadcaster implements Broadcaster.
|
||||
type broadcaster struct {
|
||||
grpcConn *grpc.ClientConn
|
||||
config *config.NetworkConfig
|
||||
txServiceClient tx.ServiceClient
|
||||
retryConfig RetryConfig
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewBroadcaster creates a new transaction broadcaster.
|
||||
func NewBroadcaster(grpcConn *grpc.ClientConn, cfg *config.NetworkConfig) Broadcaster {
|
||||
return &broadcaster{
|
||||
grpcConn: grpcConn,
|
||||
config: cfg,
|
||||
txServiceClient: tx.NewServiceClient(grpcConn),
|
||||
retryConfig: DefaultRetryConfig(),
|
||||
timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultRetryConfig returns sensible defaults for retry configuration.
|
||||
func DefaultRetryConfig() RetryConfig {
|
||||
return RetryConfig{
|
||||
MaxRetries: 3,
|
||||
InitialDelay: 1 * time.Second,
|
||||
MaxDelay: 10 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast broadcasts a transaction with the specified mode.
|
||||
func (b *broadcaster) Broadcast(ctx context.Context, txBytes []byte, mode BroadcastMode) (*BroadcastResult, error) {
|
||||
// Convert our mode to SDK broadcast mode
|
||||
var sdkMode tx.BroadcastMode
|
||||
switch mode {
|
||||
case BroadcastModeSync:
|
||||
sdkMode = tx.BroadcastMode_BROADCAST_MODE_SYNC
|
||||
case BroadcastModeAsync:
|
||||
sdkMode = tx.BroadcastMode_BROADCAST_MODE_ASYNC
|
||||
case BroadcastModeBlock:
|
||||
sdkMode = tx.BroadcastMode_BROADCAST_MODE_BLOCK
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid broadcast mode: %s", mode)
|
||||
}
|
||||
|
||||
// Create broadcast request
|
||||
req := &tx.BroadcastTxRequest{
|
||||
TxBytes: txBytes,
|
||||
Mode: sdkMode,
|
||||
}
|
||||
|
||||
// Apply timeout to context
|
||||
broadcastCtx, cancel := context.WithTimeout(ctx, b.timeout)
|
||||
defer cancel()
|
||||
|
||||
// Broadcast the transaction
|
||||
resp, err := b.txServiceClient.BroadcastTx(broadcastCtx, req)
|
||||
if err != nil {
|
||||
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast transaction")
|
||||
}
|
||||
|
||||
// Convert response
|
||||
return convertBroadcastResponse(resp), nil
|
||||
}
|
||||
|
||||
// BroadcastSync broadcasts a transaction synchronously.
|
||||
func (b *broadcaster) BroadcastSync(ctx context.Context, txBytes []byte) (*BroadcastResult, error) {
|
||||
return b.Broadcast(ctx, txBytes, BroadcastModeSync)
|
||||
}
|
||||
|
||||
// BroadcastAsync broadcasts a transaction asynchronously.
|
||||
func (b *broadcaster) BroadcastAsync(ctx context.Context, txBytes []byte) (*BroadcastResult, error) {
|
||||
return b.Broadcast(ctx, txBytes, BroadcastModeAsync)
|
||||
}
|
||||
|
||||
// BroadcastBlock broadcasts a transaction and waits for block confirmation.
|
||||
func (b *broadcaster) BroadcastBlock(ctx context.Context, txBytes []byte) (*BroadcastResult, error) {
|
||||
return b.Broadcast(ctx, txBytes, BroadcastModeBlock)
|
||||
}
|
||||
|
||||
// BroadcastWithRetry broadcasts a transaction with retry logic.
|
||||
func (b *broadcaster) BroadcastWithRetry(ctx context.Context, txBytes []byte, mode BroadcastMode, maxRetries int) (*BroadcastResult, error) {
|
||||
var lastErr error
|
||||
delay := b.retryConfig.InitialDelay
|
||||
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
result, err := b.Broadcast(ctx, txBytes, mode)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// Don't retry on the last attempt
|
||||
if attempt == maxRetries {
|
||||
break
|
||||
}
|
||||
|
||||
// Check if error is retryable
|
||||
if !isRetryableError(err) {
|
||||
break
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(delay):
|
||||
// Exponential backoff
|
||||
delay = time.Duration(float64(delay) * b.retryConfig.BackoffFactor)
|
||||
if delay > b.retryConfig.MaxDelay {
|
||||
delay = b.retryConfig.MaxDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.WrapError(lastErr, errors.ErrBroadcastFailed, "failed to broadcast transaction after %d retries", maxRetries)
|
||||
}
|
||||
|
||||
// WaitForConfirmation waits for a transaction to be confirmed on-chain.
|
||||
func (b *broadcaster) WaitForConfirmation(ctx context.Context, txHash string, timeout time.Duration) (*TxConfirmation, error) {
|
||||
// Create timeout context
|
||||
confirmCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
// Poll for transaction confirmation
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-confirmCtx.Done():
|
||||
return nil, errors.WrapError(confirmCtx.Err(), errors.ErrTimeout, "timeout waiting for transaction confirmation")
|
||||
case <-ticker.C:
|
||||
// Try to fetch transaction
|
||||
req := &tx.GetTxRequest{Hash: txHash}
|
||||
resp, err := b.txServiceClient.GetTx(confirmCtx, req)
|
||||
if err != nil {
|
||||
// Transaction not found yet, continue polling
|
||||
continue
|
||||
}
|
||||
|
||||
// Transaction found, convert to confirmation
|
||||
confirmation := &TxConfirmation{
|
||||
TxHash: resp.TxResponse.TxHash,
|
||||
BlockHeight: resp.TxResponse.Height,
|
||||
Code: resp.TxResponse.Code,
|
||||
Log: resp.TxResponse.RawLog,
|
||||
GasWanted: resp.TxResponse.GasWanted,
|
||||
GasUsed: resp.TxResponse.GasUsed,
|
||||
// BlockTime would need to be fetched from block info
|
||||
}
|
||||
|
||||
// Convert events
|
||||
for _, event := range resp.TxResponse.Events {
|
||||
e := Event{
|
||||
Type: event.Type,
|
||||
}
|
||||
for _, attr := range event.Attributes {
|
||||
e.Attributes = append(e.Attributes, Attribute{
|
||||
Key: attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
}
|
||||
confirmation.Events = append(confirmation.Events, e)
|
||||
}
|
||||
|
||||
return confirmation, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryConfig sets the retry configuration.
|
||||
func (b *broadcaster) WithRetryConfig(config RetryConfig) Broadcaster {
|
||||
b.retryConfig = config
|
||||
return b
|
||||
}
|
||||
|
||||
// WithTimeout sets the broadcast timeout.
|
||||
func (b *broadcaster) WithTimeout(timeout time.Duration) Broadcaster {
|
||||
b.timeout = timeout
|
||||
return b
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// convertBroadcastResponse converts SDK broadcast response to our format.
|
||||
func convertBroadcastResponse(resp *tx.BroadcastTxResponse) *BroadcastResult {
|
||||
result := &BroadcastResult{
|
||||
TxHash: resp.TxResponse.TxHash,
|
||||
Code: resp.TxResponse.Code,
|
||||
Log: resp.TxResponse.RawLog,
|
||||
GasWanted: resp.TxResponse.GasWanted,
|
||||
GasUsed: resp.TxResponse.GasUsed,
|
||||
Height: resp.TxResponse.Height,
|
||||
}
|
||||
|
||||
// Convert events
|
||||
for _, event := range resp.TxResponse.Events {
|
||||
e := Event{
|
||||
Type: event.Type,
|
||||
}
|
||||
for _, attr := range event.Attributes {
|
||||
e.Attributes = append(e.Attributes, Attribute{
|
||||
Key: attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
}
|
||||
result.Events = append(result.Events, e)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// isRetryableError determines if an error is worth retrying.
|
||||
func isRetryableError(err error) bool {
|
||||
// Check for specific error types that are retryable
|
||||
if errors.IsConnectionError(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Timeouts are generally retryable
|
||||
if errors.GetErrorCode(err) == errors.CodeTimeout {
|
||||
return true
|
||||
}
|
||||
|
||||
// Network unreachable errors are retryable
|
||||
if errors.GetErrorCode(err) == errors.CodeNetworkUnreachable {
|
||||
return true
|
||||
}
|
||||
|
||||
// Other errors like invalid transaction, insufficient funds, etc. are not retryable
|
||||
return false
|
||||
}
|
||||
|
||||
// BroadcastConfig provides configuration options for broadcasting.
|
||||
type BroadcastConfig struct {
|
||||
Mode BroadcastMode
|
||||
Timeout time.Duration
|
||||
RetryConfig RetryConfig
|
||||
WaitForBlock bool
|
||||
}
|
||||
|
||||
// DefaultBroadcastConfig returns sensible defaults for broadcasting.
|
||||
func DefaultBroadcastConfig() BroadcastConfig {
|
||||
return BroadcastConfig{
|
||||
Mode: BroadcastModeSync,
|
||||
Timeout: 30 * time.Second,
|
||||
RetryConfig: DefaultRetryConfig(),
|
||||
WaitForBlock: false,
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastWithConfig broadcasts a transaction using the provided configuration.
|
||||
func (b *broadcaster) BroadcastWithConfig(ctx context.Context, txBytes []byte, config BroadcastConfig) (*BroadcastResult, error) {
|
||||
// Set timeout
|
||||
originalTimeout := b.timeout
|
||||
b.timeout = config.Timeout
|
||||
defer func() { b.timeout = originalTimeout }()
|
||||
|
||||
// Set retry config
|
||||
originalRetryConfig := b.retryConfig
|
||||
b.retryConfig = config.RetryConfig
|
||||
defer func() { b.retryConfig = originalRetryConfig }()
|
||||
|
||||
// Broadcast with retry
|
||||
result, err := b.BroadcastWithRetry(ctx, txBytes, config.Mode, config.RetryConfig.MaxRetries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for block confirmation if requested
|
||||
if config.WaitForBlock && result.TxHash != "" {
|
||||
confirmation, err := b.WaitForConfirmation(ctx, result.TxHash, config.Timeout)
|
||||
if err != nil {
|
||||
// Return the broadcast result even if we couldn't wait for confirmation
|
||||
return result, fmt.Errorf("transaction broadcast succeeded but confirmation failed: %w", err)
|
||||
}
|
||||
|
||||
// Update result with confirmation data
|
||||
result.Height = confirmation.BlockHeight
|
||||
result.Code = confirmation.Code
|
||||
result.Log = confirmation.Log
|
||||
result.GasWanted = confirmation.GasWanted
|
||||
result.GasUsed = confirmation.GasUsed
|
||||
result.Events = confirmation.Events
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user