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) + } +}