mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -0,0 +1,442 @@
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user