Files
sonr/client/tx/builder.go
T
Prad NukalaandGitHub 13e6c3e84d Master (#1262)
* clear

* feat: Add everything

* fix: Commenht
2025-10-03 14:45:52 -04:00

443 lines
12 KiB
Go

// Package tx provides transaction building utilities for the Sonr client SDK.
package tx
import (
"context"
"fmt"
"google.golang.org/grpc"
"cosmossdk.io/math"
sdktypes "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/keys"
)
// TxBuilder provides an interface for building and broadcasting transactions.
type TxBuilder interface {
// Transaction configuration
WithChainID(chainID string) TxBuilder
WithGasPrice(price float64, denom string) TxBuilder
WithGasLimit(limit uint64) TxBuilder
WithMemo(memo string) TxBuilder
WithTimeoutHeight(height uint64) TxBuilder
// Message operations
AddMessage(msg sdktypes.Msg) TxBuilder
AddMessages(msgs ...sdktypes.Msg) TxBuilder
ClearMessages() TxBuilder
// Fee operations
WithFee(amount sdktypes.Coins) TxBuilder
WithGasAdjustment(adjustment float64) TxBuilder
EstimateGas(ctx context.Context) (uint64, error)
// Signing and broadcasting
Sign(ctx context.Context, keyring keys.KeyringManager) (*SignedTx, error)
SignAndBroadcast(ctx context.Context, keyring keys.KeyringManager) (*BroadcastResult, error)
Broadcast(ctx context.Context, signedTx *SignedTx) (*BroadcastResult, error)
// Simulation
Simulate(ctx context.Context) (*SimulateResult, error)
// Building
Build() (*UnsignedTx, error)
BuildSigned(signature []byte, pubKey []byte) (*SignedTx, error)
// Configuration access
Config() *TxConfig
}
// TxConfig holds transaction configuration.
type TxConfig struct {
ChainID string
GasPrice float64
GasDenom string
GasLimit uint64
GasAdjustment float64
Memo string
TimeoutHeight uint64
Fee sdktypes.Coins
}
// UnsignedTx represents an unsigned transaction.
type UnsignedTx struct {
Messages []sdktypes.Msg
Config *TxConfig
SignBytes []byte
AccountNumber uint64
Sequence uint64
}
// SignedTx represents a signed transaction.
type SignedTx struct {
UnsignedTx *UnsignedTx
Signature []byte
PubKey []byte
TxBytes []byte
}
// BroadcastResult contains the result of broadcasting a transaction.
type BroadcastResult struct {
TxHash string
Code uint32
Log string
GasWanted int64
GasUsed int64
Height int64
Events []Event
}
// Event represents a transaction event.
type Event struct {
Type string
Attributes []Attribute
}
// Attribute represents an event attribute.
type Attribute struct {
Key string
Value string
}
// SimulateResult contains the result of transaction simulation.
type SimulateResult struct {
GasWanted int64
GasUsed int64
Log string
Events []Event
}
// txBuilder implements TxBuilder.
type txBuilder struct {
grpcConn *grpc.ClientConn
config *config.NetworkConfig
txConfig *TxConfig
messages []sdktypes.Msg
// Cosmos SDK clients
txServiceClient tx.ServiceClient
}
// NewTxBuilder creates a new transaction builder.
func NewTxBuilder(cfg *config.NetworkConfig, grpcConn *grpc.ClientConn) (TxBuilder, error) {
if cfg == nil {
return nil, fmt.Errorf("network configuration is required")
}
if grpcConn == nil {
return nil, fmt.Errorf("gRPC connection is required")
}
txConfig := &TxConfig{
ChainID: cfg.ChainID,
GasPrice: cfg.GasPrice,
GasDenom: cfg.StakingDenom,
GasAdjustment: cfg.GasAdjustment,
GasLimit: 200000, // Default gas limit
}
return &txBuilder{
grpcConn: grpcConn,
config: cfg,
txConfig: txConfig,
messages: make([]sdktypes.Msg, 0),
txServiceClient: tx.NewServiceClient(grpcConn),
}, nil
}
// WithChainID sets the chain ID for the transaction.
func (tb *txBuilder) WithChainID(chainID string) TxBuilder {
tb.txConfig.ChainID = chainID
return tb
}
// WithGasPrice sets the gas price and denomination.
func (tb *txBuilder) WithGasPrice(price float64, denom string) TxBuilder {
tb.txConfig.GasPrice = price
tb.txConfig.GasDenom = denom
return tb
}
// WithGasLimit sets the gas limit for the transaction.
func (tb *txBuilder) WithGasLimit(limit uint64) TxBuilder {
tb.txConfig.GasLimit = limit
return tb
}
// WithMemo sets the memo for the transaction.
func (tb *txBuilder) WithMemo(memo string) TxBuilder {
tb.txConfig.Memo = memo
return tb
}
// WithTimeoutHeight sets the timeout height for the transaction.
func (tb *txBuilder) WithTimeoutHeight(height uint64) TxBuilder {
tb.txConfig.TimeoutHeight = height
return tb
}
// AddMessage adds a single message to the transaction.
func (tb *txBuilder) AddMessage(msg sdktypes.Msg) TxBuilder {
tb.messages = append(tb.messages, msg)
return tb
}
// AddMessages adds multiple messages to the transaction.
func (tb *txBuilder) AddMessages(msgs ...sdktypes.Msg) TxBuilder {
tb.messages = append(tb.messages, msgs...)
return tb
}
// ClearMessages removes all messages from the transaction.
func (tb *txBuilder) ClearMessages() TxBuilder {
tb.messages = make([]sdktypes.Msg, 0)
return tb
}
// WithFee sets the transaction fee directly.
func (tb *txBuilder) WithFee(amount sdktypes.Coins) TxBuilder {
tb.txConfig.Fee = amount
return tb
}
// WithGasAdjustment sets the gas adjustment factor.
func (tb *txBuilder) WithGasAdjustment(adjustment float64) TxBuilder {
tb.txConfig.GasAdjustment = adjustment
return tb
}
// EstimateGas estimates the gas required for the transaction.
func (tb *txBuilder) EstimateGas(ctx context.Context) (uint64, error) {
// Build unsigned transaction for simulation
_, err := tb.Build()
if err != nil {
return 0, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to build transaction for gas estimation")
}
// Simulate the transaction
simulateResult, err := tb.Simulate(ctx)
if err != nil {
return 0, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to simulate transaction")
}
// Apply gas adjustment
estimatedGas := float64(simulateResult.GasUsed) * tb.txConfig.GasAdjustment
return uint64(estimatedGas), nil
}
// Sign signs the transaction using the provided keyring.
func (tb *txBuilder) Sign(ctx context.Context, keyring keys.KeyringManager) (*SignedTx, error) {
// Build unsigned transaction
unsignedTx, err := tb.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to build unsigned transaction")
}
// Sign the transaction bytes using the DWN plugin
signature, err := keyring.SignTransaction(ctx, unsignedTx.SignBytes)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to sign transaction")
}
// Get wallet identity for public key
identity, err := keyring.GetIssuerDID(ctx)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to get wallet identity")
}
// For now, use a placeholder for public key - this should be derived from the DID
// TODO: Extract public key from DID or add GetPubKey method to KeyringManager
pubKey := []byte(identity.DID) // Placeholder
// Build signed transaction
signedTx, err := tb.BuildSigned(signature.Signature, pubKey)
if err != nil {
return nil, errors.WrapError(err, errors.ErrSigningFailed, "failed to build signed transaction")
}
return signedTx, nil
}
// SignAndBroadcast signs and broadcasts the transaction in one operation.
func (tb *txBuilder) SignAndBroadcast(ctx context.Context, keyring keys.KeyringManager) (*BroadcastResult, error) {
// Sign the transaction
signedTx, err := tb.Sign(ctx, keyring)
if err != nil {
return nil, err
}
// Broadcast the signed transaction
return tb.Broadcast(ctx, signedTx)
}
// Broadcast broadcasts a signed transaction to the network.
func (tb *txBuilder) Broadcast(ctx context.Context, signedTx *SignedTx) (*BroadcastResult, error) {
// Create broadcast request
req := &tx.BroadcastTxRequest{
TxBytes: signedTx.TxBytes,
Mode: tx.BroadcastMode_BROADCAST_MODE_SYNC, // Default to sync mode
}
// Broadcast the transaction
resp, err := tb.txServiceClient.BroadcastTx(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast transaction")
}
// Convert response to our format
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, nil
}
// Simulate simulates the transaction to estimate gas and check for errors.
func (tb *txBuilder) Simulate(ctx context.Context) (*SimulateResult, error) {
// Build unsigned transaction for simulation
unsignedTx, err := tb.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to build transaction for simulation")
}
// Create simulate request
req := &tx.SimulateRequest{
TxBytes: unsignedTx.SignBytes, // Use sign bytes for simulation
}
// Simulate the transaction
resp, err := tb.txServiceClient.Simulate(ctx, req)
if err != nil {
return nil, errors.WrapError(err, errors.ErrGasEstimationFailed, "failed to simulate transaction")
}
// Convert response to our format
result := &SimulateResult{
GasWanted: int64(resp.GasInfo.GasWanted),
GasUsed: int64(resp.GasInfo.GasUsed),
Log: resp.Result.Log,
}
// Convert events
for _, event := range resp.Result.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, nil
}
// Build creates an unsigned transaction.
func (tb *txBuilder) Build() (*UnsignedTx, error) {
// Allow building without messages for testing/simulation purposes
// Real transactions will still require messages when broadcasting
// Calculate fee if not set
fee := tb.txConfig.Fee
if fee.IsZero() {
// Calculate fee based on gas price and limit
gasAmount := math.NewIntFromUint64(uint64(float64(tb.txConfig.GasLimit) * tb.txConfig.GasPrice))
fee = sdktypes.NewCoins(sdktypes.NewCoin(tb.txConfig.GasDenom, gasAmount))
}
// Create sign bytes (simplified - in a real implementation this would use proper transaction encoding)
signBytes := []byte(fmt.Sprintf("chain_id:%s,messages:%d,fee:%s,memo:%s",
tb.txConfig.ChainID,
len(tb.messages),
fee.String(),
tb.txConfig.Memo))
return &UnsignedTx{
Messages: tb.messages,
Config: tb.txConfig,
SignBytes: signBytes,
// TODO: Fetch account number and sequence from chain
AccountNumber: 0,
Sequence: 0,
}, nil
}
// BuildSigned creates a signed transaction from signature and public key.
func (tb *txBuilder) BuildSigned(signature []byte, pubKey []byte) (*SignedTx, error) {
unsignedTx, err := tb.Build()
if err != nil {
return nil, err
}
// Create transaction bytes (simplified - in a real implementation this would use proper transaction encoding)
txBytes := append(unsignedTx.SignBytes, signature...)
txBytes = append(txBytes, pubKey...)
return &SignedTx{
UnsignedTx: unsignedTx,
Signature: signature,
PubKey: pubKey,
TxBytes: txBytes,
}, nil
}
// Config returns the current transaction configuration.
func (tb *txBuilder) Config() *TxConfig {
return tb.txConfig
}
// Utility functions
// NewTxConfig creates a new transaction configuration with defaults.
func NewTxConfig(chainID string) *TxConfig {
return &TxConfig{
ChainID: chainID,
GasPrice: 0.001,
GasDenom: "usnr",
GasAdjustment: 1.5,
GasLimit: 200000,
}
}
// DefaultGasLimit returns the default gas limit for transactions.
func DefaultGasLimit() uint64 {
return 200000
}
// DefaultGasPrice returns the default gas price for the Sonr network.
func DefaultGasPrice() float64 {
return 0.001
}
// CalculateFee calculates the transaction fee based on gas price and limit.
func CalculateFee(gasPrice float64, gasLimit uint64, denom string) sdktypes.Coins {
gasAmount := math.NewIntFromUint64(uint64(float64(gasLimit) * gasPrice))
return sdktypes.NewCoins(sdktypes.NewCoin(denom, gasAmount))
}