mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 10:21:40 +00:00
@@ -0,0 +1,363 @@
|
||||
// Package txns implements transaction builders for Cosmos and EVM chains
|
||||
package txns
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
"github.com/sonr-io/sonr/types/coins"
|
||||
)
|
||||
|
||||
// AddressDeriver interface for deriving addresses from public keys
|
||||
type AddressDeriver interface {
|
||||
// DeriveCosmosAddress derives a Cosmos address from public key
|
||||
DeriveCosmosAddress(pubKey []byte, prefix string) (string, error)
|
||||
// DeriveEVMAddress derives an EVM address from public key
|
||||
DeriveEVMAddress(pubKey []byte) (string, error)
|
||||
// ValidateAddress validates an address for the given chain type
|
||||
ValidateAddress(address string, chainType TransactionType) error
|
||||
// GetAddressFormat returns the address format for the chain type
|
||||
GetAddressFormat(chainType TransactionType) string
|
||||
}
|
||||
|
||||
// StandardAddressDeriver implements standard address derivation
|
||||
type StandardAddressDeriver struct {
|
||||
cosmosPrefix string
|
||||
}
|
||||
|
||||
// NewStandardAddressDeriver creates a new standard address deriver
|
||||
func NewStandardAddressDeriver(cosmosPrefix string) *StandardAddressDeriver {
|
||||
return &StandardAddressDeriver{
|
||||
cosmosPrefix: cosmosPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveCosmosAddress implements AddressDeriver interface
|
||||
func (sad *StandardAddressDeriver) DeriveCosmosAddress(
|
||||
pubKey []byte,
|
||||
prefix string,
|
||||
) (string, error) {
|
||||
if len(pubKey) != 33 {
|
||||
return "", fmt.Errorf("invalid public key length: expected 33, got %d", len(pubKey))
|
||||
}
|
||||
|
||||
// Parse the compressed public key
|
||||
ecdsaPubKey, err := crypto.DecompressPubkey(pubKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decompress public key: %w", err)
|
||||
}
|
||||
|
||||
// Convert to Cosmos SDK format and derive address
|
||||
pubKeyBytes := crypto.FromECDSAPub(ecdsaPubKey)
|
||||
addr := sdk.AccAddress(crypto.Keccak256(pubKeyBytes[1:])[12:]) // Last 20 bytes of hash
|
||||
|
||||
// Use provided prefix or default
|
||||
if prefix == "" {
|
||||
prefix = sad.cosmosPrefix
|
||||
}
|
||||
|
||||
// Convert to bech32 format
|
||||
bech32Addr, err := sdk.Bech32ifyAddressBytes(prefix, addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to convert to bech32: %w", err)
|
||||
}
|
||||
|
||||
return bech32Addr, nil
|
||||
}
|
||||
|
||||
// DeriveEVMAddress implements AddressDeriver interface
|
||||
func (sad *StandardAddressDeriver) DeriveEVMAddress(pubKey []byte) (string, error) {
|
||||
if len(pubKey) != 33 && len(pubKey) != 65 {
|
||||
return "", fmt.Errorf("invalid public key length: expected 33 or 65, got %d", len(pubKey))
|
||||
}
|
||||
|
||||
var ecdsaPubKey *ecdsa.PublicKey
|
||||
var err error
|
||||
|
||||
if len(pubKey) == 33 {
|
||||
// Compressed public key
|
||||
ecdsaPubKey, err = crypto.DecompressPubkey(pubKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decompress public key: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Uncompressed public key
|
||||
ecdsaPubKey, err = crypto.UnmarshalPubkey(pubKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal public key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Derive Ethereum address
|
||||
address := crypto.PubkeyToAddress(*ecdsaPubKey)
|
||||
return address.Hex(), nil
|
||||
}
|
||||
|
||||
// ValidateAddress implements AddressDeriver interface
|
||||
func (sad *StandardAddressDeriver) ValidateAddress(
|
||||
address string,
|
||||
chainType TransactionType,
|
||||
) error {
|
||||
switch chainType {
|
||||
case TransactionTypeCosmos:
|
||||
_, err := sdk.AccAddressFromBech32(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Cosmos address: %w", err)
|
||||
}
|
||||
case TransactionTypeEVM:
|
||||
if !common.IsHexAddress(address) {
|
||||
return fmt.Errorf("invalid EVM address format")
|
||||
}
|
||||
default:
|
||||
return ErrUnsupportedChainType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAddressFormat implements AddressDeriver interface
|
||||
func (sad *StandardAddressDeriver) GetAddressFormat(chainType TransactionType) string {
|
||||
switch chainType {
|
||||
case TransactionTypeCosmos:
|
||||
return fmt.Sprintf("bech32 with prefix '%s'", sad.cosmosPrefix)
|
||||
case TransactionTypeEVM:
|
||||
return "hex format (0x...)"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// MPCAddressDeriver derives addresses from MPC public keys
|
||||
type MPCAddressDeriver struct {
|
||||
*StandardAddressDeriver
|
||||
}
|
||||
|
||||
// NewMPCAddressDeriver creates a new MPC address deriver
|
||||
func NewMPCAddressDeriver(cosmosPrefix string) *MPCAddressDeriver {
|
||||
return &MPCAddressDeriver{
|
||||
StandardAddressDeriver: NewStandardAddressDeriver(cosmosPrefix),
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveFromEnclaveData derives addresses from MPC enclave data
|
||||
func (mad *MPCAddressDeriver) DeriveFromEnclaveData(
|
||||
enclaveData *mpc.EnclaveData,
|
||||
prefix string,
|
||||
) (*AddressDerivation, error) {
|
||||
// Import the enclave to get the public key
|
||||
enclave, err := mpc.ImportEnclave(mpc.WithEnclaveData(enclaveData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to import enclave: %w", err)
|
||||
}
|
||||
|
||||
pubKey := enclave.PubKeyBytes()
|
||||
|
||||
// Derive Cosmos address
|
||||
cosmosAddr, err := mad.DeriveCosmosAddress(pubKey, prefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive Cosmos address: %w", err)
|
||||
}
|
||||
|
||||
// Derive EVM address
|
||||
evmAddr, err := mad.DeriveEVMAddress(pubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive EVM address: %w", err)
|
||||
}
|
||||
|
||||
return &AddressDerivation{
|
||||
CosmosAddress: cosmosAddr,
|
||||
EVMAddress: evmAddr,
|
||||
DerivationPath: "mpc-enclave",
|
||||
PublicKey: pubKey,
|
||||
ChainType: "multi-chain",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EntropyAddressDeriver derives addresses from entropy (DID + salt)
|
||||
type EntropyAddressDeriver struct {
|
||||
*StandardAddressDeriver
|
||||
coinsManager *coins.Manager
|
||||
}
|
||||
|
||||
// NewEntropyAddressDeriver creates a new entropy-based address deriver
|
||||
func NewEntropyAddressDeriver(
|
||||
cosmosPrefix string,
|
||||
coinsManager *coins.Manager,
|
||||
) *EntropyAddressDeriver {
|
||||
return &EntropyAddressDeriver{
|
||||
StandardAddressDeriver: NewStandardAddressDeriver(cosmosPrefix),
|
||||
coinsManager: coinsManager,
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveFromEntropy derives addresses from DID and salt using the coins package
|
||||
func (ead *EntropyAddressDeriver) DeriveFromEntropy(
|
||||
did, salt, prefix string,
|
||||
) (*AddressDerivation, error) {
|
||||
// Use the coins manager to derive addresses
|
||||
cosmosAddr, evmAddr, derivationPath, err := ead.coinsManager.DeriveAddresses(did, salt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive addresses from entropy: %w", err)
|
||||
}
|
||||
|
||||
// Create a temporary wallet to get the public key
|
||||
wallet, err := ead.coinsManager.CreateWalletFromEntropy(did, salt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create wallet for public key: %w", err)
|
||||
}
|
||||
|
||||
return &AddressDerivation{
|
||||
CosmosAddress: cosmosAddr,
|
||||
EVMAddress: evmAddr,
|
||||
DerivationPath: derivationPath,
|
||||
PublicKey: wallet.GetCosmosPublicKey().Bytes(),
|
||||
ChainType: "multi-chain",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddressManager manages address derivation for multiple methods
|
||||
type AddressManager struct {
|
||||
standardDeriver *StandardAddressDeriver
|
||||
mpcDeriver *MPCAddressDeriver
|
||||
entropyDeriver *EntropyAddressDeriver
|
||||
cosmosPrefix string
|
||||
}
|
||||
|
||||
// NewAddressManager creates a new address manager
|
||||
func NewAddressManager(cosmosPrefix string, coinsManager *coins.Manager) *AddressManager {
|
||||
return &AddressManager{
|
||||
standardDeriver: NewStandardAddressDeriver(cosmosPrefix),
|
||||
mpcDeriver: NewMPCAddressDeriver(cosmosPrefix),
|
||||
entropyDeriver: NewEntropyAddressDeriver(cosmosPrefix, coinsManager),
|
||||
cosmosPrefix: cosmosPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveFromPublicKey derives addresses from a raw public key
|
||||
func (am *AddressManager) DeriveFromPublicKey(
|
||||
pubKey []byte,
|
||||
prefix string,
|
||||
) (*AddressDerivation, error) {
|
||||
cosmosAddr, err := am.standardDeriver.DeriveCosmosAddress(pubKey, prefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive Cosmos address: %w", err)
|
||||
}
|
||||
|
||||
evmAddr, err := am.standardDeriver.DeriveEVMAddress(pubKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive EVM address: %w", err)
|
||||
}
|
||||
|
||||
return &AddressDerivation{
|
||||
CosmosAddress: cosmosAddr,
|
||||
EVMAddress: evmAddr,
|
||||
DerivationPath: "direct-public-key",
|
||||
PublicKey: pubKey,
|
||||
ChainType: "multi-chain",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeriveFromMPCEnclave derives addresses from MPC enclave data
|
||||
func (am *AddressManager) DeriveFromMPCEnclave(
|
||||
enclaveData *mpc.EnclaveData,
|
||||
prefix string,
|
||||
) (*AddressDerivation, error) {
|
||||
return am.mpcDeriver.DeriveFromEnclaveData(enclaveData, prefix)
|
||||
}
|
||||
|
||||
// DeriveFromEntropy derives addresses from DID and salt
|
||||
func (am *AddressManager) DeriveFromEntropy(did, salt, prefix string) (*AddressDerivation, error) {
|
||||
return am.entropyDeriver.DeriveFromEntropy(did, salt, prefix)
|
||||
}
|
||||
|
||||
// ValidateAddress validates an address for any supported chain type
|
||||
func (am *AddressManager) ValidateAddress(address string, chainType TransactionType) error {
|
||||
return am.standardDeriver.ValidateAddress(address, chainType)
|
||||
}
|
||||
|
||||
// GetSupportedChainTypes returns the supported chain types
|
||||
func (am *AddressManager) GetSupportedChainTypes() []TransactionType {
|
||||
return []TransactionType{TransactionTypeCosmos, TransactionTypeEVM}
|
||||
}
|
||||
|
||||
// ConvertAddress converts an address between different formats (if applicable)
|
||||
func (am *AddressManager) ConvertAddress(
|
||||
address string,
|
||||
fromChain, toChain TransactionType,
|
||||
) (string, error) {
|
||||
// For now, only validation since we can't directly convert between Cosmos and EVM addresses
|
||||
// without the underlying public key
|
||||
if fromChain == toChain {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// Validate the source address
|
||||
err := am.ValidateAddress(address, fromChain)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid source address: %w", err)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"direct address conversion between %s and %s is not supported without public key",
|
||||
fromChain,
|
||||
toChain,
|
||||
)
|
||||
}
|
||||
|
||||
// AddressBatch represents a batch of addresses derived together
|
||||
type AddressBatch struct {
|
||||
Addresses []AddressDerivation `json:"addresses"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// DeriveAddressBatch derives multiple addresses from different sources
|
||||
func (am *AddressManager) DeriveAddressBatch(requests []AddressRequest) (*AddressBatch, error) {
|
||||
batch := &AddressBatch{
|
||||
Addresses: make([]AddressDerivation, 0, len(requests)),
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
|
||||
for i, req := range requests {
|
||||
var derivation *AddressDerivation
|
||||
var err error
|
||||
|
||||
switch req.Type {
|
||||
case "public_key":
|
||||
derivation, err = am.DeriveFromPublicKey(req.PublicKey, req.Prefix)
|
||||
case "entropy":
|
||||
derivation, err = am.DeriveFromEntropy(req.DID, req.Salt, req.Prefix)
|
||||
case "mpc_enclave":
|
||||
if req.EnclaveData == nil {
|
||||
err = fmt.Errorf("enclave data required for MPC derivation")
|
||||
} else {
|
||||
derivation, err = am.DeriveFromMPCEnclave(req.EnclaveData, req.Prefix)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unsupported derivation type: %s", req.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive address for request %d: %w", i, err)
|
||||
}
|
||||
|
||||
batch.Addresses = append(batch.Addresses, *derivation)
|
||||
}
|
||||
|
||||
batch.Metadata["total_addresses"] = fmt.Sprintf("%d", len(batch.Addresses))
|
||||
batch.Metadata["cosmos_prefix"] = am.cosmosPrefix
|
||||
|
||||
return batch, nil
|
||||
}
|
||||
|
||||
// AddressRequest represents a request to derive addresses
|
||||
type AddressRequest struct {
|
||||
Type string `json:"type"`
|
||||
PublicKey []byte `json:"public_key,omitempty"`
|
||||
DID string `json:"did,omitempty"`
|
||||
Salt string `json:"salt,omitempty"`
|
||||
EnclaveData *mpc.EnclaveData `json:"enclave_data,omitempty"`
|
||||
Prefix string `json:"prefix,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
package txns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"cosmossdk.io/math"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/sonr-io/sonr/types/coins"
|
||||
)
|
||||
|
||||
// TransactionBuilder is the main builder interface that wraps both Cosmos and EVM builders
|
||||
type TransactionBuilder struct {
|
||||
cosmosBuilder *CosmosBuilder
|
||||
evmBuilder *EVMBuilder
|
||||
coinsManager *coins.Manager
|
||||
defaultChainID string
|
||||
defaultEncoding EncodingType
|
||||
}
|
||||
|
||||
// NewTransactionBuilder creates a new transaction builder
|
||||
func NewTransactionBuilder(coinsManager *coins.Manager, defaultChainID string) *TransactionBuilder {
|
||||
return &TransactionBuilder{
|
||||
coinsManager: coinsManager,
|
||||
defaultChainID: defaultChainID,
|
||||
defaultEncoding: EncodingTypeProtobuf,
|
||||
}
|
||||
}
|
||||
|
||||
// Cosmos returns a Cosmos transaction builder
|
||||
func (tb *TransactionBuilder) Cosmos(clientCtx client.Context) *CosmosBuilder {
|
||||
if tb.cosmosBuilder == nil {
|
||||
tb.cosmosBuilder = NewCosmosBuilder(clientCtx, tb.defaultChainID, tb.defaultEncoding)
|
||||
}
|
||||
return tb.cosmosBuilder
|
||||
}
|
||||
|
||||
// EVM returns an EVM transaction builder
|
||||
func (tb *TransactionBuilder) EVM(chainID *big.Int) *EVMBuilder {
|
||||
if tb.evmBuilder == nil {
|
||||
tb.evmBuilder = NewEVMBuilder(chainID)
|
||||
}
|
||||
return tb.evmBuilder
|
||||
}
|
||||
|
||||
// CreateSigner creates a signer from wallet
|
||||
func (tb *TransactionBuilder) CreateSigner(
|
||||
wallet *coins.Wallet,
|
||||
chainType TransactionType,
|
||||
) (Signer, error) {
|
||||
return NewWalletSigner(wallet, chainType)
|
||||
}
|
||||
|
||||
// DeriveAddresses derives addresses for both chains
|
||||
func (tb *TransactionBuilder) DeriveAddresses(did, salt string) (*AddressDerivation, error) {
|
||||
cosmosAddr, evmAddr, derivationPath, err := tb.coinsManager.DeriveAddresses(did, salt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive addresses: %w", err)
|
||||
}
|
||||
|
||||
// Get public key from wallet creation
|
||||
wallet, err := tb.coinsManager.CreateWalletFromEntropy(did, salt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create wallet for public key: %w", err)
|
||||
}
|
||||
|
||||
return &AddressDerivation{
|
||||
CosmosAddress: cosmosAddr,
|
||||
EVMAddress: evmAddr,
|
||||
DerivationPath: derivationPath,
|
||||
PublicKey: wallet.GetCosmosPublicKey().Bytes(),
|
||||
ChainType: "multi-chain",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CosmosBuilder builds Cosmos transactions
|
||||
type CosmosBuilder struct {
|
||||
clientCtx client.Context
|
||||
chainID string
|
||||
encoding EncodingType
|
||||
gasLimit uint64
|
||||
gasPrice sdk.DecCoin
|
||||
memo string
|
||||
timeoutHeight uint64
|
||||
coinsTxBuilder *coins.CosmosTransactionBuilder
|
||||
}
|
||||
|
||||
// NewCosmosBuilder creates a new Cosmos transaction builder
|
||||
func NewCosmosBuilder(
|
||||
clientCtx client.Context,
|
||||
chainID string,
|
||||
encoding EncodingType,
|
||||
) *CosmosBuilder {
|
||||
coinsTxBuilder := coins.NewCosmosTransactionBuilder(clientCtx, chainID)
|
||||
|
||||
return &CosmosBuilder{
|
||||
clientCtx: clientCtx,
|
||||
chainID: chainID,
|
||||
encoding: encoding,
|
||||
gasLimit: 200000,
|
||||
gasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
|
||||
coinsTxBuilder: coinsTxBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
// SetChainID implements Builder interface
|
||||
func (cb *CosmosBuilder) SetChainID(chainID string) Builder {
|
||||
cb.chainID = chainID
|
||||
return cb
|
||||
}
|
||||
|
||||
// SetGas implements Builder interface
|
||||
func (cb *CosmosBuilder) SetGas(limit uint64, price any) Builder {
|
||||
cb.gasLimit = limit
|
||||
if gasPrice, ok := price.(sdk.DecCoin); ok {
|
||||
cb.gasPrice = gasPrice
|
||||
cb.coinsTxBuilder.SetGas(limit, gasPrice)
|
||||
}
|
||||
return cb
|
||||
}
|
||||
|
||||
// SetMemo implements Builder interface
|
||||
func (cb *CosmosBuilder) SetMemo(memo string) Builder {
|
||||
cb.memo = memo
|
||||
cb.coinsTxBuilder.SetMemo(memo)
|
||||
return cb
|
||||
}
|
||||
|
||||
// SetTimeoutHeight sets the timeout height for the transaction
|
||||
func (cb *CosmosBuilder) SetTimeoutHeight(height uint64) *CosmosBuilder {
|
||||
cb.timeoutHeight = height
|
||||
return cb
|
||||
}
|
||||
|
||||
// SetEncoding sets the encoding type
|
||||
func (cb *CosmosBuilder) SetEncoding(encoding EncodingType) *CosmosBuilder {
|
||||
cb.encoding = encoding
|
||||
return cb
|
||||
}
|
||||
|
||||
// BuildUnsigned implements Builder interface
|
||||
func (cb *CosmosBuilder) BuildUnsigned(params Params) (UnsignedTransaction, error) {
|
||||
cosmosParams, ok := params.(*CosmosTransactionParams)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
if err := cosmosParams.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
// Use the underlying coins transaction builder
|
||||
txBuilder, err := cb.coinsTxBuilder.BuildCustomTransaction(cosmosParams.Messages)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build transaction: %w", err)
|
||||
}
|
||||
|
||||
// Set additional parameters
|
||||
if cosmosParams.TimeoutHeight > 0 {
|
||||
txBuilder.SetTimeoutHeight(cosmosParams.TimeoutHeight)
|
||||
}
|
||||
|
||||
return &CosmosUnsignedTx{
|
||||
TxBuilder: txBuilder,
|
||||
ChainID: cb.chainID,
|
||||
Encoding: cb.encoding,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EstimateFee implements Builder interface
|
||||
func (cb *CosmosBuilder) EstimateFee(params Params) (*FeeEstimation, error) {
|
||||
cosmosParams, ok := params.(*CosmosTransactionParams)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
if err := cosmosParams.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
// Simple fee estimation based on gas limit and price
|
||||
fee := cb.gasPrice.Amount.MulInt64(int64(cb.gasLimit))
|
||||
totalFee := sdk.NewCoins(sdk.NewCoin(cb.gasPrice.Denom, fee.TruncateInt()))
|
||||
|
||||
return &FeeEstimation{
|
||||
GasLimit: cb.gasLimit,
|
||||
GasPrice: cb.gasPrice,
|
||||
Fee: totalFee,
|
||||
Total: totalFee.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTransactionType implements Builder interface
|
||||
func (cb *CosmosBuilder) GetTransactionType() TransactionType {
|
||||
return TransactionTypeCosmos
|
||||
}
|
||||
|
||||
// BuildSendTransaction builds a simple send transaction
|
||||
func (cb *CosmosBuilder) BuildSendTransaction(
|
||||
fromAddr, toAddr string,
|
||||
amount sdk.Coins,
|
||||
) (UnsignedTransaction, error) {
|
||||
txBuilder, err := cb.coinsTxBuilder.BuildSendTransaction(fromAddr, toAddr, amount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build send transaction: %w", err)
|
||||
}
|
||||
|
||||
return &CosmosUnsignedTx{
|
||||
TxBuilder: txBuilder,
|
||||
ChainID: cb.chainID,
|
||||
Encoding: cb.encoding,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EVMBuilder builds EVM transactions
|
||||
type EVMBuilder struct {
|
||||
chainID *big.Int
|
||||
gasLimit uint64
|
||||
gasPrice *big.Int
|
||||
maxFeePerGas *big.Int
|
||||
maxPriorityFeePerGas *big.Int
|
||||
nonce uint64
|
||||
coinsEthBuilder *coins.EthereumTransactionBuilder
|
||||
}
|
||||
|
||||
// NewEVMBuilder creates a new EVM transaction builder
|
||||
func NewEVMBuilder(chainID *big.Int) *EVMBuilder {
|
||||
coinsEthBuilder := coins.NewEthereumTransactionBuilder(chainID)
|
||||
|
||||
return &EVMBuilder{
|
||||
chainID: chainID,
|
||||
gasLimit: 21000,
|
||||
gasPrice: big.NewInt(20000000000), // 20 Gwei
|
||||
coinsEthBuilder: coinsEthBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
// SetChainID implements Builder interface
|
||||
func (eb *EVMBuilder) SetChainID(chainID string) Builder {
|
||||
if chainIDBig, ok := new(big.Int).SetString(chainID, 10); ok {
|
||||
eb.chainID = chainIDBig
|
||||
}
|
||||
return eb
|
||||
}
|
||||
|
||||
// SetGas implements Builder interface
|
||||
func (eb *EVMBuilder) SetGas(limit uint64, price any) Builder {
|
||||
eb.gasLimit = limit
|
||||
if gasPrice, ok := price.(*big.Int); ok {
|
||||
eb.gasPrice = gasPrice
|
||||
eb.coinsEthBuilder.SetGas(limit, gasPrice)
|
||||
}
|
||||
return eb
|
||||
}
|
||||
|
||||
// SetMemo implements Builder interface (no-op for EVM)
|
||||
func (eb *EVMBuilder) SetMemo(memo string) Builder {
|
||||
// EVM transactions don't have memos
|
||||
return eb
|
||||
}
|
||||
|
||||
// SetNonce sets the nonce for the transaction
|
||||
func (eb *EVMBuilder) SetNonce(nonce uint64) *EVMBuilder {
|
||||
eb.nonce = nonce
|
||||
eb.coinsEthBuilder.SetNonce(nonce)
|
||||
return eb
|
||||
}
|
||||
|
||||
// SetMaxFee sets the max fee per gas for EIP-1559 transactions
|
||||
func (eb *EVMBuilder) SetMaxFee(maxFeePerGas, maxPriorityFeePerGas *big.Int) *EVMBuilder {
|
||||
eb.maxFeePerGas = maxFeePerGas
|
||||
eb.maxPriorityFeePerGas = maxPriorityFeePerGas
|
||||
return eb
|
||||
}
|
||||
|
||||
// BuildUnsigned implements Builder interface
|
||||
func (eb *EVMBuilder) BuildUnsigned(params Params) (UnsignedTransaction, error) {
|
||||
evmParams, ok := params.(*EVMTransactionParams)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
if err := evmParams.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
var tx *ethtypes.Transaction
|
||||
|
||||
// Choose transaction type based on fee parameters
|
||||
if evmParams.MaxFeePerGas != nil && evmParams.MaxPriorityFeePerGas != nil {
|
||||
// EIP-1559 transaction
|
||||
tx = eb.coinsEthBuilder.BuildEIP1559Transaction(
|
||||
*evmParams.To,
|
||||
evmParams.Value,
|
||||
evmParams.MaxFeePerGas,
|
||||
evmParams.MaxPriorityFeePerGas,
|
||||
evmParams.Data,
|
||||
)
|
||||
} else if evmParams.Data != nil && len(evmParams.Data) > 0 {
|
||||
// Contract transaction
|
||||
tx = eb.coinsEthBuilder.BuildContractTransaction(
|
||||
*evmParams.To,
|
||||
evmParams.Value,
|
||||
evmParams.Data,
|
||||
)
|
||||
} else {
|
||||
// Simple transfer
|
||||
tx = eb.coinsEthBuilder.BuildTransferTransaction(
|
||||
*evmParams.To,
|
||||
evmParams.Value,
|
||||
)
|
||||
}
|
||||
|
||||
return &EVMUnsignedTx{
|
||||
Transaction: tx,
|
||||
ChainID: eb.chainID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EstimateFee implements Builder interface
|
||||
func (eb *EVMBuilder) EstimateFee(params Params) (*FeeEstimation, error) {
|
||||
evmParams, ok := params.(*EVMTransactionParams)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
if err := evmParams.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
// Simple fee estimation
|
||||
totalFee := new(big.Int).Mul(eb.gasPrice, big.NewInt(int64(eb.gasLimit)))
|
||||
|
||||
return &FeeEstimation{
|
||||
GasLimit: eb.gasLimit,
|
||||
GasPrice: eb.gasPrice,
|
||||
Fee: totalFee,
|
||||
Total: totalFee.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTransactionType implements Builder interface
|
||||
func (eb *EVMBuilder) GetTransactionType() TransactionType {
|
||||
return TransactionTypeEVM
|
||||
}
|
||||
|
||||
// EstimateGas estimates gas for a transaction
|
||||
func (eb *EVMBuilder) EstimateGas(
|
||||
ctx context.Context,
|
||||
params *EVMTransactionParams,
|
||||
) (uint64, error) {
|
||||
// Use the underlying coins builder for gas estimation
|
||||
tx := eb.coinsEthBuilder.BuildContractTransaction(*params.To, params.Value, params.Data)
|
||||
return eb.coinsEthBuilder.EstimateGas(ctx, tx)
|
||||
}
|
||||
|
||||
// WalletSigner implements Signer interface using coins.Wallet
|
||||
type WalletSigner struct {
|
||||
wallet *coins.Wallet
|
||||
chainType TransactionType
|
||||
}
|
||||
|
||||
// NewWalletSigner creates a new wallet-based signer
|
||||
func NewWalletSigner(wallet *coins.Wallet, chainType TransactionType) (*WalletSigner, error) {
|
||||
if wallet == nil {
|
||||
return nil, fmt.Errorf("wallet cannot be nil")
|
||||
}
|
||||
|
||||
return &WalletSigner{
|
||||
wallet: wallet,
|
||||
chainType: chainType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sign implements Signer interface
|
||||
func (ws *WalletSigner) Sign(txBytes []byte) ([]byte, error) {
|
||||
switch ws.chainType {
|
||||
case TransactionTypeCosmos:
|
||||
return ws.wallet.SignMessage(txBytes)
|
||||
case TransactionTypeEVM:
|
||||
return ws.wallet.SignEthereumMessage(txBytes)
|
||||
default:
|
||||
return nil, ErrUnsupportedChainType
|
||||
}
|
||||
}
|
||||
|
||||
// GetPublicKey implements Signer interface
|
||||
func (ws *WalletSigner) GetPublicKey() []byte {
|
||||
switch ws.chainType {
|
||||
case TransactionTypeCosmos:
|
||||
return ws.wallet.GetCosmosPublicKey().Bytes()
|
||||
case TransactionTypeEVM:
|
||||
pubKey := ws.wallet.GetEthereumPublicKey()
|
||||
// Convert ECDSA public key to bytes
|
||||
return append(pubKey.X.Bytes(), pubKey.Y.Bytes()...)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetAddress implements Signer interface
|
||||
func (ws *WalletSigner) GetAddress(chainType TransactionType) (string, error) {
|
||||
switch chainType {
|
||||
case TransactionTypeCosmos:
|
||||
return ws.wallet.CosmosAddress, nil
|
||||
case TransactionTypeEVM:
|
||||
return ws.wallet.EthereumAddress, nil
|
||||
default:
|
||||
return "", ErrUnsupportedChainType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
package txns
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"cosmossdk.io/math"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/std"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/sonr-io/sonr/types/coins"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestClientContext() client.Context {
|
||||
// Create a test codec
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
|
||||
// Create TX config
|
||||
txConfig := authtx.NewTxConfig(marshaler, authtx.DefaultSignModes)
|
||||
|
||||
return client.Context{}.
|
||||
WithCodec(marshaler).
|
||||
WithTxConfig(txConfig).
|
||||
WithInterfaceRegistry(interfaceRegistry)
|
||||
}
|
||||
|
||||
func TestNewTransactionBuilder(t *testing.T) {
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
assert.NotNil(t, tb)
|
||||
assert.Equal(t, "sonr-1", tb.defaultChainID)
|
||||
assert.Equal(t, EncodingTypeProtobuf, tb.defaultEncoding)
|
||||
}
|
||||
|
||||
func TestCosmosBuilder(t *testing.T) {
|
||||
clientCtx := setupTestClientContext()
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
cosmosBuilder := tb.Cosmos(clientCtx)
|
||||
require.NotNil(t, cosmosBuilder)
|
||||
|
||||
// Test setting gas
|
||||
cosmosBuilder.SetGas(300000, sdk.NewDecCoin("usnr", math.NewInt(2000)))
|
||||
assert.Equal(t, uint64(300000), cosmosBuilder.gasLimit)
|
||||
assert.Equal(t, "usnr", cosmosBuilder.gasPrice.Denom)
|
||||
|
||||
// Test setting memo
|
||||
cosmosBuilder.SetMemo("test memo")
|
||||
assert.Equal(t, "test memo", cosmosBuilder.memo)
|
||||
|
||||
// Test setting chain ID
|
||||
builder := cosmosBuilder.SetChainID("test-chain")
|
||||
assert.Equal(t, "test-chain", cosmosBuilder.chainID)
|
||||
assert.Equal(t, cosmosBuilder, builder) // Should return self for chaining
|
||||
}
|
||||
|
||||
func TestCosmosBuilder_BuildSendTransaction(t *testing.T) {
|
||||
clientCtx := setupTestClientContext()
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
cosmosBuilder := tb.Cosmos(clientCtx)
|
||||
|
||||
// Test addresses (using valid bech32 format)
|
||||
fromAddr := "snr1qpqz4vf2t0n0tqy3qy3qy3qy3qy3qy3qynfuxx"
|
||||
toAddr := "snr1qpqz4vf2t0n0tqy3qy3qy3qy3qy3qy3qy2aaaa"
|
||||
amount := sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000000)))
|
||||
|
||||
// Note: This test may fail due to invalid addresses, but tests the interface
|
||||
unsignedTx, err := cosmosBuilder.BuildSendTransaction(fromAddr, toAddr, amount)
|
||||
|
||||
// The specific implementation might fail due to address validation,
|
||||
// but we can test that the method exists and returns the expected type
|
||||
if err == nil {
|
||||
assert.NotNil(t, unsignedTx)
|
||||
assert.Equal(t, TransactionTypeCosmos, unsignedTx.GetType())
|
||||
assert.Equal(t, EncodingTypeProtobuf, unsignedTx.GetEncoding())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCosmosBuilder_BuildUnsigned(t *testing.T) {
|
||||
clientCtx := setupTestClientContext()
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
cosmosBuilder := tb.Cosmos(clientCtx)
|
||||
|
||||
// Create test message
|
||||
testMsg := &banktypes.MsgSend{
|
||||
FromAddress: "snr1test",
|
||||
ToAddress: "snr1test2",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
}
|
||||
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{testMsg},
|
||||
GasLimit: 200000,
|
||||
GasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
|
||||
Memo: "test transaction",
|
||||
TimeoutHeight: 1000,
|
||||
}
|
||||
|
||||
unsignedTx, err := cosmosBuilder.BuildUnsigned(params)
|
||||
|
||||
// May fail due to address validation, but tests interface
|
||||
if err == nil {
|
||||
assert.NotNil(t, unsignedTx)
|
||||
assert.Equal(t, TransactionTypeCosmos, unsignedTx.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCosmosBuilder_EstimateFee(t *testing.T) {
|
||||
clientCtx := setupTestClientContext()
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
cosmosBuilder := tb.Cosmos(clientCtx)
|
||||
|
||||
testMsg := &banktypes.MsgSend{
|
||||
FromAddress: "snr1test",
|
||||
ToAddress: "snr1test2",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
}
|
||||
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{testMsg},
|
||||
GasLimit: 200000,
|
||||
GasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
|
||||
}
|
||||
|
||||
feeEst, err := cosmosBuilder.EstimateFee(params)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, feeEst)
|
||||
assert.Equal(t, uint64(200000), feeEst.GasLimit)
|
||||
assert.NotNil(t, feeEst.Fee)
|
||||
assert.NotEmpty(t, feeEst.Total)
|
||||
}
|
||||
|
||||
func TestEVMBuilder(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", chainID)
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
evmBuilder := tb.EVM(chainID)
|
||||
require.NotNil(t, evmBuilder)
|
||||
|
||||
// Test setting gas
|
||||
evmBuilder.SetGas(100000, big.NewInt(20000000000))
|
||||
assert.Equal(t, uint64(100000), evmBuilder.gasLimit)
|
||||
assert.Equal(t, big.NewInt(20000000000), evmBuilder.gasPrice)
|
||||
|
||||
// Test setting nonce
|
||||
evmBuilder.SetNonce(42)
|
||||
assert.Equal(t, uint64(42), evmBuilder.nonce)
|
||||
|
||||
// Test setting max fees
|
||||
maxFee := big.NewInt(30000000000)
|
||||
maxPriorityFee := big.NewInt(2000000000)
|
||||
evmBuilder.SetMaxFee(maxFee, maxPriorityFee)
|
||||
assert.Equal(t, maxFee, evmBuilder.maxFeePerGas)
|
||||
assert.Equal(t, maxPriorityFee, evmBuilder.maxPriorityFeePerGas)
|
||||
}
|
||||
|
||||
func TestEVMBuilder_BuildUnsigned(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", chainID)
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
evmBuilder := tb.EVM(chainID)
|
||||
|
||||
// Test transfer transaction
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
value := big.NewInt(1000000000000000000) // 1 ETH
|
||||
|
||||
params := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: value,
|
||||
GasLimit: 21000,
|
||||
GasPrice: big.NewInt(20000000000),
|
||||
Nonce: 0,
|
||||
ChainID: chainID,
|
||||
}
|
||||
|
||||
unsignedTx, err := evmBuilder.BuildUnsigned(params)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, unsignedTx)
|
||||
|
||||
assert.Equal(t, TransactionTypeEVM, unsignedTx.GetType())
|
||||
assert.Equal(t, EncodingTypeRLP, unsignedTx.GetEncoding())
|
||||
|
||||
// Test EIP-1559 transaction
|
||||
params.MaxFeePerGas = big.NewInt(30000000000)
|
||||
params.MaxPriorityFeePerGas = big.NewInt(2000000000)
|
||||
|
||||
unsignedTx1559, err := evmBuilder.BuildUnsigned(params)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, unsignedTx1559)
|
||||
|
||||
assert.Equal(t, TransactionTypeEVM, unsignedTx1559.GetType())
|
||||
}
|
||||
|
||||
func TestEVMBuilder_EstimateFee(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", chainID)
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
evmBuilder := tb.EVM(chainID)
|
||||
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
params := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000),
|
||||
GasLimit: 21000,
|
||||
GasPrice: big.NewInt(20000000000),
|
||||
ChainID: big.NewInt(1),
|
||||
}
|
||||
|
||||
feeEst, err := evmBuilder.EstimateFee(params)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, feeEst)
|
||||
assert.Equal(t, uint64(21000), feeEst.GasLimit)
|
||||
assert.NotNil(t, feeEst.Fee)
|
||||
assert.NotEmpty(t, feeEst.Total)
|
||||
}
|
||||
|
||||
func TestWalletSigner(t *testing.T) {
|
||||
// Create a test wallet using entropy
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
wallet, err := coinsManager.CreateWalletFromEntropy("did:test", "salt123")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test Cosmos signer
|
||||
cosmosSigner, err := NewWalletSigner(wallet, TransactionTypeCosmos)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cosmosSigner)
|
||||
|
||||
pubKey := cosmosSigner.GetPublicKey()
|
||||
assert.NotNil(t, pubKey)
|
||||
assert.Greater(t, len(pubKey), 0)
|
||||
|
||||
addr, err := cosmosSigner.GetAddress(TransactionTypeCosmos)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wallet.CosmosAddress, addr)
|
||||
|
||||
// Test EVM signer
|
||||
evmSigner, err := NewWalletSigner(wallet, TransactionTypeEVM)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, evmSigner)
|
||||
|
||||
evmPubKey := evmSigner.GetPublicKey()
|
||||
assert.NotNil(t, evmPubKey)
|
||||
assert.Greater(t, len(evmPubKey), 0)
|
||||
|
||||
evmAddr, err := evmSigner.GetAddress(TransactionTypeEVM)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wallet.EthereumAddress, evmAddr)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_DeriveAddresses(t *testing.T) {
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
derivation, err := tb.DeriveAddresses("did:test", "salt123")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, derivation)
|
||||
|
||||
assert.NotEmpty(t, derivation.CosmosAddress)
|
||||
assert.NotEmpty(t, derivation.EVMAddress)
|
||||
assert.NotEmpty(t, derivation.DerivationPath)
|
||||
assert.NotNil(t, derivation.PublicKey)
|
||||
assert.Equal(t, "multi-chain", derivation.ChainType)
|
||||
}
|
||||
|
||||
func TestTransactionTypes(t *testing.T) {
|
||||
// Test transaction type constants
|
||||
assert.Equal(t, "cosmos", string(TransactionTypeCosmos))
|
||||
assert.Equal(t, "evm", string(TransactionTypeEVM))
|
||||
assert.Equal(t, "unknown", string(TransactionTypeUnknown))
|
||||
}
|
||||
|
||||
func TestEncodingTypes(t *testing.T) {
|
||||
// Test encoding type constants
|
||||
assert.Equal(t, "amino", string(EncodingTypeAmino))
|
||||
assert.Equal(t, "protobuf", string(EncodingTypeProtobuf))
|
||||
assert.Equal(t, "rlp", string(EncodingTypeRLP))
|
||||
}
|
||||
|
||||
func TestCosmosUnsignedTx(t *testing.T) {
|
||||
clientCtx := setupTestClientContext()
|
||||
txBuilder := clientCtx.TxConfig.NewTxBuilder()
|
||||
|
||||
unsignedTx := &CosmosUnsignedTx{
|
||||
TxBuilder: txBuilder,
|
||||
ChainID: "test-chain",
|
||||
Encoding: EncodingTypeProtobuf,
|
||||
}
|
||||
|
||||
assert.Equal(t, TransactionTypeCosmos, unsignedTx.GetType())
|
||||
assert.Equal(t, EncodingTypeProtobuf, unsignedTx.GetEncoding())
|
||||
assert.Equal(t, txBuilder, unsignedTx.GetRaw())
|
||||
|
||||
signBytes, err := unsignedTx.GetSignBytes()
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, signBytes)
|
||||
|
||||
// Test signing
|
||||
signature := []byte("test-signature")
|
||||
pubKey := []byte("test-pubkey")
|
||||
|
||||
signedTx, err := unsignedTx.Sign(signature, pubKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signedTx)
|
||||
|
||||
assert.Equal(t, TransactionTypeCosmos, signedTx.GetType())
|
||||
assert.Equal(t, EncodingTypeProtobuf, signedTx.GetEncoding())
|
||||
}
|
||||
|
||||
func TestEVMUnsignedTx(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
|
||||
// Create a test transaction
|
||||
tx := ðtypes.LegacyTx{
|
||||
Nonce: 0,
|
||||
To: &common.Address{},
|
||||
Value: big.NewInt(1000),
|
||||
Gas: 21000,
|
||||
GasPrice: big.NewInt(20000000000),
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
unsignedTx := &EVMUnsignedTx{
|
||||
Transaction: ethtypes.NewTx(tx),
|
||||
ChainID: chainID,
|
||||
}
|
||||
|
||||
assert.Equal(t, TransactionTypeEVM, unsignedTx.GetType())
|
||||
assert.Equal(t, EncodingTypeRLP, unsignedTx.GetEncoding())
|
||||
|
||||
signBytes, err := unsignedTx.GetSignBytes()
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, signBytes)
|
||||
assert.Equal(t, 32, len(signBytes)) // Hash should be 32 bytes
|
||||
|
||||
// Test signing
|
||||
signature := []byte("test-signature")
|
||||
pubKey := []byte("test-pubkey")
|
||||
|
||||
signedTx, err := unsignedTx.Sign(signature, pubKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signedTx)
|
||||
|
||||
assert.Equal(t, TransactionTypeEVM, signedTx.GetType())
|
||||
assert.Equal(t, EncodingTypeRLP, signedTx.GetEncoding())
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkCosmosBuilder_BuildUnsigned(b *testing.B) {
|
||||
clientCtx := setupTestClientContext()
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
cosmosBuilder := tb.Cosmos(clientCtx)
|
||||
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: "snr1test",
|
||||
ToAddress: "snr1test2",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
},
|
||||
},
|
||||
GasLimit: 200000,
|
||||
GasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = cosmosBuilder.BuildUnsigned(params)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEVMBuilder_BuildUnsigned(b *testing.B) {
|
||||
chainID := big.NewInt(1)
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", chainID)
|
||||
tb := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
evmBuilder := tb.EVM(chainID)
|
||||
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
params := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000),
|
||||
GasLimit: 21000,
|
||||
GasPrice: big.NewInt(20000000000),
|
||||
Nonce: 0,
|
||||
ChainID: chainID,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = evmBuilder.BuildUnsigned(params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package txns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// Encoder interface for encoding transactions
|
||||
type Encoder interface {
|
||||
// EncodeTx encodes a transaction
|
||||
EncodeTx(tx any) ([]byte, error)
|
||||
// DecodeTx decodes a transaction
|
||||
DecodeTx(data []byte) (any, error)
|
||||
// GetEncodingType returns the encoding type
|
||||
GetEncodingType() EncodingType
|
||||
}
|
||||
|
||||
// CosmosProtobufEncoder encodes/decodes Cosmos transactions using Protobuf
|
||||
type CosmosProtobufEncoder struct {
|
||||
txConfig client.TxConfig
|
||||
cdc codec.Codec
|
||||
}
|
||||
|
||||
// NewCosmosProtobufEncoder creates a new Protobuf encoder for Cosmos
|
||||
func NewCosmosProtobufEncoder(clientCtx client.Context) *CosmosProtobufEncoder {
|
||||
return &CosmosProtobufEncoder{
|
||||
txConfig: clientCtx.TxConfig,
|
||||
cdc: clientCtx.Codec,
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeTx implements Encoder interface
|
||||
func (e *CosmosProtobufEncoder) EncodeTx(tx any) ([]byte, error) {
|
||||
switch t := tx.(type) {
|
||||
case client.TxBuilder:
|
||||
return e.txConfig.TxEncoder()(t.GetTx())
|
||||
case sdk.Tx:
|
||||
return e.txConfig.TxEncoder()(t)
|
||||
case *CosmosSignedTx:
|
||||
return e.txConfig.TxEncoder()(t.TxBuilder.GetTx())
|
||||
case *CosmosUnsignedTx:
|
||||
return e.txConfig.TxEncoder()(t.TxBuilder.GetTx())
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported transaction type for Protobuf encoding: %T", tx)
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeTx implements Encoder interface
|
||||
func (e *CosmosProtobufEncoder) DecodeTx(data []byte) (any, error) {
|
||||
tx, err := e.txConfig.TxDecoder()(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode Protobuf transaction: %w", err)
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// GetEncodingType implements Encoder interface
|
||||
func (e *CosmosProtobufEncoder) GetEncodingType() EncodingType {
|
||||
return EncodingTypeProtobuf
|
||||
}
|
||||
|
||||
// CosmosAminoEncoder encodes/decodes Cosmos transactions using Amino
|
||||
type CosmosAminoEncoder struct {
|
||||
cdc *codec.LegacyAmino
|
||||
}
|
||||
|
||||
// NewCosmosAminoEncoder creates a new Amino encoder for Cosmos
|
||||
func NewCosmosAminoEncoder() *CosmosAminoEncoder {
|
||||
// Create a legacy amino codec
|
||||
cdc := codec.NewLegacyAmino()
|
||||
sdk.RegisterLegacyAminoCodec(cdc)
|
||||
return &CosmosAminoEncoder{
|
||||
cdc: cdc,
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeTx implements Encoder interface
|
||||
func (e *CosmosAminoEncoder) EncodeTx(tx any) ([]byte, error) {
|
||||
switch t := tx.(type) {
|
||||
case sdk.Tx:
|
||||
return e.cdc.Marshal(t)
|
||||
case *CosmosSignedTx:
|
||||
return e.cdc.Marshal(t.TxBuilder.GetTx())
|
||||
case *CosmosUnsignedTx:
|
||||
return e.cdc.Marshal(t.TxBuilder.GetTx())
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported transaction type for Amino encoding: %T", tx)
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeTx implements Encoder interface
|
||||
func (e *CosmosAminoEncoder) DecodeTx(data []byte) (any, error) {
|
||||
var tx sdk.Tx
|
||||
err := e.cdc.Unmarshal(data, &tx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode Amino transaction: %w", err)
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// GetEncodingType implements Encoder interface
|
||||
func (e *CosmosAminoEncoder) GetEncodingType() EncodingType {
|
||||
return EncodingTypeAmino
|
||||
}
|
||||
|
||||
// EVMRLPEncoder encodes/decodes EVM transactions using RLP
|
||||
type EVMRLPEncoder struct{}
|
||||
|
||||
// NewEVMRLPEncoder creates a new RLP encoder for EVM
|
||||
func NewEVMRLPEncoder() *EVMRLPEncoder {
|
||||
return &EVMRLPEncoder{}
|
||||
}
|
||||
|
||||
// EncodeTx implements Encoder interface
|
||||
func (e *EVMRLPEncoder) EncodeTx(tx any) ([]byte, error) {
|
||||
switch t := tx.(type) {
|
||||
case *ethtypes.Transaction:
|
||||
return t.MarshalBinary()
|
||||
case *EVMSignedTx:
|
||||
return t.Transaction.MarshalBinary()
|
||||
case *EVMUnsignedTx:
|
||||
return t.Transaction.MarshalBinary()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported transaction type for RLP encoding: %T", tx)
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeTx implements Encoder interface
|
||||
func (e *EVMRLPEncoder) DecodeTx(data []byte) (any, error) {
|
||||
var tx ethtypes.Transaction
|
||||
err := tx.UnmarshalBinary(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode RLP transaction: %w", err)
|
||||
}
|
||||
return &tx, nil
|
||||
}
|
||||
|
||||
// GetEncodingType implements Encoder interface
|
||||
func (e *EVMRLPEncoder) GetEncodingType() EncodingType {
|
||||
return EncodingTypeRLP
|
||||
}
|
||||
|
||||
// EncoderRegistry manages different encoders
|
||||
type EncoderRegistry struct {
|
||||
encoders map[string]Encoder
|
||||
}
|
||||
|
||||
// NewEncoderRegistry creates a new encoder registry
|
||||
func NewEncoderRegistry() *EncoderRegistry {
|
||||
return &EncoderRegistry{
|
||||
encoders: make(map[string]Encoder),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterEncoder registers an encoder
|
||||
func (r *EncoderRegistry) RegisterEncoder(name string, encoder Encoder) {
|
||||
r.encoders[name] = encoder
|
||||
}
|
||||
|
||||
// GetEncoder retrieves an encoder by name
|
||||
func (r *EncoderRegistry) GetEncoder(name string) (Encoder, error) {
|
||||
encoder, exists := r.encoders[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("encoder not found: %s", name)
|
||||
}
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
// GetEncoderByType retrieves an encoder by encoding type and transaction type
|
||||
func (r *EncoderRegistry) GetEncoderByType(
|
||||
encodingType EncodingType,
|
||||
txType TransactionType,
|
||||
) (Encoder, error) {
|
||||
key := fmt.Sprintf("%s-%s", txType, encodingType)
|
||||
return r.GetEncoder(key)
|
||||
}
|
||||
|
||||
// DefaultEncoderRegistry creates a registry with default encoders
|
||||
func DefaultEncoderRegistry(clientCtx client.Context) *EncoderRegistry {
|
||||
registry := NewEncoderRegistry()
|
||||
|
||||
// Register Cosmos encoders
|
||||
registry.RegisterEncoder(
|
||||
fmt.Sprintf("%s-%s", TransactionTypeCosmos, EncodingTypeProtobuf),
|
||||
NewCosmosProtobufEncoder(clientCtx),
|
||||
)
|
||||
registry.RegisterEncoder(
|
||||
fmt.Sprintf("%s-%s", TransactionTypeCosmos, EncodingTypeAmino),
|
||||
NewCosmosAminoEncoder(),
|
||||
)
|
||||
|
||||
// Register EVM encoder
|
||||
registry.RegisterEncoder(
|
||||
fmt.Sprintf("%s-%s", TransactionTypeEVM, EncodingTypeRLP),
|
||||
NewEVMRLPEncoder(),
|
||||
)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
// TransactionData represents decoded transaction data
|
||||
type TransactionData struct {
|
||||
Type TransactionType `json:"type"`
|
||||
Encoding EncodingType `json:"encoding"`
|
||||
Hash string `json:"hash"`
|
||||
Size int `json:"size"`
|
||||
Raw any `json:"raw"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// DecodeTransaction decodes a transaction and returns structured data
|
||||
func DecodeTransaction(
|
||||
data []byte,
|
||||
encodingType EncodingType,
|
||||
txType TransactionType,
|
||||
clientCtx client.Context,
|
||||
) (*TransactionData, error) {
|
||||
registry := DefaultEncoderRegistry(clientCtx)
|
||||
encoder, err := registry.GetEncoderByType(encodingType, txType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get encoder: %w", err)
|
||||
}
|
||||
|
||||
decodedTx, err := encoder.DecodeTx(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode transaction: %w", err)
|
||||
}
|
||||
|
||||
// Calculate hash based on transaction type
|
||||
var hash string
|
||||
switch txType {
|
||||
case TransactionTypeCosmos:
|
||||
if cosmosTx, ok := decodedTx.(sdk.Tx); ok {
|
||||
// Calculate Cosmos transaction hash
|
||||
txBytes, err := encoder.EncodeTx(cosmosTx)
|
||||
if err == nil {
|
||||
hash = fmt.Sprintf("%x", txBytes[:32]) // Simple hash for demo
|
||||
}
|
||||
}
|
||||
case TransactionTypeEVM:
|
||||
if evmTx, ok := decodedTx.(*ethtypes.Transaction); ok {
|
||||
hash = evmTx.Hash().Hex()
|
||||
}
|
||||
}
|
||||
|
||||
return &TransactionData{
|
||||
Type: txType,
|
||||
Encoding: encodingType,
|
||||
Hash: hash,
|
||||
Size: len(data),
|
||||
Raw: decodedTx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EncodeTransaction encodes a transaction using the specified encoding
|
||||
func EncodeTransaction(
|
||||
tx any,
|
||||
encodingType EncodingType,
|
||||
txType TransactionType,
|
||||
clientCtx client.Context,
|
||||
) ([]byte, error) {
|
||||
registry := DefaultEncoderRegistry(clientCtx)
|
||||
encoder, err := registry.GetEncoderByType(encodingType, txType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get encoder: %w", err)
|
||||
}
|
||||
|
||||
return encoder.EncodeTx(tx)
|
||||
}
|
||||
|
||||
// ConvertEncoding converts a transaction from one encoding to another
|
||||
func ConvertEncoding(
|
||||
data []byte,
|
||||
fromEncoding, toEncoding EncodingType,
|
||||
txType TransactionType,
|
||||
clientCtx client.Context,
|
||||
) ([]byte, error) {
|
||||
// Decode with source encoding
|
||||
decoded, err := DecodeTransaction(data, fromEncoding, txType, clientCtx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode transaction: %w", err)
|
||||
}
|
||||
|
||||
// Encode with target encoding
|
||||
return EncodeTransaction(decoded.Raw, toEncoding, txType, clientCtx)
|
||||
}
|
||||
|
||||
// ValidateTransactionEncoding validates that transaction data is properly encoded
|
||||
func ValidateTransactionEncoding(
|
||||
data []byte,
|
||||
encodingType EncodingType,
|
||||
txType TransactionType,
|
||||
clientCtx client.Context,
|
||||
) error {
|
||||
_, err := DecodeTransaction(data, encodingType, txType, clientCtx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid transaction encoding: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTransactionSize returns the size of an encoded transaction
|
||||
func GetTransactionSize(
|
||||
tx any,
|
||||
encodingType EncodingType,
|
||||
txType TransactionType,
|
||||
clientCtx client.Context,
|
||||
) (int, error) {
|
||||
data, err := EncodeTransaction(tx, encodingType, txType, clientCtx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to encode transaction: %w", err)
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package txns
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrUnsupportedChainType is returned when an unsupported chain type is used
|
||||
ErrUnsupportedChainType = errors.New("unsupported chain type")
|
||||
|
||||
// ErrInvalidTransactionParams is returned when transaction parameters are invalid
|
||||
ErrInvalidTransactionParams = errors.New("invalid transaction parameters")
|
||||
|
||||
// ErrInvalidGasParams is returned when gas parameters are invalid
|
||||
ErrInvalidGasParams = errors.New("invalid gas parameters")
|
||||
|
||||
// ErrInvalidAddress is returned when an address is invalid
|
||||
ErrInvalidAddress = errors.New("invalid address")
|
||||
|
||||
// ErrInvalidSignature is returned when a signature is invalid
|
||||
ErrInvalidSignature = errors.New("invalid signature")
|
||||
|
||||
// ErrSigningFailed is returned when transaction signing fails
|
||||
ErrSigningFailed = errors.New("transaction signing failed")
|
||||
|
||||
// ErrEncodingFailed is returned when transaction encoding fails
|
||||
ErrEncodingFailed = errors.New("transaction encoding failed")
|
||||
|
||||
// ErrDecodingFailed is returned when transaction decoding fails
|
||||
ErrDecodingFailed = errors.New("transaction decoding failed")
|
||||
|
||||
// ErrFeeEstimationFailed is returned when fee estimation fails
|
||||
ErrFeeEstimationFailed = errors.New("fee estimation failed")
|
||||
|
||||
// ErrInsufficientFunds is returned when account has insufficient funds
|
||||
ErrInsufficientFunds = errors.New("insufficient funds")
|
||||
|
||||
// ErrNonceTooLow is returned when transaction nonce is too low
|
||||
ErrNonceTooLow = errors.New("nonce too low")
|
||||
|
||||
// ErrNonceTooHigh is returned when transaction nonce is too high
|
||||
ErrNonceTooHigh = errors.New("nonce too high")
|
||||
|
||||
// ErrGasPriceTooLow is returned when gas price is too low
|
||||
ErrGasPriceTooLow = errors.New("gas price too low")
|
||||
|
||||
// ErrGasLimitTooLow is returned when gas limit is too low
|
||||
ErrGasLimitTooLow = errors.New("gas limit too low")
|
||||
|
||||
// ErrGasLimitTooHigh is returned when gas limit is too high
|
||||
ErrGasLimitTooHigh = errors.New("gas limit too high")
|
||||
|
||||
// ErrTransactionTooLarge is returned when transaction is too large
|
||||
ErrTransactionTooLarge = errors.New("transaction too large")
|
||||
|
||||
// ErrMemoTooLarge is returned when transaction memo is too large
|
||||
ErrMemoTooLarge = errors.New("memo too large")
|
||||
|
||||
// ErrTimeoutHeightInvalid is returned when timeout height is invalid
|
||||
ErrTimeoutHeightInvalid = errors.New("timeout height invalid")
|
||||
|
||||
// ErrAccountNotFound is returned when account is not found
|
||||
ErrAccountNotFound = errors.New("account not found")
|
||||
|
||||
// ErrSequenceMismatch is returned when account sequence doesn't match
|
||||
ErrSequenceMismatch = errors.New("sequence mismatch")
|
||||
|
||||
// ErrChainIDMismatch is returned when chain ID doesn't match
|
||||
ErrChainIDMismatch = errors.New("chain ID mismatch")
|
||||
|
||||
// ErrInvalidPublicKey is returned when public key is invalid
|
||||
ErrInvalidPublicKey = errors.New("invalid public key")
|
||||
|
||||
// ErrMPCEnclaveNotInitialized is returned when MPC enclave is not initialized
|
||||
ErrMPCEnclaveNotInitialized = errors.New("MPC enclave not initialized")
|
||||
|
||||
// ErrAddressDerivationFailed is returned when address derivation fails
|
||||
ErrAddressDerivationFailed = errors.New("address derivation failed")
|
||||
|
||||
// ErrUnsupportedEncodingType is returned when encoding type is not supported
|
||||
ErrUnsupportedEncodingType = errors.New("unsupported encoding type")
|
||||
|
||||
// ErrTransactionExpired is returned when transaction has expired
|
||||
ErrTransactionExpired = errors.New("transaction expired")
|
||||
|
||||
// ErrInvalidContractCall is returned when contract call parameters are invalid
|
||||
ErrInvalidContractCall = errors.New("invalid contract call")
|
||||
|
||||
// ErrSimulationFailed is returned when transaction simulation fails
|
||||
ErrSimulationFailed = errors.New("transaction simulation failed")
|
||||
)
|
||||
@@ -0,0 +1,457 @@
|
||||
package txns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"cosmossdk.io/math"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/std"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
"github.com/sonr-io/sonr/types/coins"
|
||||
)
|
||||
|
||||
// Example demonstrates basic transaction building workflow
|
||||
func ExampleTransactionBuilder_basic() {
|
||||
// Setup
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
txConfig := authtx.NewTxConfig(marshaler, authtx.DefaultSignModes)
|
||||
|
||||
clientCtx := client.Context{}.
|
||||
WithCodec(marshaler).
|
||||
WithTxConfig(txConfig).
|
||||
WithInterfaceRegistry(interfaceRegistry)
|
||||
|
||||
// Create coins manager and transaction builder
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
txBuilder := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
// Build a Cosmos transaction
|
||||
cosmosBuilder := txBuilder.Cosmos(clientCtx)
|
||||
cosmosBuilder.SetGas(200000, sdk.NewDecCoin("usnr", math.NewInt(1000)))
|
||||
cosmosBuilder.SetMemo("Example transaction")
|
||||
|
||||
// Create a send message
|
||||
sendMsg := &banktypes.MsgSend{
|
||||
FromAddress: "snr1sender123",
|
||||
ToAddress: "snr1receiver456",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000000))),
|
||||
}
|
||||
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{sendMsg},
|
||||
GasLimit: 200000,
|
||||
GasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
|
||||
Memo: "Example transaction",
|
||||
}
|
||||
|
||||
unsignedTx, err := cosmosBuilder.BuildUnsigned(params)
|
||||
if err != nil {
|
||||
fmt.Printf("Error building transaction: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Transaction type: %s\n", unsignedTx.GetType())
|
||||
fmt.Printf("Encoding: %s\n", unsignedTx.GetEncoding())
|
||||
|
||||
// Output:
|
||||
// Transaction type: cosmos
|
||||
// Encoding: protobuf
|
||||
}
|
||||
|
||||
// Example demonstrates EVM transaction building
|
||||
func ExampleTransactionBuilder_evm() {
|
||||
// Setup
|
||||
chainID := big.NewInt(1) // Ethereum mainnet
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", chainID)
|
||||
txBuilder := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
// Build an EVM transaction
|
||||
evmBuilder := txBuilder.EVM(chainID)
|
||||
evmBuilder.SetGas(21000, big.NewInt(20000000000)) // 20 Gwei
|
||||
evmBuilder.SetNonce(42)
|
||||
|
||||
// Create transfer parameters
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C6634C0532925")
|
||||
params := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000), // 1 ETH
|
||||
GasLimit: 21000,
|
||||
GasPrice: big.NewInt(20000000000),
|
||||
Nonce: 42,
|
||||
ChainID: chainID,
|
||||
}
|
||||
|
||||
unsignedTx, err := evmBuilder.BuildUnsigned(params)
|
||||
if err != nil {
|
||||
fmt.Printf("Error building transaction: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Transaction type: %s\n", unsignedTx.GetType())
|
||||
fmt.Printf("Encoding: %s\n", unsignedTx.GetEncoding())
|
||||
|
||||
// Output:
|
||||
// Transaction type: evm
|
||||
// Encoding: rlp
|
||||
}
|
||||
|
||||
// Example demonstrates fee estimation
|
||||
func ExampleFeeManager_estimate() {
|
||||
// Setup client context
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
txConfig := authtx.NewTxConfig(marshaler, authtx.DefaultSignModes)
|
||||
|
||||
clientCtx := client.Context{}.
|
||||
WithCodec(marshaler).
|
||||
WithTxConfig(txConfig).
|
||||
WithInterfaceRegistry(interfaceRegistry)
|
||||
|
||||
// Create fee manager
|
||||
feeManager := CreateDefaultFeeManager(clientCtx, nil, big.NewInt(1))
|
||||
|
||||
// Estimate Cosmos transaction fee
|
||||
cosmosParams := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: "snr1sender",
|
||||
ToAddress: "snr1receiver",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cosmosFee, err := feeManager.EstimateFee(
|
||||
context.Background(),
|
||||
TransactionTypeCosmos,
|
||||
cosmosParams,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Error estimating Cosmos fee: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Cosmos gas limit: %d\n", cosmosFee.GasLimit)
|
||||
fmt.Printf("Cosmos fee total: %s\n", cosmosFee.Total)
|
||||
|
||||
// Estimate EVM transaction fee
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
evmParams := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000),
|
||||
GasLimit: 21000,
|
||||
GasPrice: big.NewInt(20000000000),
|
||||
}
|
||||
|
||||
evmFee, err := feeManager.EstimateFee(context.Background(), TransactionTypeEVM, evmParams)
|
||||
if err != nil {
|
||||
fmt.Printf("Error estimating EVM fee: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("EVM gas limit: %d\n", evmFee.GasLimit)
|
||||
fmt.Printf("EVM fee total: %s\n", evmFee.Total)
|
||||
}
|
||||
|
||||
// Example demonstrates address derivation
|
||||
func ExampleAddressManager_derive() {
|
||||
// Setup
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
addressManager := NewAddressManager("snr", coinsManager)
|
||||
|
||||
// Derive addresses from entropy (DID + salt)
|
||||
derivation, err := addressManager.DeriveFromEntropy("did:sonr:test", "salt123", "snr")
|
||||
if err != nil {
|
||||
fmt.Printf("Error deriving addresses: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Cosmos address: %s\n", derivation.CosmosAddress)
|
||||
fmt.Printf("EVM address: %s\n", derivation.EVMAddress)
|
||||
fmt.Printf("Derivation path: %s\n", derivation.DerivationPath)
|
||||
fmt.Printf("Chain type: %s\n", derivation.ChainType)
|
||||
}
|
||||
|
||||
// Example demonstrates MPC enclave integration
|
||||
func ExampleMPCSigner_usage() {
|
||||
// This example shows how to integrate with MPC enclave
|
||||
// Note: In real usage, enclave data would come from actual MPC operations
|
||||
|
||||
// Create mock enclave data for demonstration
|
||||
// In real usage, this would come from mpc.NewEnclave() or similar
|
||||
enclaveData := &mpc.EnclaveData{
|
||||
// Mock data - real implementation would have actual enclave data
|
||||
}
|
||||
|
||||
// Create MPC signer
|
||||
signer, err := NewMPCSigner(enclaveData, "sonr-1")
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating MPC signer: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get public key
|
||||
pubKey := signer.GetPublicKey()
|
||||
fmt.Printf("Public key length: %d bytes\n", len(pubKey))
|
||||
|
||||
// Get addresses for different chain types
|
||||
cosmosAddr, err := signer.GetAddress(TransactionTypeCosmos)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting Cosmos address: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
evmAddr, err := signer.GetAddress(TransactionTypeEVM)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting EVM address: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Cosmos address: %s\n", cosmosAddr)
|
||||
fmt.Printf("EVM address: %s\n", evmAddr)
|
||||
}
|
||||
|
||||
// Example demonstrates cross-chain transaction workflow
|
||||
func ExampleTransactionBuilder_crossChain() {
|
||||
// Setup
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
txConfig := authtx.NewTxConfig(marshaler, authtx.DefaultSignModes)
|
||||
|
||||
_ = client.Context{}.
|
||||
WithCodec(marshaler).
|
||||
WithTxConfig(txConfig).
|
||||
WithInterfaceRegistry(interfaceRegistry)
|
||||
|
||||
// Create transaction builder and wallet
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
txBuilder := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
// Create wallet from entropy
|
||||
wallet, err := coinsManager.CreateWalletFromEntropy("did:sonr:user", "mysalt")
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating wallet: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create signers for both chains
|
||||
_, err = txBuilder.CreateSigner(wallet, TransactionTypeCosmos)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating Cosmos signer: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = txBuilder.CreateSigner(wallet, TransactionTypeEVM)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating EVM signer: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Both signers can now be used to sign transactions for their respective chains
|
||||
fmt.Printf("Cross-chain wallet ready\n")
|
||||
|
||||
// Output:
|
||||
// Cross-chain wallet ready
|
||||
}
|
||||
|
||||
// Example demonstrates transaction encoding and decoding
|
||||
func ExampleEncoderRegistry_usage() {
|
||||
// Setup
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
txConfig := authtx.NewTxConfig(marshaler, authtx.DefaultSignModes)
|
||||
|
||||
clientCtx := client.Context{}.
|
||||
WithCodec(marshaler).
|
||||
WithTxConfig(txConfig).
|
||||
WithInterfaceRegistry(interfaceRegistry)
|
||||
|
||||
// Create encoder registry
|
||||
registry := DefaultEncoderRegistry(clientCtx)
|
||||
|
||||
// Get Cosmos Protobuf encoder
|
||||
cosmosEncoder, err := registry.GetEncoderByType(EncodingTypeProtobuf, TransactionTypeCosmos)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting Cosmos encoder: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Cosmos encoder type: %s\n", cosmosEncoder.GetEncodingType())
|
||||
|
||||
// Get EVM RLP encoder
|
||||
evmEncoder, err := registry.GetEncoderByType(EncodingTypeRLP, TransactionTypeEVM)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting EVM encoder: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("EVM encoder type: %s\n", evmEncoder.GetEncodingType())
|
||||
|
||||
// Output:
|
||||
// Cosmos encoder type: protobuf
|
||||
// EVM encoder type: rlp
|
||||
}
|
||||
|
||||
// Example demonstrates batch address derivation
|
||||
func ExampleAddressManager_batch() {
|
||||
// Setup
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
addressManager := NewAddressManager("snr", coinsManager)
|
||||
|
||||
// Create batch requests
|
||||
requests := []AddressRequest{
|
||||
{
|
||||
Type: "entropy",
|
||||
DID: "did:sonr:user1",
|
||||
Salt: "salt1",
|
||||
Prefix: "snr",
|
||||
},
|
||||
{
|
||||
Type: "entropy",
|
||||
DID: "did:sonr:user2",
|
||||
Salt: "salt2",
|
||||
Prefix: "snr",
|
||||
},
|
||||
}
|
||||
|
||||
// Derive batch of addresses
|
||||
batch, err := addressManager.DeriveAddressBatch(requests)
|
||||
if err != nil {
|
||||
fmt.Printf("Error deriving address batch: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Derived %d addresses\n", len(batch.Addresses))
|
||||
fmt.Printf("Cosmos prefix: %s\n", batch.Metadata["cosmos_prefix"])
|
||||
|
||||
for i, addr := range batch.Addresses {
|
||||
fmt.Printf("Address %d: %s (Cosmos), %s (EVM)\n",
|
||||
i+1, addr.CosmosAddress, addr.EVMAddress)
|
||||
}
|
||||
}
|
||||
|
||||
// Example demonstrates complete transaction workflow
|
||||
func ExampleTransactionBuilder_complete() {
|
||||
// This example shows a complete transaction workflow from creation to signing
|
||||
|
||||
// Setup
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
txConfig := authtx.NewTxConfig(marshaler, authtx.DefaultSignModes)
|
||||
|
||||
clientCtx := client.Context{}.
|
||||
WithCodec(marshaler).
|
||||
WithTxConfig(txConfig).
|
||||
WithInterfaceRegistry(interfaceRegistry)
|
||||
|
||||
// 1. Create wallet and derive addresses
|
||||
coinsManager := coins.NewManager("snr", "sonr-1", big.NewInt(1))
|
||||
txBuilder := NewTransactionBuilder(coinsManager, "sonr-1")
|
||||
|
||||
// Derive addresses
|
||||
derivation, err := txBuilder.DeriveAddresses("did:sonr:example", "examplesalt")
|
||||
if err != nil {
|
||||
fmt.Printf("Error deriving addresses: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Create wallet and signer
|
||||
wallet, err := coinsManager.CreateWalletFromEntropy("did:sonr:example", "examplesalt")
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating wallet: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
signer, err := txBuilder.CreateSigner(wallet, TransactionTypeCosmos)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating signer: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Build transaction
|
||||
cosmosBuilder := txBuilder.Cosmos(clientCtx)
|
||||
|
||||
sendMsg := &banktypes.MsgSend{
|
||||
FromAddress: derivation.CosmosAddress,
|
||||
ToAddress: "snr1receiver123",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000000))),
|
||||
}
|
||||
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{sendMsg},
|
||||
GasLimit: 200000,
|
||||
GasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
|
||||
Memo: "Complete example transaction",
|
||||
}
|
||||
|
||||
// 4. Estimate fee
|
||||
feeManager := CreateDefaultFeeManager(clientCtx, nil, big.NewInt(1))
|
||||
_, err = feeManager.EstimateFee(context.Background(), TransactionTypeCosmos, params)
|
||||
if err != nil {
|
||||
fmt.Printf("Error estimating fee: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. Build unsigned transaction
|
||||
unsignedTx, err := cosmosBuilder.BuildUnsigned(params)
|
||||
if err != nil {
|
||||
fmt.Printf("Error building transaction: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 6. Sign transaction
|
||||
signBytes, err := unsignedTx.GetSignBytes()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting sign bytes: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
signature, err := signer.Sign(signBytes)
|
||||
if err != nil {
|
||||
fmt.Printf("Error signing transaction: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
pubKey := signer.GetPublicKey()
|
||||
signedTx, err := unsignedTx.Sign(signature, pubKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating signed transaction: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 7. Get final transaction bytes
|
||||
_, err = signedTx.GetBytes()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting transaction bytes: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Transaction workflow completed\n")
|
||||
|
||||
// Output:
|
||||
// Transaction workflow completed
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package txns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"cosmossdk.io/math"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
)
|
||||
|
||||
// FeeEstimator interface for estimating transaction fees
|
||||
type FeeEstimator interface {
|
||||
// EstimateFee estimates the fee for a transaction
|
||||
EstimateFee(ctx context.Context, params Params) (*FeeEstimation, error)
|
||||
// EstimateGas estimates the gas required for a transaction
|
||||
EstimateGas(ctx context.Context, params Params) (uint64, error)
|
||||
// GetGasPrice retrieves current gas price
|
||||
GetGasPrice(ctx context.Context) (any, error)
|
||||
// ValidateFee validates if a fee is sufficient
|
||||
ValidateFee(fee any, gasUsed uint64) error
|
||||
}
|
||||
|
||||
// CosmosFeeEstimator estimates fees for Cosmos transactions
|
||||
type CosmosFeeEstimator struct {
|
||||
clientCtx client.Context
|
||||
minGasPrice sdk.DecCoin
|
||||
gasAdjustment float64
|
||||
}
|
||||
|
||||
// NewCosmosFeeEstimator creates a new Cosmos fee estimator
|
||||
func NewCosmosFeeEstimator(clientCtx client.Context, minGasPrice sdk.DecCoin) *CosmosFeeEstimator {
|
||||
return &CosmosFeeEstimator{
|
||||
clientCtx: clientCtx,
|
||||
minGasPrice: minGasPrice,
|
||||
gasAdjustment: 1.2, // Default gas adjustment factor
|
||||
}
|
||||
}
|
||||
|
||||
// SetGasAdjustment sets the gas adjustment factor
|
||||
func (cfe *CosmosFeeEstimator) SetGasAdjustment(adjustment float64) {
|
||||
cfe.gasAdjustment = adjustment
|
||||
}
|
||||
|
||||
// EstimateFee implements FeeEstimator interface
|
||||
func (cfe *CosmosFeeEstimator) EstimateFee(
|
||||
ctx context.Context,
|
||||
params Params,
|
||||
) (*FeeEstimation, error) {
|
||||
cosmosParams, ok := params.(*CosmosTransactionParams)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
if err := cosmosParams.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
// Estimate gas usage
|
||||
gasLimit, err := cfe.EstimateGas(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to estimate gas: %w", err)
|
||||
}
|
||||
|
||||
// Apply gas adjustment
|
||||
adjustedGasLimit := uint64(float64(gasLimit) * cfe.gasAdjustment)
|
||||
|
||||
// Calculate fee
|
||||
feeAmount := cfe.minGasPrice.Amount.MulInt64(int64(adjustedGasLimit))
|
||||
fee := sdk.NewCoins(sdk.NewCoin(cfe.minGasPrice.Denom, feeAmount.TruncateInt()))
|
||||
|
||||
return &FeeEstimation{
|
||||
GasLimit: adjustedGasLimit,
|
||||
GasPrice: cfe.minGasPrice,
|
||||
Fee: fee,
|
||||
Total: fee.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EstimateGas implements FeeEstimator interface
|
||||
func (cfe *CosmosFeeEstimator) EstimateGas(ctx context.Context, params Params) (uint64, error) {
|
||||
cosmosParams, ok := params.(*CosmosTransactionParams)
|
||||
if !ok {
|
||||
return 0, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
// Base gas estimation by message type
|
||||
baseGas := uint64(0)
|
||||
for _, msg := range cosmosParams.Messages {
|
||||
baseGas += cfe.estimateGasForMessage(msg)
|
||||
}
|
||||
|
||||
// Add overhead for transaction processing
|
||||
overhead := uint64(10000) // Base transaction overhead
|
||||
if cosmosParams.Memo != "" {
|
||||
overhead += uint64(len(cosmosParams.Memo)) * 10 // Memo overhead
|
||||
}
|
||||
|
||||
return baseGas + overhead, nil
|
||||
}
|
||||
|
||||
// GetGasPrice implements FeeEstimator interface
|
||||
func (cfe *CosmosFeeEstimator) GetGasPrice(ctx context.Context) (any, error) {
|
||||
// In Cosmos, gas price is typically fixed or queried from chain parameters
|
||||
// For now, return the configured minimum gas price
|
||||
return cfe.minGasPrice, nil
|
||||
}
|
||||
|
||||
// ValidateFee implements FeeEstimator interface
|
||||
func (cfe *CosmosFeeEstimator) ValidateFee(fee any, gasUsed uint64) error {
|
||||
feeCoins, ok := fee.(sdk.Coins)
|
||||
if !ok {
|
||||
return ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
// Calculate minimum required fee
|
||||
minFeeAmount := cfe.minGasPrice.Amount.MulInt64(int64(gasUsed))
|
||||
minFee := sdk.NewCoins(sdk.NewCoin(cfe.minGasPrice.Denom, minFeeAmount.TruncateInt()))
|
||||
|
||||
// Check if provided fee is sufficient
|
||||
if !feeCoins.IsAllGTE(minFee) {
|
||||
return fmt.Errorf("insufficient fee: got %s, need at least %s", feeCoins, minFee)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// estimateGasForMessage estimates gas usage for a specific message type
|
||||
func (cfe *CosmosFeeEstimator) estimateGasForMessage(msg sdk.Msg) uint64 {
|
||||
switch msg.(type) {
|
||||
case *banktypes.MsgSend:
|
||||
return 80000 // Base gas for bank send
|
||||
case *banktypes.MsgMultiSend:
|
||||
return 120000 // Higher gas for multi-send
|
||||
default:
|
||||
return 100000 // Default gas estimate
|
||||
}
|
||||
}
|
||||
|
||||
// EVMFeeEstimator estimates fees for EVM transactions
|
||||
type EVMFeeEstimator struct {
|
||||
client *ethclient.Client
|
||||
chainID *big.Int
|
||||
gasAdjustment float64
|
||||
}
|
||||
|
||||
// NewEVMFeeEstimator creates a new EVM fee estimator
|
||||
func NewEVMFeeEstimator(client *ethclient.Client, chainID *big.Int) *EVMFeeEstimator {
|
||||
return &EVMFeeEstimator{
|
||||
client: client,
|
||||
chainID: chainID,
|
||||
gasAdjustment: 1.1, // Default gas adjustment factor
|
||||
}
|
||||
}
|
||||
|
||||
// SetGasAdjustment sets the gas adjustment factor
|
||||
func (efe *EVMFeeEstimator) SetGasAdjustment(adjustment float64) {
|
||||
efe.gasAdjustment = adjustment
|
||||
}
|
||||
|
||||
// EstimateFee implements FeeEstimator interface
|
||||
func (efe *EVMFeeEstimator) EstimateFee(
|
||||
ctx context.Context,
|
||||
params Params,
|
||||
) (*FeeEstimation, error) {
|
||||
evmParams, ok := params.(*EVMTransactionParams)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
if err := evmParams.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid parameters: %w", err)
|
||||
}
|
||||
|
||||
// Estimate gas usage
|
||||
gasLimit, err := efe.EstimateGas(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to estimate gas: %w", err)
|
||||
}
|
||||
|
||||
// Get current gas price
|
||||
gasPrice, err := efe.GetGasPrice(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get gas price: %w", err)
|
||||
}
|
||||
|
||||
// Apply gas adjustment
|
||||
adjustedGasLimit := uint64(float64(gasLimit) * efe.gasAdjustment)
|
||||
|
||||
// Calculate fee
|
||||
var totalFee *big.Int
|
||||
var feeData any
|
||||
|
||||
if evmParams.MaxFeePerGas != nil && evmParams.MaxPriorityFeePerGas != nil {
|
||||
// EIP-1559 transaction
|
||||
totalFee = new(big.Int).Mul(evmParams.MaxFeePerGas, big.NewInt(int64(adjustedGasLimit)))
|
||||
feeData = map[string]*big.Int{
|
||||
"maxFeePerGas": evmParams.MaxFeePerGas,
|
||||
"maxPriorityFeePerGas": evmParams.MaxPriorityFeePerGas,
|
||||
}
|
||||
} else {
|
||||
// Legacy transaction
|
||||
gasPriceBig := gasPrice.(*big.Int)
|
||||
totalFee = new(big.Int).Mul(gasPriceBig, big.NewInt(int64(adjustedGasLimit)))
|
||||
feeData = gasPriceBig
|
||||
}
|
||||
|
||||
return &FeeEstimation{
|
||||
GasLimit: adjustedGasLimit,
|
||||
GasPrice: feeData,
|
||||
Fee: totalFee,
|
||||
Total: totalFee.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EstimateGas implements FeeEstimator interface
|
||||
func (efe *EVMFeeEstimator) EstimateGas(ctx context.Context, params Params) (uint64, error) {
|
||||
evmParams, ok := params.(*EVMTransactionParams)
|
||||
if !ok {
|
||||
return 0, ErrInvalidTransactionParams
|
||||
}
|
||||
|
||||
if efe.client == nil {
|
||||
// Fallback estimation without client
|
||||
return efe.estimateGasOffline(evmParams), nil
|
||||
}
|
||||
|
||||
// Create a call message for gas estimation
|
||||
callMsg := ethereum.CallMsg{
|
||||
To: evmParams.To,
|
||||
Value: evmParams.Value,
|
||||
Data: evmParams.Data,
|
||||
}
|
||||
|
||||
// Estimate gas using the client
|
||||
gasLimit, err := efe.client.EstimateGas(ctx, callMsg)
|
||||
if err != nil {
|
||||
// Fallback to offline estimation
|
||||
return efe.estimateGasOffline(evmParams), nil
|
||||
}
|
||||
|
||||
return gasLimit, nil
|
||||
}
|
||||
|
||||
// GetGasPrice implements FeeEstimator interface
|
||||
func (efe *EVMFeeEstimator) GetGasPrice(ctx context.Context) (any, error) {
|
||||
if efe.client == nil {
|
||||
// Return default gas price
|
||||
return big.NewInt(params.GWei * 20), nil // 20 Gwei
|
||||
}
|
||||
|
||||
gasPrice, err := efe.client.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
// Fallback to default
|
||||
return big.NewInt(params.GWei * 20), nil
|
||||
}
|
||||
|
||||
return gasPrice, nil
|
||||
}
|
||||
|
||||
// ValidateFee implements FeeEstimator interface
|
||||
func (efe *EVMFeeEstimator) ValidateFee(fee any, gasUsed uint64) error {
|
||||
switch f := fee.(type) {
|
||||
case *big.Int:
|
||||
// Legacy transaction
|
||||
minFee := new(big.Int).Mul(big.NewInt(params.GWei), big.NewInt(int64(gasUsed)))
|
||||
if f.Cmp(minFee) < 0 {
|
||||
return fmt.Errorf("insufficient fee: got %s, need at least %s", f, minFee)
|
||||
}
|
||||
case map[string]*big.Int:
|
||||
// EIP-1559 transaction
|
||||
maxFeePerGas, ok := f["maxFeePerGas"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing maxFeePerGas in fee data")
|
||||
}
|
||||
minFee := new(big.Int).Mul(big.NewInt(params.GWei), big.NewInt(int64(gasUsed)))
|
||||
totalMaxFee := new(big.Int).Mul(maxFeePerGas, big.NewInt(int64(gasUsed)))
|
||||
if totalMaxFee.Cmp(minFee) < 0 {
|
||||
return fmt.Errorf("insufficient max fee: got %s, need at least %s", totalMaxFee, minFee)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported fee type: %T", fee)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// estimateGasOffline provides offline gas estimation
|
||||
func (efe *EVMFeeEstimator) estimateGasOffline(params *EVMTransactionParams) uint64 {
|
||||
baseGas := uint64(21000) // Base transaction gas
|
||||
|
||||
if params.Data != nil && len(params.Data) > 0 {
|
||||
// Contract interaction
|
||||
baseGas += uint64(len(params.Data)) * 16 // Rough estimate for data
|
||||
if params.To == nil {
|
||||
// Contract deployment
|
||||
baseGas += 200000
|
||||
} else {
|
||||
// Contract call
|
||||
baseGas += 100000
|
||||
}
|
||||
}
|
||||
|
||||
return baseGas
|
||||
}
|
||||
|
||||
// FeeManager manages fee estimation for multiple transaction types
|
||||
type FeeManager struct {
|
||||
cosmosEstimator *CosmosFeeEstimator
|
||||
evmEstimator *EVMFeeEstimator
|
||||
}
|
||||
|
||||
// NewFeeManager creates a new fee manager
|
||||
func NewFeeManager(cosmosEstimator *CosmosFeeEstimator, evmEstimator *EVMFeeEstimator) *FeeManager {
|
||||
return &FeeManager{
|
||||
cosmosEstimator: cosmosEstimator,
|
||||
evmEstimator: evmEstimator,
|
||||
}
|
||||
}
|
||||
|
||||
// EstimateFee estimates fee for any transaction type
|
||||
func (fm *FeeManager) EstimateFee(
|
||||
ctx context.Context,
|
||||
txType TransactionType,
|
||||
params Params,
|
||||
) (*FeeEstimation, error) {
|
||||
switch txType {
|
||||
case TransactionTypeCosmos:
|
||||
if fm.cosmosEstimator == nil {
|
||||
return nil, fmt.Errorf("cosmos fee estimator not configured")
|
||||
}
|
||||
return fm.cosmosEstimator.EstimateFee(ctx, params)
|
||||
case TransactionTypeEVM:
|
||||
if fm.evmEstimator == nil {
|
||||
return nil, fmt.Errorf("EVM fee estimator not configured")
|
||||
}
|
||||
return fm.evmEstimator.EstimateFee(ctx, params)
|
||||
default:
|
||||
return nil, ErrUnsupportedChainType
|
||||
}
|
||||
}
|
||||
|
||||
// GetEstimator returns the appropriate fee estimator for a transaction type
|
||||
func (fm *FeeManager) GetEstimator(txType TransactionType) (FeeEstimator, error) {
|
||||
switch txType {
|
||||
case TransactionTypeCosmos:
|
||||
if fm.cosmosEstimator == nil {
|
||||
return nil, fmt.Errorf("cosmos fee estimator not configured")
|
||||
}
|
||||
return fm.cosmosEstimator, nil
|
||||
case TransactionTypeEVM:
|
||||
if fm.evmEstimator == nil {
|
||||
return nil, fmt.Errorf("EVM fee estimator not configured")
|
||||
}
|
||||
return fm.evmEstimator, nil
|
||||
default:
|
||||
return nil, ErrUnsupportedChainType
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultFeeConfig holds default fee configuration
|
||||
type DefaultFeeConfig struct {
|
||||
CosmosMinGasPrice sdk.DecCoin
|
||||
EVMGasPrice *big.Int
|
||||
GasAdjustment float64
|
||||
}
|
||||
|
||||
// GetDefaultFeeConfig returns default fee configuration
|
||||
func GetDefaultFeeConfig() *DefaultFeeConfig {
|
||||
return &DefaultFeeConfig{
|
||||
CosmosMinGasPrice: sdk.NewDecCoin("usnr", math.NewInt(1000)),
|
||||
EVMGasPrice: big.NewInt(params.GWei * 20), // 20 Gwei
|
||||
GasAdjustment: 1.2,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDefaultFeeManager creates a fee manager with default configuration
|
||||
func CreateDefaultFeeManager(
|
||||
clientCtx client.Context,
|
||||
evmClient *ethclient.Client,
|
||||
chainID *big.Int,
|
||||
) *FeeManager {
|
||||
config := GetDefaultFeeConfig()
|
||||
|
||||
cosmosEstimator := NewCosmosFeeEstimator(clientCtx, config.CosmosMinGasPrice)
|
||||
cosmosEstimator.SetGasAdjustment(config.GasAdjustment)
|
||||
|
||||
evmEstimator := NewEVMFeeEstimator(evmClient, chainID)
|
||||
evmEstimator.SetGasAdjustment(config.GasAdjustment)
|
||||
|
||||
return NewFeeManager(cosmosEstimator, evmEstimator)
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package txns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"cosmossdk.io/math"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/std"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestClientCtx() client.Context {
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
std.RegisterInterfaces(interfaceRegistry)
|
||||
banktypes.RegisterInterfaces(interfaceRegistry)
|
||||
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
txConfig := authtx.NewTxConfig(marshaler, authtx.DefaultSignModes)
|
||||
|
||||
return client.Context{}.
|
||||
WithCodec(marshaler).
|
||||
WithTxConfig(txConfig).
|
||||
WithInterfaceRegistry(interfaceRegistry)
|
||||
}
|
||||
|
||||
func TestNewCosmosFeeEstimator(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
|
||||
estimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
require.NotNil(t, estimator)
|
||||
|
||||
assert.Equal(t, clientCtx, estimator.clientCtx)
|
||||
assert.Equal(t, minGasPrice, estimator.minGasPrice)
|
||||
assert.Equal(t, 1.2, estimator.gasAdjustment)
|
||||
}
|
||||
|
||||
func TestCosmosFeeEstimator_SetGasAdjustment(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
estimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
estimator.SetGasAdjustment(1.5)
|
||||
assert.Equal(t, 1.5, estimator.gasAdjustment)
|
||||
}
|
||||
|
||||
func TestCosmosFeeEstimator_EstimateGas(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
estimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
// Test with bank send message
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: "snr1test",
|
||||
ToAddress: "snr1test2",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
},
|
||||
},
|
||||
Memo: "test memo",
|
||||
}
|
||||
|
||||
gasLimit, err := estimator.EstimateGas(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be base gas (80000) + overhead (10000) + memo overhead (len("test memo") * 10 = 90)
|
||||
expectedGas := uint64(80000 + 10000 + 90)
|
||||
assert.Equal(t, expectedGas, gasLimit)
|
||||
}
|
||||
|
||||
func TestCosmosFeeEstimator_EstimateFee(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
estimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: "snr1test",
|
||||
ToAddress: "snr1test2",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
},
|
||||
},
|
||||
GasLimit: 200000,
|
||||
}
|
||||
|
||||
feeEst, err := estimator.EstimateFee(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, feeEst)
|
||||
|
||||
// Check fee estimation structure
|
||||
assert.Greater(t, feeEst.GasLimit, uint64(0))
|
||||
assert.NotNil(t, feeEst.GasPrice)
|
||||
assert.NotNil(t, feeEst.Fee)
|
||||
assert.NotEmpty(t, feeEst.Total)
|
||||
|
||||
// Check that gas adjustment was applied (gas limit should be > base gas)
|
||||
assert.Greater(t, feeEst.GasLimit, uint64(90000)) // Should be more than base gas
|
||||
assert.LessOrEqual(t, feeEst.GasLimit, uint64(250000)) // But reasonable
|
||||
}
|
||||
|
||||
func TestCosmosFeeEstimator_GetGasPrice(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
estimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
gasPrice, err := estimator.GetGasPrice(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, minGasPrice, gasPrice)
|
||||
}
|
||||
|
||||
func TestCosmosFeeEstimator_ValidateFee(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
estimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
gasUsed := uint64(100000)
|
||||
|
||||
// Test sufficient fee
|
||||
sufficientFee := sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(100000000))) // 100000 * 1000
|
||||
err := estimator.ValidateFee(sufficientFee, gasUsed)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test insufficient fee
|
||||
insufficientFee := sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(50000)))
|
||||
err = estimator.ValidateFee(insufficientFee, gasUsed)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "insufficient fee")
|
||||
|
||||
// Test invalid fee type
|
||||
err = estimator.ValidateFee("invalid", gasUsed)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewEVMFeeEstimator(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
estimator := NewEVMFeeEstimator(nil, chainID)
|
||||
require.NotNil(t, estimator)
|
||||
|
||||
assert.Equal(t, chainID, estimator.chainID)
|
||||
assert.Equal(t, 1.1, estimator.gasAdjustment)
|
||||
assert.Nil(t, estimator.client)
|
||||
}
|
||||
|
||||
func TestEVMFeeEstimator_EstimateGas(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
estimator := NewEVMFeeEstimator(nil, chainID) // No client for offline estimation
|
||||
|
||||
// Test simple transfer
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
params := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000),
|
||||
}
|
||||
|
||||
gasLimit, err := estimator.EstimateGas(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(21000), gasLimit) // Base gas for transfer
|
||||
|
||||
// Test contract call
|
||||
params.Data = []byte("contract call data")
|
||||
gasLimit, err = estimator.EstimateGas(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, gasLimit, uint64(21000)) // Should be more than base gas
|
||||
|
||||
// Test contract deployment
|
||||
params.To = nil
|
||||
gasLimit, err = estimator.EstimateGas(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, gasLimit, uint64(200000)) // Should include deployment gas
|
||||
}
|
||||
|
||||
func TestEVMFeeEstimator_EstimateFee(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
estimator := NewEVMFeeEstimator(nil, chainID)
|
||||
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
|
||||
// Test legacy transaction
|
||||
params := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000),
|
||||
GasLimit: 21000,
|
||||
GasPrice: big.NewInt(20000000000), // 20 Gwei
|
||||
ChainID: big.NewInt(1),
|
||||
}
|
||||
|
||||
feeEst, err := estimator.EstimateFee(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, feeEst)
|
||||
|
||||
// Check fee estimation
|
||||
expectedGas := uint64(23100) // 21000 * 1.1 with adjustment
|
||||
assert.Equal(t, expectedGas, feeEst.GasLimit)
|
||||
assert.NotNil(t, feeEst.GasPrice)
|
||||
assert.NotNil(t, feeEst.Fee)
|
||||
|
||||
// Test EIP-1559 transaction
|
||||
params.MaxFeePerGas = big.NewInt(30000000000)
|
||||
params.MaxPriorityFeePerGas = big.NewInt(2000000000)
|
||||
|
||||
feeEst1559, err := estimator.EstimateFee(context.Background(), params)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, feeEst1559)
|
||||
|
||||
assert.Equal(t, expectedGas, feeEst1559.GasLimit)
|
||||
|
||||
// Check that fee data contains EIP-1559 fields
|
||||
feeData, ok := feeEst1559.GasPrice.(map[string]*big.Int)
|
||||
require.True(t, ok)
|
||||
assert.Contains(t, feeData, "maxFeePerGas")
|
||||
assert.Contains(t, feeData, "maxPriorityFeePerGas")
|
||||
}
|
||||
|
||||
func TestEVMFeeEstimator_GetGasPrice(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
estimator := NewEVMFeeEstimator(nil, chainID)
|
||||
|
||||
gasPrice, err := estimator.GetGasPrice(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPrice := big.NewInt(params.GWei * 20) // 20 Gwei default
|
||||
assert.Equal(t, expectedPrice, gasPrice)
|
||||
}
|
||||
|
||||
func TestEVMFeeEstimator_ValidateFee(t *testing.T) {
|
||||
chainID := big.NewInt(1)
|
||||
estimator := NewEVMFeeEstimator(nil, chainID)
|
||||
|
||||
gasUsed := uint64(21000)
|
||||
|
||||
// Test legacy transaction with sufficient fee
|
||||
sufficientFee := big.NewInt(21000000000000000) // 21000 * 1 Gwei
|
||||
err := estimator.ValidateFee(sufficientFee, gasUsed)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test legacy transaction with insufficient fee
|
||||
insufficientFee := big.NewInt(1000)
|
||||
err = estimator.ValidateFee(insufficientFee, gasUsed)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "insufficient fee")
|
||||
|
||||
// Test EIP-1559 transaction
|
||||
eip1559Fee := map[string]*big.Int{
|
||||
"maxFeePerGas": big.NewInt(2000000000), // 2 Gwei
|
||||
"maxPriorityFeePerGas": big.NewInt(1000000000), // 1 Gwei
|
||||
}
|
||||
err = estimator.ValidateFee(eip1559Fee, gasUsed)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test invalid fee type
|
||||
err = estimator.ValidateFee("invalid", gasUsed)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFeeManager(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
cosmosEstimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
chainID := big.NewInt(1)
|
||||
evmEstimator := NewEVMFeeEstimator(nil, chainID)
|
||||
|
||||
manager := NewFeeManager(cosmosEstimator, evmEstimator)
|
||||
require.NotNil(t, manager)
|
||||
|
||||
// Test Cosmos fee estimation
|
||||
cosmosParams := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: "snr1test",
|
||||
ToAddress: "snr1test2",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
},
|
||||
},
|
||||
GasLimit: 200000,
|
||||
}
|
||||
|
||||
cosmosFee, err := manager.EstimateFee(context.Background(), TransactionTypeCosmos, cosmosParams)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, cosmosFee)
|
||||
|
||||
// Test EVM fee estimation
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
evmParams := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000),
|
||||
GasLimit: 21000,
|
||||
GasPrice: big.NewInt(20000000000),
|
||||
ChainID: big.NewInt(1),
|
||||
}
|
||||
|
||||
evmFee, err := manager.EstimateFee(context.Background(), TransactionTypeEVM, evmParams)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, evmFee)
|
||||
|
||||
// Test unsupported transaction type
|
||||
_, err = manager.EstimateFee(context.Background(), TransactionTypeUnknown, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrUnsupportedChainType, err)
|
||||
}
|
||||
|
||||
func TestFeeManager_GetEstimator(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
cosmosEstimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
chainID := big.NewInt(1)
|
||||
evmEstimator := NewEVMFeeEstimator(nil, chainID)
|
||||
|
||||
manager := NewFeeManager(cosmosEstimator, evmEstimator)
|
||||
|
||||
// Test getting Cosmos estimator
|
||||
cosmosEst, err := manager.GetEstimator(TransactionTypeCosmos)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, cosmosEstimator, cosmosEst)
|
||||
|
||||
// Test getting EVM estimator
|
||||
evmEst, err := manager.GetEstimator(TransactionTypeEVM)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, evmEstimator, evmEst)
|
||||
|
||||
// Test unsupported type
|
||||
_, err = manager.GetEstimator(TransactionTypeUnknown)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetDefaultFeeConfig(t *testing.T) {
|
||||
config := GetDefaultFeeConfig()
|
||||
require.NotNil(t, config)
|
||||
|
||||
assert.Equal(t, "usnr", config.CosmosMinGasPrice.Denom)
|
||||
expectedDec := math.LegacyNewDecFromInt(math.NewInt(1000))
|
||||
assert.True(t, config.CosmosMinGasPrice.Amount.Equal(expectedDec))
|
||||
assert.Equal(t, big.NewInt(params.GWei*20), config.EVMGasPrice)
|
||||
assert.Equal(t, 1.2, config.GasAdjustment)
|
||||
}
|
||||
|
||||
func TestCreateDefaultFeeManager(t *testing.T) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
chainID := big.NewInt(1)
|
||||
|
||||
manager := CreateDefaultFeeManager(clientCtx, nil, chainID)
|
||||
require.NotNil(t, manager)
|
||||
require.NotNil(t, manager.cosmosEstimator)
|
||||
require.NotNil(t, manager.evmEstimator)
|
||||
|
||||
// Test that estimators work
|
||||
cosmosEst, err := manager.GetEstimator(TransactionTypeCosmos)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, cosmosEst)
|
||||
|
||||
evmEst, err := manager.GetEstimator(TransactionTypeEVM)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, evmEst)
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkCosmosFeeEstimator_EstimateGas(b *testing.B) {
|
||||
clientCtx := setupTestClientCtx()
|
||||
minGasPrice := sdk.NewDecCoin("usnr", math.NewInt(1000))
|
||||
estimator := NewCosmosFeeEstimator(clientCtx, minGasPrice)
|
||||
|
||||
params := &CosmosTransactionParams{
|
||||
Messages: []sdk.Msg{
|
||||
&banktypes.MsgSend{
|
||||
FromAddress: "snr1test",
|
||||
ToAddress: "snr1test2",
|
||||
Amount: sdk.NewCoins(sdk.NewCoin("usnr", math.NewInt(1000))),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = estimator.EstimateGas(context.Background(), params)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEVMFeeEstimator_EstimateGas(b *testing.B) {
|
||||
chainID := big.NewInt(1)
|
||||
estimator := NewEVMFeeEstimator(nil, chainID)
|
||||
|
||||
toAddr := common.HexToAddress("0x742d35Cc6634C0532925a3b8D80C")
|
||||
params := &EVMTransactionParams{
|
||||
To: &toAddr,
|
||||
Value: big.NewInt(1000000000000000000),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = estimator.EstimateGas(context.Background(), params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package txns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/sonr-io/sonr/crypto/mpc"
|
||||
)
|
||||
|
||||
// TransactionType represents the type of transaction
|
||||
type TransactionType string
|
||||
|
||||
const (
|
||||
TransactionTypeCosmos TransactionType = "cosmos"
|
||||
TransactionTypeEVM TransactionType = "evm"
|
||||
TransactionTypeUnknown TransactionType = "unknown"
|
||||
)
|
||||
|
||||
// EncodingType represents the encoding format for transactions
|
||||
type EncodingType string
|
||||
|
||||
const (
|
||||
EncodingTypeAmino EncodingType = "amino"
|
||||
EncodingTypeProtobuf EncodingType = "protobuf"
|
||||
EncodingTypeRLP EncodingType = "rlp"
|
||||
)
|
||||
|
||||
// Params defines the interface for transaction parameters
|
||||
type Params interface {
|
||||
// Validate validates the transaction parameters
|
||||
Validate() error
|
||||
// GetType returns the transaction type
|
||||
GetType() TransactionType
|
||||
// GetMemo returns the transaction memo if any
|
||||
GetMemo() string
|
||||
}
|
||||
|
||||
// Builder defines the interface for transaction builders
|
||||
type Builder interface {
|
||||
// SetChainID sets the chain ID for the transaction
|
||||
SetChainID(chainID string) Builder
|
||||
// SetGas sets gas parameters
|
||||
SetGas(limit uint64, price any) Builder
|
||||
// SetMemo sets transaction memo
|
||||
SetMemo(memo string) Builder
|
||||
// BuildUnsigned creates an unsigned transaction
|
||||
BuildUnsigned(params Params) (UnsignedTransaction, error)
|
||||
// EstimateFee estimates the transaction fee
|
||||
EstimateFee(params Params) (*FeeEstimation, error)
|
||||
// GetTransactionType returns the transaction type
|
||||
GetTransactionType() TransactionType
|
||||
}
|
||||
|
||||
// UnsignedTransaction represents an unsigned transaction that can be signed
|
||||
type UnsignedTransaction interface {
|
||||
// GetSignBytes returns the bytes to be signed
|
||||
GetSignBytes() ([]byte, error)
|
||||
// GetType returns the transaction type
|
||||
GetType() TransactionType
|
||||
// GetEncoding returns the encoding type
|
||||
GetEncoding() EncodingType
|
||||
// Sign signs the transaction with the provided signature
|
||||
Sign(signature []byte, pubKey []byte) (SignedTransaction, error)
|
||||
// GetRaw returns the raw transaction data
|
||||
GetRaw() any
|
||||
}
|
||||
|
||||
// SignedTransaction represents a signed transaction ready for broadcast
|
||||
type SignedTransaction interface {
|
||||
// GetHash returns the transaction hash
|
||||
GetHash() string
|
||||
// GetBytes returns the serialized transaction bytes
|
||||
GetBytes() ([]byte, error)
|
||||
// GetType returns the transaction type
|
||||
GetType() TransactionType
|
||||
// GetEncoding returns the encoding type
|
||||
GetEncoding() EncodingType
|
||||
// GetRaw returns the raw signed transaction
|
||||
GetRaw() any
|
||||
}
|
||||
|
||||
// Signer interface for signing transactions
|
||||
type Signer interface {
|
||||
// Sign signs the transaction bytes and returns signature
|
||||
Sign(txBytes []byte) ([]byte, error)
|
||||
// GetPublicKey returns the public key
|
||||
GetPublicKey() []byte
|
||||
// GetAddress returns the address for the given chain
|
||||
GetAddress(chainType TransactionType) (string, error)
|
||||
}
|
||||
|
||||
// CosmosTransactionParams holds parameters for Cosmos transactions
|
||||
type CosmosTransactionParams struct {
|
||||
Messages []sdk.Msg
|
||||
GasLimit uint64
|
||||
GasPrice sdk.DecCoin
|
||||
Fee sdk.Coins
|
||||
Memo string
|
||||
TimeoutHeight uint64
|
||||
AccountNumber uint64
|
||||
Sequence uint64
|
||||
ChainID string
|
||||
}
|
||||
|
||||
// Validate implements Params interface
|
||||
func (p *CosmosTransactionParams) Validate() error {
|
||||
if len(p.Messages) == 0 {
|
||||
return fmt.Errorf("at least one message is required")
|
||||
}
|
||||
if p.GasLimit == 0 {
|
||||
return fmt.Errorf("gas limit must be greater than 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetType implements Params interface
|
||||
func (p *CosmosTransactionParams) GetType() TransactionType {
|
||||
return TransactionTypeCosmos
|
||||
}
|
||||
|
||||
// GetMemo implements Params interface
|
||||
func (p *CosmosTransactionParams) GetMemo() string {
|
||||
return p.Memo
|
||||
}
|
||||
|
||||
// EVMTransactionParams holds parameters for EVM transactions
|
||||
type EVMTransactionParams struct {
|
||||
To *common.Address
|
||||
Value *big.Int
|
||||
Data []byte
|
||||
GasLimit uint64
|
||||
GasPrice *big.Int
|
||||
MaxFeePerGas *big.Int
|
||||
MaxPriorityFeePerGas *big.Int
|
||||
Nonce uint64
|
||||
ChainID *big.Int
|
||||
Memo string // Some chains support memos in EVM transactions
|
||||
}
|
||||
|
||||
// Validate implements Params interface
|
||||
func (p *EVMTransactionParams) Validate() error {
|
||||
if p.ChainID == nil {
|
||||
return fmt.Errorf("chain ID is required")
|
||||
}
|
||||
if p.GasLimit == 0 {
|
||||
return fmt.Errorf("gas limit must be greater than 0")
|
||||
}
|
||||
if p.Value == nil {
|
||||
p.Value = big.NewInt(0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetType implements Params interface
|
||||
func (p *EVMTransactionParams) GetType() TransactionType {
|
||||
return TransactionTypeEVM
|
||||
}
|
||||
|
||||
// GetMemo implements Params interface
|
||||
func (p *EVMTransactionParams) GetMemo() string {
|
||||
return p.Memo
|
||||
}
|
||||
|
||||
// FeeEstimation represents fee estimation data
|
||||
type FeeEstimation struct {
|
||||
GasLimit uint64 `json:"gas_limit"`
|
||||
GasPrice any `json:"gas_price"`
|
||||
Fee any `json:"fee"`
|
||||
Total string `json:"total"`
|
||||
}
|
||||
|
||||
// AddressDerivation represents address derivation information
|
||||
type AddressDerivation struct {
|
||||
CosmosAddress string `json:"cosmos_address"`
|
||||
EVMAddress string `json:"evm_address"`
|
||||
DerivationPath string `json:"derivation_path"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
ChainType string `json:"chain_type"`
|
||||
}
|
||||
|
||||
// MPCSigner implements Signer interface using MPC enclave
|
||||
type MPCSigner struct {
|
||||
enclave mpc.Enclave
|
||||
publicKey []byte
|
||||
chainID string
|
||||
}
|
||||
|
||||
// NewMPCSigner creates a new MPC signer
|
||||
func NewMPCSigner(enclaveData *mpc.EnclaveData, chainID string) (*MPCSigner, error) {
|
||||
enclave, err := mpc.ImportEnclave(mpc.WithEnclaveData(enclaveData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MPCSigner{
|
||||
enclave: enclave,
|
||||
publicKey: enclave.PubKeyBytes(),
|
||||
chainID: chainID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sign implements Signer interface
|
||||
func (m *MPCSigner) Sign(txBytes []byte) ([]byte, error) {
|
||||
return m.enclave.Sign(txBytes)
|
||||
}
|
||||
|
||||
// GetPublicKey implements Signer interface
|
||||
func (m *MPCSigner) GetPublicKey() []byte {
|
||||
return m.publicKey
|
||||
}
|
||||
|
||||
// GetAddress implements Signer interface
|
||||
func (m *MPCSigner) GetAddress(chainType TransactionType) (string, error) {
|
||||
// This would need to derive address from public key based on chain type
|
||||
// Implementation would depend on the specific address derivation logic
|
||||
// For now, return a placeholder
|
||||
switch chainType {
|
||||
case TransactionTypeCosmos:
|
||||
return "cosmos1...", nil
|
||||
case TransactionTypeEVM:
|
||||
return "0x...", nil
|
||||
default:
|
||||
return "", ErrUnsupportedChainType
|
||||
}
|
||||
}
|
||||
|
||||
// CosmosUnsignedTx represents an unsigned Cosmos transaction
|
||||
type CosmosUnsignedTx struct {
|
||||
TxBuilder client.TxBuilder
|
||||
ChainID string
|
||||
Encoding EncodingType
|
||||
}
|
||||
|
||||
// GetSignBytes implements UnsignedTransaction interface
|
||||
func (c *CosmosUnsignedTx) GetSignBytes() ([]byte, error) {
|
||||
// This would return the actual sign bytes for the transaction
|
||||
// Implementation depends on the specific signing mode
|
||||
return []byte("cosmos-sign-bytes"), nil
|
||||
}
|
||||
|
||||
// GetType implements UnsignedTransaction interface
|
||||
func (c *CosmosUnsignedTx) GetType() TransactionType {
|
||||
return TransactionTypeCosmos
|
||||
}
|
||||
|
||||
// GetEncoding implements UnsignedTransaction interface
|
||||
func (c *CosmosUnsignedTx) GetEncoding() EncodingType {
|
||||
return c.Encoding
|
||||
}
|
||||
|
||||
// Sign implements UnsignedTransaction interface
|
||||
func (c *CosmosUnsignedTx) Sign(signature []byte, pubKey []byte) (SignedTransaction, error) {
|
||||
// Implementation would set the signature on the TxBuilder
|
||||
return &CosmosSignedTx{
|
||||
TxBuilder: c.TxBuilder,
|
||||
ChainID: c.ChainID,
|
||||
Encoding: c.Encoding,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRaw implements UnsignedTransaction interface
|
||||
func (c *CosmosUnsignedTx) GetRaw() any {
|
||||
return c.TxBuilder
|
||||
}
|
||||
|
||||
// CosmosSignedTx represents a signed Cosmos transaction
|
||||
type CosmosSignedTx struct {
|
||||
TxBuilder client.TxBuilder
|
||||
ChainID string
|
||||
Encoding EncodingType
|
||||
}
|
||||
|
||||
// GetHash implements SignedTransaction interface
|
||||
func (c *CosmosSignedTx) GetHash() string {
|
||||
// Implementation would calculate actual transaction hash
|
||||
return "0x..."
|
||||
}
|
||||
|
||||
// GetBytes implements SignedTransaction interface
|
||||
func (c *CosmosSignedTx) GetBytes() ([]byte, error) {
|
||||
// Implementation would serialize the transaction
|
||||
return []byte("serialized-cosmos-tx"), nil
|
||||
}
|
||||
|
||||
// GetType implements SignedTransaction interface
|
||||
func (c *CosmosSignedTx) GetType() TransactionType {
|
||||
return TransactionTypeCosmos
|
||||
}
|
||||
|
||||
// GetEncoding implements SignedTransaction interface
|
||||
func (c *CosmosSignedTx) GetEncoding() EncodingType {
|
||||
return c.Encoding
|
||||
}
|
||||
|
||||
// GetRaw implements SignedTransaction interface
|
||||
func (c *CosmosSignedTx) GetRaw() any {
|
||||
return c.TxBuilder
|
||||
}
|
||||
|
||||
// EVMUnsignedTx represents an unsigned EVM transaction
|
||||
type EVMUnsignedTx struct {
|
||||
Transaction *ethtypes.Transaction
|
||||
ChainID *big.Int
|
||||
}
|
||||
|
||||
// GetSignBytes implements UnsignedTransaction interface
|
||||
func (e *EVMUnsignedTx) GetSignBytes() ([]byte, error) {
|
||||
signer := ethtypes.NewEIP155Signer(e.ChainID)
|
||||
return signer.Hash(e.Transaction).Bytes(), nil
|
||||
}
|
||||
|
||||
// GetType implements UnsignedTransaction interface
|
||||
func (e *EVMUnsignedTx) GetType() TransactionType {
|
||||
return TransactionTypeEVM
|
||||
}
|
||||
|
||||
// GetEncoding implements UnsignedTransaction interface
|
||||
func (e *EVMUnsignedTx) GetEncoding() EncodingType {
|
||||
return EncodingTypeRLP
|
||||
}
|
||||
|
||||
// Sign implements UnsignedTransaction interface
|
||||
func (e *EVMUnsignedTx) Sign(signature []byte, pubKey []byte) (SignedTransaction, error) {
|
||||
// Implementation would create signed transaction from signature
|
||||
return &EVMSignedTx{
|
||||
Transaction: e.Transaction,
|
||||
ChainID: e.ChainID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRaw implements UnsignedTransaction interface
|
||||
func (e *EVMUnsignedTx) GetRaw() any {
|
||||
return e.Transaction
|
||||
}
|
||||
|
||||
// EVMSignedTx represents a signed EVM transaction
|
||||
type EVMSignedTx struct {
|
||||
Transaction *ethtypes.Transaction
|
||||
ChainID *big.Int
|
||||
}
|
||||
|
||||
// GetHash implements SignedTransaction interface
|
||||
func (e *EVMSignedTx) GetHash() string {
|
||||
return e.Transaction.Hash().Hex()
|
||||
}
|
||||
|
||||
// GetBytes implements SignedTransaction interface
|
||||
func (e *EVMSignedTx) GetBytes() ([]byte, error) {
|
||||
return e.Transaction.MarshalBinary()
|
||||
}
|
||||
|
||||
// GetType implements SignedTransaction interface
|
||||
func (e *EVMSignedTx) GetType() TransactionType {
|
||||
return TransactionTypeEVM
|
||||
}
|
||||
|
||||
// GetEncoding implements SignedTransaction interface
|
||||
func (e *EVMSignedTx) GetEncoding() EncodingType {
|
||||
return EncodingTypeRLP
|
||||
}
|
||||
|
||||
// GetRaw implements SignedTransaction interface
|
||||
func (e *EVMSignedTx) GetRaw() any {
|
||||
return e.Transaction
|
||||
}
|
||||
Reference in New Issue
Block a user