diff --git a/api/docgen/examples.go b/api/docgen/examples.go
index 572d5c3f04..5d80f99773 100644
--- a/api/docgen/examples.go
+++ b/api/docgen/examples.go
@@ -182,6 +182,8 @@ func init() {
state.WithKeyName("my_celes_key"),
state.WithSignerAddress("celestia1pjcmwj8w6hyr2c4wehakc5g8cfs36aysgucx66"),
state.WithFeeGranterAddress("celestia1hakc56ax66ypjcmwj8w6hyr2c4g8cfs3wesguc"),
+ state.WithMaxGasPrice(state.DefaultMaxGasPrice),
+ state.WithTxPriority(1),
))
}
diff --git a/go.mod b/go.mod
index a365488ed4..16a626ae60 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b
github.com/benbjohnson/clock v1.3.5
- github.com/celestiaorg/celestia-app/v3 v3.3.1
+ github.com/celestiaorg/celestia-app/v3 v3.3.0
github.com/celestiaorg/go-fraud v0.2.1
github.com/celestiaorg/go-header v0.6.4
github.com/celestiaorg/go-libp2p-messenger v0.2.0
diff --git a/go.sum b/go.sum
index d8c2c7becd..68569b0a9e 100644
--- a/go.sum
+++ b/go.sum
@@ -347,8 +347,8 @@ github.com/celestiaorg/blobstream-contracts/v3 v3.1.0 h1:h1Y4V3EMQ2mFmNtWt2sIhZI
github.com/celestiaorg/blobstream-contracts/v3 v3.1.0/go.mod h1:x4DKyfKOSv1ZJM9NwV+Pw01kH2CD7N5zTFclXIVJ6GQ=
github.com/celestiaorg/boxo v0.0.0-20241118122411-70a650316c3b h1:M9X7s1WJ/7Ju84ZUbO/6/8XlODkFsj/ln85AE0F6pj8=
github.com/celestiaorg/boxo v0.0.0-20241118122411-70a650316c3b/go.mod h1:OpUrJtGmZZktUqJvPOtmP8wSfEFcdF/55d3PNCcYLwc=
-github.com/celestiaorg/celestia-app/v3 v3.3.1 h1:e0iSWbf84mMOGU3aVCDd+I7a7wUQLXurHXhcmG6lyQI=
-github.com/celestiaorg/celestia-app/v3 v3.3.1/go.mod h1:FSv7/cIGoZIzcQIQPxTYYDeCO78A4VmC20jxf3Oqn4Y=
+github.com/celestiaorg/celestia-app/v3 v3.3.0 h1:NW/Sx5EZiGUrBIqojoqgKYFmVHsb3R7u2hWdOxZV+wI=
+github.com/celestiaorg/celestia-app/v3 v3.3.0/go.mod h1:MKhiQgATDdLouzC5KvXDAnDpEgIXyD0MNiq0ChrWFco=
github.com/celestiaorg/celestia-core v1.47.0-tm-v0.34.35 h1:K0kSVRlKfsPwfiA4o8GNUNPfZ+wF1MnYajom4CzJxpQ=
github.com/celestiaorg/celestia-core v1.47.0-tm-v0.34.35/go.mod h1:FSd32MUffdVUYIXW+m/1v5pHptRQF2RJC88fwsgrKG8=
github.com/celestiaorg/cosmos-sdk v1.27.0-sdk-v0.46.16 h1:qxWiGrDEcg4FzVTpIXU/v3wjP7q1Lz4AMhSBBRABInU=
diff --git a/libs/utils/address.go b/libs/utils/address.go
index b4efca7bf3..0f32f54831 100644
--- a/libs/utils/address.go
+++ b/libs/utils/address.go
@@ -10,14 +10,20 @@ import (
var ErrInvalidIP = errors.New("invalid IP address or hostname given")
-// SanitizeAddr trims leading protocol scheme and port from the given
-// IP address or hostname if present.
-func SanitizeAddr(addr string) (string, error) {
- original := addr
+// NormalizeAddress extracts the host and port, removing unsupported schemes.
+func NormalizeAddress(addr string) string {
addr = strings.TrimPrefix(addr, "http://")
addr = strings.TrimPrefix(addr, "https://")
addr = strings.TrimPrefix(addr, "tcp://")
addr = strings.TrimSuffix(addr, "/")
+ return addr
+}
+
+// SanitizeAddr trims leading protocol scheme and port from the given
+// IP address or hostname if present.
+func SanitizeAddr(addr string) (string, error) {
+ original := addr
+ addr = NormalizeAddress(addr)
addr = strings.Split(addr, ":")[0]
if addr == "" {
return "", fmt.Errorf("%w: %s", ErrInvalidIP, original)
diff --git a/nodebuilder/core/config.go b/nodebuilder/core/config.go
index 53472069de..6ca648b1af 100644
--- a/nodebuilder/core/config.go
+++ b/nodebuilder/core/config.go
@@ -13,6 +13,8 @@ const (
var MetricsEnabled bool
+type EstimatorAddress string
+
// Config combines all configuration fields for managing the relationship with a Core node.
type Config struct {
IP string
@@ -24,6 +26,8 @@ type Config struct {
// The JSON file should have a key-value pair where the key is "x-token" and the value is the authentication token.
// If left empty, the client will not include the X-Token in its requests.
XTokenPath string
+ // FeeEstimatorAddress specifies a third-party endpoint that will be used to calculate the gas price and gas.
+ FeeEstimatorAddress EstimatorAddress
}
// DefaultConfig returns default configuration for managing the
@@ -54,6 +58,8 @@ func (cfg *Config) Validate() error {
if err != nil {
return fmt.Errorf("nodebuilder/core: invalid grpc port: %s", err.Error())
}
+ pasedAddr := utils.NormalizeAddress(string(cfg.FeeEstimatorAddress))
+ cfg.FeeEstimatorAddress = EstimatorAddress(pasedAddr)
return nil
}
diff --git a/nodebuilder/core/flags.go b/nodebuilder/core/flags.go
index 24d5bcf367..8002af8921 100644
--- a/nodebuilder/core/flags.go
+++ b/nodebuilder/core/flags.go
@@ -8,11 +8,12 @@ import (
)
var (
- coreIPFlag = "core.ip"
- corePortFlag = "core.port"
- coreGRPCFlag = "core.grpc.port"
- coreTLS = "core.tls"
- coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
+ coreIPFlag = "core.ip"
+ corePortFlag = "core.port"
+ coreGRPCFlag = "core.grpc.port"
+ coreTLS = "core.tls"
+ coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
+ coreEstimatorAddressFlag = "core.estimator.address"
)
// Flags gives a set of hardcoded Core flags.
@@ -50,6 +51,13 @@ func Flags() *flag.FlagSet {
"NOTE: the path is parsed only if coreTLS enabled."+
"If left empty, the client will not include the X-Token in its requests.",
)
+ flags.String(
+ coreEstimatorAddressFlag,
+ "",
+ "specifies the endpoint of the third-party service that should be used to calculate"+
+ "the gas price and gas. Format:
:. Default connection to the consensus node will be used if "+
+ "left empty.",
+ )
return flags
}
@@ -88,5 +96,10 @@ func ParseFlags(
}
}
cfg.IP = coreIP
+
+ if cmd.Flag(coreEstimatorAddressFlag).Changed {
+ addr := cmd.Flag(coreEstimatorAddressFlag).Value.String()
+ cfg.FeeEstimatorAddress = EstimatorAddress(addr)
+ }
return cfg.Validate()
}
diff --git a/nodebuilder/state/cmd/state.go b/nodebuilder/state/cmd/state.go
index 3e254d554b..79f9fa216a 100644
--- a/nodebuilder/state/cmd/state.go
+++ b/nodebuilder/state/cmd/state.go
@@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"strconv"
+ "strings"
"cosmossdk.io/math"
"github.com/spf13/cobra"
@@ -18,6 +19,8 @@ var (
gasPrice float64
feeGranterAddress string
amount uint64
+ txPriority int
+ maxGasPrice float64
)
func init() {
@@ -460,6 +463,25 @@ func ApplyFlags(cmds ...*cobra.Command) {
"Note: The granter should be provided as a Bech32 address.\n"+
"Example: celestiaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
)
+
+ // add additional flags for all submit transactions besides submit blobs.
+ if !strings.Contains(cmd.Name(), "blob") {
+ cmd.PersistentFlags().Float64Var(
+ &maxGasPrice,
+ "max.gas.price",
+ state.DefaultMaxGasPrice,
+ "Specifies max gas price for the tx submission.",
+ )
+ cmd.PersistentFlags().IntVar(
+ &txPriority,
+ "tx.priority",
+ state.TxPriorityMedium,
+ "Specifies tx priority. Should be set in range:"+
+ "1. TxPriorityLow;\n"+
+ "2. TxPriorityMedium;\n"+
+ "3. TxPriorityHigh.\nDefault: TxPriorityMedium",
+ )
+ }
}
}
@@ -470,5 +492,7 @@ func GetTxConfig() *state.TxConfig {
state.WithKeyName(keyName),
state.WithSignerAddress(signer),
state.WithFeeGranterAddress(feeGranterAddress),
+ state.WithMaxGasPrice(maxGasPrice),
+ state.WithTxPriority(txPriority),
)
}
diff --git a/nodebuilder/state/core.go b/nodebuilder/state/core.go
index d66da88c44..157563d4b5 100644
--- a/nodebuilder/state/core.go
+++ b/nodebuilder/state/core.go
@@ -8,6 +8,7 @@ import (
"github.com/celestiaorg/go-header/sync"
"github.com/celestiaorg/celestia-node/header"
+ "github.com/celestiaorg/celestia-node/nodebuilder/core"
modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud"
"github.com/celestiaorg/celestia-node/nodebuilder/p2p"
"github.com/celestiaorg/celestia-node/share/eds/byzantine"
@@ -23,13 +24,14 @@ func coreAccessor(
fraudServ libfraud.Service[*header.ExtendedHeader],
network p2p.Network,
client *grpc.ClientConn,
+ address core.EstimatorAddress,
) (
*state.CoreAccessor,
Module,
*modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader],
error,
) {
- ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, client, network.String())
+ ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, client, network.String(), string(address))
sBreaker := &modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]{
Service: ca,
diff --git a/nodebuilder/state/module.go b/nodebuilder/state/module.go
index 0e80ab3209..c47a09f2db 100644
--- a/nodebuilder/state/module.go
+++ b/nodebuilder/state/module.go
@@ -25,6 +25,7 @@ func ConstructModule(tp node.Type, cfg *Config, coreCfg *core.Config) fx.Option
cfgErr := cfg.Validate()
baseComponents := fx.Options(
fx.Supply(*cfg),
+ fx.Supply(coreCfg.FeeEstimatorAddress),
fx.Error(cfgErr),
fx.Provide(func(ks keystore.Keystore) (keyring.Keyring, AccountName, error) {
return Keyring(*cfg, ks)
diff --git a/state/core_access.go b/state/core_access.go
index bbc3025fb9..5ab82784fe 100644
--- a/state/core_access.go
+++ b/state/core_access.go
@@ -35,9 +35,9 @@ const (
)
var (
- ErrInvalidAmount = errors.New("state: amount must be greater than zero")
-
- log = logging.Logger("state")
+ ErrInvalidAmount = errors.New("state: amount must be greater than zero")
+ errGasPriceExceedsLimit = errors.New("state: estimated gasPrice exceeds max gasPrice")
+ log = logging.Logger("state")
)
// CoreAccessor implements service over a gRPC connection
@@ -63,6 +63,7 @@ type CoreAccessor struct {
coreConn *grpc.ClientConn
network string
+ estimator *estimator
// these fields are mutatable and thus need to be protected by a mutex
lock sync.Mutex
lastPayForBlob int64
@@ -83,6 +84,7 @@ func NewCoreAccessor(
getter libhead.Head[*header.ExtendedHeader],
conn *grpc.ClientConn,
network string,
+ estimatorAddress string,
) (*CoreAccessor, error) {
// create verifier
prt := merkle.DefaultProofRuntime()
@@ -106,6 +108,7 @@ func NewCoreAccessor(
prt: prt,
coreConn: conn,
network: network,
+ estimator: &estimator{estimatorAddress: estimatorAddress, defaultClientConn: conn},
}
return ca, nil
}
@@ -136,6 +139,7 @@ func (ca *CoreAccessor) Start(ctx context.Context) error {
if err != nil {
return fmt.Errorf("querying minimum gas price: %w", err)
}
+ ca.estimator.connect()
return nil
}
@@ -176,7 +180,7 @@ func (ca *CoreAccessor) SubmitPayForBlob(
for i, blob := range libBlobs {
blobSizes[i] = uint32(len(blob.Data()))
}
- gas = estimateGasForBlobs(blobSizes)
+ gas = ca.estimator.estimateGasForBlobs(blobSizes)
}
gasPrice := cfg.GasPrice()
@@ -574,22 +578,6 @@ func (ca *CoreAccessor) submitMsg(
}
txConfig := make([]user.TxOption, 0)
- gas := cfg.GasLimit()
-
- if gas == 0 {
- gas, err = estimateGas(ctx, client, msg)
- if err != nil {
- return nil, fmt.Errorf("estimating gas: %w", err)
- }
- }
-
- gasPrice := cfg.GasPrice()
- if gasPrice == DefaultGasPrice {
- gasPrice = ca.minGasPrice
- }
-
- txConfig = append(txConfig, user.SetGasLimitAndGasPrice(gas, gasPrice))
-
if cfg.FeeGranterAddress() != "" {
granter, err := parseAccAddressFromString(cfg.FeeGranterAddress())
if err != nil {
@@ -598,7 +586,33 @@ func (ca *CoreAccessor) submitMsg(
txConfig = append(txConfig, user.SetFeeGranter(granter))
}
+ if cfg.GasLimit() == 0 || cfg.GasPrice() == DefaultGasPrice {
+ gasPrice, gas, err := ca.estimator.estimateGas(ctx, client, cfg.priority, msg)
+ if err != nil {
+ return nil, err
+ }
+
+ if cfg.GasLimit() == 0 {
+ cfg.gas = gas
+ }
+ if cfg.GasPrice() == DefaultGasPrice {
+ migGasPrice := ca.getMinGasPrice()
+ if gasPrice < migGasPrice {
+ gasPrice = migGasPrice
+ }
+ cfg.gasPrice = gasPrice
+ cfg.isGasPriceSet = true
+ }
+ }
+
+ if cfg.maxGasPrice < cfg.GasPrice() {
+ return nil, errGasPriceExceedsLimit
+ }
+ txConfig = append(txConfig, user.SetGasLimitAndGasPrice(cfg.GasLimit(), cfg.GasPrice()))
resp, err := client.SubmitTx(ctx, []sdktypes.Msg{msg}, txConfig...)
+ if err != nil {
+ return nil, err
+ }
return convertToSdkTxResponse(resp), err
}
diff --git a/state/core_access_test.go b/state/core_access_test.go
index 33c467a9fe..f7908872e7 100644
--- a/state/core_access_test.go
+++ b/state/core_access_test.go
@@ -131,6 +131,13 @@ func TestTransfer(t *testing.T) {
account: accounts[2],
expErr: nil,
},
+ {
+ name: "gas price limit exceeded",
+ gasPrice: DefaultMaxGasPrice * 100,
+ gasLim: 1000,
+ account: accounts[2],
+ expErr: errGasPriceExceedsLimit,
+ },
}
for _, tc := range testcases {
@@ -265,7 +272,7 @@ func buildAccessor(t *testing.T) (*CoreAccessor, []string) {
conn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
- ca, err := NewCoreAccessor(cctx.Keyring, accounts[0].Name, nil, conn, chainID)
+ ca, err := NewCoreAccessor(cctx.Keyring, accounts[0].Name, nil, conn, chainID, "")
require.NoError(t, err)
return ca, getNames(accounts)
}
diff --git a/state/estimator.go b/state/estimator.go
new file mode 100644
index 0000000000..b290064ec1
--- /dev/null
+++ b/state/estimator.go
@@ -0,0 +1,104 @@
+package state
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ sdktypes "github.com/cosmos/cosmos-sdk/types"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/connectivity"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/celestiaorg/celestia-app/v3/pkg/user"
+ apptypes "github.com/celestiaorg/celestia-app/v3/x/blob/types"
+)
+
+// gasMultiplier is used to increase gas limit in case if tx has additional options.
+const gasMultiplier = 1.1
+
+type estimator struct {
+ estimatorAddress string
+
+ defaultClientConn *grpc.ClientConn
+ estimatorConn *grpc.ClientConn
+}
+
+func (e *estimator) connect() {
+ if e.estimatorConn != nil && e.estimatorConn.GetState() != connectivity.Shutdown {
+ return
+ }
+ if e.estimatorAddress == "" {
+ return
+ }
+
+ conn, err := grpc.NewClient(e.estimatorAddress, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ log.Warn("state: failed to connect to estimator endpoint", "err", err)
+ return
+ }
+ e.estimatorConn = conn
+}
+
+// estimateGas estimates gas in case it has not been set.
+func (e *estimator) estimateGas(
+ ctx context.Context,
+ client *user.TxClient,
+ priority TxPriority,
+ msg sdktypes.Msg,
+) (float64, uint64, error) {
+ signer := client.Signer()
+ rawTx, err := signer.CreateTx([]sdktypes.Msg{msg}, user.SetFee(1))
+ if err != nil {
+ return 0, 0, fmt.Errorf("failed to create raw tx: %w", err)
+ }
+
+ gasPrice, gas, err := e.queryGasUsedAndPrice(ctx, signer, priority, rawTx)
+ if err == nil {
+ return gasPrice, uint64(float64(gas) * gasMultiplier), nil
+ }
+
+ // NOTE: final result of the estimation will be multiplied by the `gasMultiplier`(1.1) to cover
+ // additional costs.
+ // set fee as 1utia helps to simulate the tx more reliably.
+ gas, err = client.EstimateGas(ctx, []sdktypes.Msg{msg}, user.SetFee(1))
+ if err != nil {
+ return 0, 0, fmt.Errorf("state: failed to estimate gas: %w", err)
+ }
+ return DefaultGasPrice, uint64(float64(gas) * gasMultiplier), nil
+}
+
+func (e *estimator) queryGasUsedAndPrice(
+ ctx context.Context,
+ signer *user.Signer,
+ priority TxPriority,
+ rawTx []byte,
+) (float64, uint64, error) {
+ e.connect()
+
+ if e.estimatorConn != nil && e.estimatorConn.GetState() != connectivity.Shutdown {
+ gasPrice, gas, err := signer.QueryGasUsedAndPrice(ctx, e.estimatorConn, priority.toApp(), rawTx)
+ if err == nil {
+ return gasPrice, gas, nil
+ }
+ log.Warn("failed to query gas used and price from the estimator endpoint.", "err", err)
+ }
+
+ if e.defaultClientConn == nil || e.defaultClientConn.GetState() == connectivity.Shutdown {
+ return 0, 0, errors.New("connection with the consensus node is dropped")
+ }
+
+ gasPrice, gas, err := signer.QueryGasUsedAndPrice(ctx, e.defaultClientConn, priority.toApp(), rawTx)
+ if err != nil {
+ log.Warn("state: failed to query gas used and price from the default endpoint", "err", err)
+ }
+ return gasPrice, gas, err
+}
+
+// estimateGasForBlobs returns a gas limit that can be applied to the `MsgPayForBlob` transactions.
+// NOTE: final result of the estimation will be multiplied by the `gasMultiplier`(1.1)
+// to cover additional options of the Tx.
+func (e *estimator) estimateGasForBlobs(blobSizes []uint32) uint64 {
+ gas := apptypes.DefaultEstimateGas(blobSizes)
+ return uint64(float64(gas) * gasMultiplier)
+}
diff --git a/state/estimator_test.go b/state/estimator_test.go
new file mode 100644
index 0000000000..24868f4bcc
--- /dev/null
+++ b/state/estimator_test.go
@@ -0,0 +1,117 @@
+package state
+
+import (
+ "context"
+ "errors"
+ "net"
+ "testing"
+
+ sdk "github.com/cosmos/cosmos-sdk/types"
+ banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/celestiaorg/celestia-app/v3/app"
+ "github.com/celestiaorg/celestia-app/v3/app/encoding"
+ "github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation"
+ "github.com/celestiaorg/celestia-app/v3/pkg/appconsts"
+ "github.com/celestiaorg/celestia-app/v3/pkg/user"
+ "github.com/celestiaorg/celestia-app/v3/test/util/testfactory"
+ "github.com/celestiaorg/celestia-app/v3/test/util/testnode"
+)
+
+type mockEstimatorServer struct {
+ *gasestimation.UnimplementedGasEstimatorServer
+ conn *grpc.Server
+}
+
+func (m *mockEstimatorServer) EstimateGasPriceAndUsage(
+ context.Context,
+ *gasestimation.EstimateGasPriceAndUsageRequest,
+) (*gasestimation.EstimateGasPriceAndUsageResponse, error) {
+ return &gasestimation.EstimateGasPriceAndUsageResponse{
+ EstimatedGasPrice: 0.02,
+ EstimatedGasUsed: 70000,
+ }, nil
+}
+
+func setupServer(t *testing.T) *mockEstimatorServer {
+ t.Helper()
+ net, err := net.Listen("tcp", ":9090")
+ require.NoError(t, err)
+
+ grpcServer := grpc.NewServer()
+ gasestimation.RegisterGasEstimatorServer(grpcServer, &mockEstimatorServer{})
+
+ go func() {
+ err := grpcServer.Serve(net)
+ if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
+ panic(err)
+ }
+ }()
+
+ return &mockEstimatorServer{conn: grpcServer}
+}
+
+func (m *mockEstimatorServer) stop() {
+ m.conn.GracefulStop()
+}
+
+func TestEstimator(t *testing.T) {
+ server := setupServer(t)
+ t.Cleanup(server.stop)
+ target := "0.0.0.0:9090"
+
+ accountName := "test"
+ config := encoding.MakeConfig(app.ModuleEncodingRegisters...)
+ keyring := testfactory.TestKeyring(config.Codec, accountName)
+ account := user.NewAccount(accountName, 0, 0)
+ signer, err := user.NewSigner(keyring, config.TxConfig, "test", appconsts.LatestVersion, account)
+ require.NoError(t, err)
+
+ msgSend := banktypes.NewMsgSend(
+ account.Address(),
+ testnode.RandomAddress().(sdk.AccAddress),
+ sdk.NewCoins(sdk.NewInt64Coin(appconsts.BondDenom, 10)),
+ )
+ rawTx, err := signer.CreateTx([]sdk.Msg{msgSend})
+ require.NoError(t, err)
+
+ defaultConn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
+ require.NoError(t, err)
+ estimator := estimator{}
+
+ testCases := []struct {
+ name string
+ doFn func()
+ }{
+ {
+ name: "query from estimator endpoint",
+ doFn: func() {
+ estimator.estimatorAddress = target
+ },
+ },
+ {
+ name: "query from default estimator endpoint",
+ doFn: func() {
+ // cleanup estimator service
+ estimator.estimatorAddress = ""
+ require.NoError(t, estimator.estimatorConn.Close())
+
+ estimator.defaultClientConn = defaultConn
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.doFn()
+ gasPrice, gas, err := estimator.queryGasUsedAndPrice(context.Background(), signer, TxPriorityMedium, rawTx)
+ require.NoError(t, err)
+ assert.Greater(t, gasPrice, float64(0))
+ assert.Greater(t, gas, uint64(0))
+ })
+ }
+}
diff --git a/state/integration_test.go b/state/integration_test.go
index f606841417..302f69e19d 100644
--- a/state/integration_test.go
+++ b/state/integration_test.go
@@ -52,7 +52,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
s.Require().Greater(len(s.accounts), 0)
accountName := s.accounts[0].Name
- accessor, err := NewCoreAccessor(s.cctx.Keyring, accountName, localHeader{s.cctx.Client}, nil, "")
+ accessor, err := NewCoreAccessor(s.cctx.Keyring, accountName, localHeader{s.cctx.Client}, nil, "", "")
require.NoError(s.T(), err)
ctx, cancel := context.WithCancel(context.Background())
accessor.ctx = ctx
diff --git a/state/tx_config.go b/state/tx_config.go
index 179a33050a..421454736e 100644
--- a/state/tx_config.go
+++ b/state/tx_config.go
@@ -1,36 +1,56 @@
package state
import (
- "context"
"encoding/json"
"fmt"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdktypes "github.com/cosmos/cosmos-sdk/types"
- "github.com/celestiaorg/celestia-app/v3/pkg/user"
- apptypes "github.com/celestiaorg/celestia-app/v3/x/blob/types"
+ "github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation"
+ "github.com/celestiaorg/celestia-app/v3/pkg/appconsts"
)
const (
// DefaultGasPrice specifies the default gas price value to be used when the user
// wants to use the global minimal gas price, which is fetched from the celestia-app.
DefaultGasPrice float64 = -1.0
- // gasMultiplier is used to increase gas limit in case if tx has additional cfg.
- gasMultiplier = 1.1
+
+ DefaultMaxGasPrice = appconsts.DefaultMinGasPrice * 100
)
// NewTxConfig constructs a new TxConfig with the provided options.
// It starts with a DefaultGasPrice and then applies any additional
// options provided through the variadic parameter.
func NewTxConfig(opts ...ConfigOption) *TxConfig {
- options := &TxConfig{gasPrice: DefaultGasPrice}
+ options := &TxConfig{gasPrice: DefaultGasPrice, maxGasPrice: DefaultMaxGasPrice}
for _, opt := range opts {
opt(options)
}
return options
}
+type TxPriority int
+
+const (
+ TxPriorityLow = iota + 1
+ TxPriorityMedium
+ TxPriorityHigh
+)
+
+func (t TxPriority) toApp() gasestimation.TxPriority {
+ switch t {
+ case TxPriorityLow:
+ return gasestimation.TxPriority_TX_PRIORITY_LOW
+ case TxPriorityMedium:
+ return gasestimation.TxPriority_TX_PRIORITY_MEDIUM
+ case TxPriorityHigh:
+ return gasestimation.TxPriority_TX_PRIORITY_HIGH
+ default:
+ return gasestimation.TxPriority_TX_PRIORITY_UNSPECIFIED
+ }
+}
+
// TxConfig specifies additional options that will be applied to the Tx.
type TxConfig struct {
// Specifies the address from the keystore that will sign transactions.
@@ -48,8 +68,16 @@ type TxConfig struct {
gasPrice float64
// since gasPrice can be 0, it is necessary to understand that user explicitly set it.
isGasPriceSet bool
+ // specifies the max gas price that user expects to pay for the transaction.
+ maxGasPrice float64
// 0 gas means users want us to calculate it for them.
gas uint64
+ // priority is the priority level of the requested gas price.
+ // - Low: The gas price is the value at the end of the lowest 10% of gas prices from the last 5 blocks.
+ // - Medium: The gas price is the mean of all gas prices from the last 5 blocks.
+ // - High: The gas price is the price at the start of the top 10% of transactions’ gas prices from the last 5 blocks.
+ // - Default: Medium.
+ priority TxPriority
// Specifies the account that will pay for the transaction.
// Input format Bech32.
feeGranterAddress string
@@ -62,7 +90,8 @@ func (cfg *TxConfig) GasPrice() float64 {
return cfg.gasPrice
}
-func (cfg *TxConfig) GasLimit() uint64 { return cfg.gas }
+func (cfg *TxConfig) GasLimit() uint64 { return cfg.gas }
+func (cfg *TxConfig) MaxGasPrice() float64 { return cfg.maxGasPrice }
func (cfg *TxConfig) KeyName() string { return cfg.keyName }
@@ -73,7 +102,9 @@ func (cfg *TxConfig) FeeGranterAddress() string { return cfg.feeGranterAddress }
type jsonTxConfig struct {
GasPrice float64 `json:"gas_price,omitempty"`
IsGasPriceSet bool `json:"is_gas_price_set,omitempty"`
+ MaxGasPrice float64 `json:"max_gas_price"`
Gas uint64 `json:"gas,omitempty"`
+ TxPriority int `json:"tx_priority,omitempty"`
KeyName string `json:"key_name,omitempty"`
SignerAddress string `json:"signer_address,omitempty"`
FeeGranterAddress string `json:"fee_granter_address,omitempty"`
@@ -81,11 +112,13 @@ type jsonTxConfig struct {
func (cfg *TxConfig) MarshalJSON() ([]byte, error) {
jsonOpts := &jsonTxConfig{
- SignerAddress: cfg.signerAddress,
- KeyName: cfg.keyName,
GasPrice: cfg.gasPrice,
IsGasPriceSet: cfg.isGasPriceSet,
+ MaxGasPrice: cfg.maxGasPrice,
Gas: cfg.gas,
+ TxPriority: int(cfg.priority),
+ KeyName: cfg.keyName,
+ SignerAddress: cfg.signerAddress,
FeeGranterAddress: cfg.feeGranterAddress,
}
return json.Marshal(jsonOpts)
@@ -98,35 +131,17 @@ func (cfg *TxConfig) UnmarshalJSON(data []byte) error {
return fmt.Errorf("unmarshalling TxConfig: %w", err)
}
- cfg.keyName = jsonOpts.KeyName
- cfg.signerAddress = jsonOpts.SignerAddress
cfg.gasPrice = jsonOpts.GasPrice
cfg.isGasPriceSet = jsonOpts.IsGasPriceSet
+ cfg.maxGasPrice = jsonOpts.MaxGasPrice
cfg.gas = jsonOpts.Gas
+ cfg.priority = TxPriority(jsonOpts.TxPriority)
+ cfg.keyName = jsonOpts.KeyName
+ cfg.signerAddress = jsonOpts.SignerAddress
cfg.feeGranterAddress = jsonOpts.FeeGranterAddress
return nil
}
-// estimateGas estimates gas in case it has not been set.
-// NOTE: final result of the estimation will be multiplied by the `gasMultiplier`(1.1) to cover
-// additional costs.
-func estimateGas(ctx context.Context, client *user.TxClient, msg sdktypes.Msg) (uint64, error) {
- // set fee as 1utia helps to simulate the tx more reliably.
- gas, err := client.EstimateGas(ctx, []sdktypes.Msg{msg}, user.SetFee(1))
- if err != nil {
- return 0, fmt.Errorf("estimating gas: %w", err)
- }
- return uint64(float64(gas) * gasMultiplier), nil
-}
-
-// estimateGasForBlobs returns a gas limit that can be applied to the `MsgPayForBlob` transactions.
-// NOTE: final result of the estimation will be multiplied by the `gasMultiplier`(1.1)
-// to cover additional options of the Tx.
-func estimateGasForBlobs(blobSizes []uint32) uint64 {
- gas := apptypes.DefaultEstimateGas(blobSizes)
- return uint64(float64(gas) * gasMultiplier)
-}
-
func parseAccountKey(kr keyring.Keyring, accountKey string) (sdktypes.AccAddress, error) {
rec, err := kr.Key(accountKey)
if err != nil {
@@ -187,3 +202,20 @@ func WithFeeGranterAddress(granter string) ConfigOption {
cfg.feeGranterAddress = granter
}
}
+
+// WithMaxGasPrice is an option that allows you to specify a `maxGasPrice` field.
+func WithMaxGasPrice(gasPrice float64) ConfigOption {
+ return func(cfg *TxConfig) {
+ cfg.maxGasPrice = gasPrice
+ }
+}
+
+// WithTxPriority is an option that allows you to specify a priority of the tx.
+func WithTxPriority(priority int) ConfigOption {
+ return func(cfg *TxConfig) {
+ if priority < 0 || priority > 3 {
+ return
+ }
+ cfg.priority = TxPriority(priority)
+ }
+}