globalrpc

package module
v0.4.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 30, 2026 License: MIT Imports: 17 Imported by: 0

README

globalrpc

EVM RPC and WSS manager with nonce tracking. HTTPS connections are pooled and reused across calls.

Usage

Setup

  • define a configuration file as provided in the example below
  • Redis needs to be present and accessed via redisAddr and redisPw (password can be an empty string)
  • create an instance:
grpc, err := globalrpc.NewGlobalRpc(chainId, "rpc_config.json", redisAddr, redisPw)

RpcQuery (for read operations)

Retries across different RPC nodes. Return a NonRetryableError if retrying won't help.

result, err := globalrpc.RpcQuery(ctx, grpc, 4, 10*time.Second,
    func(ctx context.Context, rpc *ethclient.Client) ([]string, error) {
        return myFunction(poolId, rpc)
    },
)

RpcExec (for write operations)

_, err := globalrpc.RpcExec(ctx, grpc, 3, 2*time.Second,
    func(ctx context.Context, rpc *ethclient.Client, attempt int, prevErr error) (int, error) {
        // globalrpc related errors. Here just for proper logging on the resons of teh retry but can be used to trigger specific logic on certain errors if needed
        var rpcErr *globalrpc.RpcError
        if errors.As(prevErr, &rpcErr) {
            slog.Warn("rpc error, retrying on next node", "attempt", attempt, "kind", rpcErr.Kind, "err", rpcErr)
        }

        // tx error
        var txErr *globalrpc.TxError
        if errors.As(prevErr, &txErr) {
            switch txErr.Kind {
            case globalrpc.TxErrNonceTooLow:
                // reseed from chain then retrying should work if nonce was the issue
                if err := tracker.Seed(ctx, rpc); err != nil {
                    return 0, globalrpc.NewNonRetryableError(err)
                }
            case globalrpc.TxErrNonceTooHigh:
                // blob tx nonce gap, reseed
                if err := tracker.Seed(ctx, rpc); err != nil {
                    return 0, globalrpc.NewNonRetryableError(err)
                }
            case globalrpc.TxErrAlreadyKnown:
                // exact tx already in mempool safe to skip
                return 0, nil
            case globalrpc.TxErrReplaceUnderpriced:
                //  gas too low to replace
                // ..
            case globalrpc.TxErrIntrinsicGas:
                // gas limit below minimum
                // to solve before retrying
            case globalrpc.TxErrFeeCapTooLow:
                // maxFeePerGas below base fee
                // to solve before retrying
            case globalrpc.TxErrInsufficientFunds:
                // wallet can't cover gas + value no point retrying
                return 0, globalrpc.NewNonRetryableError(prevErr)
            case globalrpc.TxErrGasLimit:
                // gas exceeds block limit no point retrying
                return 0, globalrpc.NewNonRetryableError(prevErr)
            case globalrpc.TxErrTxTypeNotSupported:
                // chain doesn't support this tx type so no point retrying
                return 0, globalrpc.NewNonRetryableError(prevErr)
            }
            slog.Warn("exec failed, retrying", "attempt", attempt, "kind", txErr.Kind, "prevErr", prevErr)
        }

        // not infrastructure and  not a known tx error sends back the raw error
        // most likely retrying will likely get the same result so we can skip retrying and return the error right away or decide to retry based on the error
        if prevErr != nil && !errors.As(prevErr, &rpcErr) && !errors.As(prevErr, &txErr) {
            return 0, globalrpc.NewNonRetryableError(prevErr)
        }

        return 0, submitTx(ctx, rpc, baseFeeScale)
    },
)

RpcDial

Pins all calls to the same RPC node. Lock is automatically renewed until cleanup is called.

client, cleanup, err := globalrpc.RpcDial(ctx, grpc, globalrpc.TypeHTTPS)
if err != nil { return err }
defer cleanup()

Nonce Tracking

Redis-backed nonce counter. Seed on startup (always syncs from chain), then call Next() for each transaction:

tracker := grpc.NewNonceTracker(address)
tracker.Seed(ctx, client)
nonce, err := tracker.Next()

// on nonce error, resync from chain:
tracker.Seed(ctx, client)

RPC Config File Example

[
    {
        "chainId": 999,
        "https": [
            "https://rpc.hyperliquid.xyz/evm",
            "https://rpc.hypurrscan.io",
            "https://hyperliquid.drpc.org",
            "https://rpc.hyperlend.finance"
        ],
        "wss": [
            "wss://hyperliquid.drpc.org",
            "wss://api.hyperliquid.xyz/ws"
        ]
    },
    {
        "chainId": 989,
        "https": [
            "https://spectrum-01.simplystaking.xyz/hyperliquid-tn-rpc/evm",
            "https://rpc.hyperliquid-testnet.xyz/evm"
        ],
        "wss": []
    }
]

Documentation

Index

Constants

View Source
const (
	EXPIRY_SEC         = "10" // rpc locks expire after this amount of time
	REDIS_KEY_CURR_IDX = "glbl_rpc:idx"
	REDIS_KEY_URLS     = "glbl_rpc:urls"
	REDIS_KEY_LOCK     = "glbl_rpc:lock"
	REDIS_SET_URL_LOCK = "glbl_rpc:urls_lock"
)
View Source
const LUA_ACQUIRE = `` /* 370-byte string literal not displayed */
View Source
const LUA_NONCE_NEXT = `` /* 144-byte string literal not displayed */
View Source
const LUA_RELEASE = `
	if redis.call("GET", KEYS[1]) == ARGV[1] then
		return redis.call("DEL", KEYS[1])
	end
	return 0
`
View Source
const LUA_RENEW = `
	if redis.call("GET", KEYS[1]) == ARGV[1] then
		return redis.call("EXPIRE", KEYS[1], ARGV[2])
	end
	return 0
`
View Source
const REDIS_KEY_NONCE = "glbl_rpc:nonce:"

Variables

This section is empty.

Functions

func IsAccountLimit added in v0.3.0

func IsAccountLimit(err error) bool

func IsAlreadyKnown added in v0.3.0

func IsAlreadyKnown(err error) bool

func IsAlreadyReserved added in v0.3.0

func IsAlreadyReserved(err error) bool

func IsAuthorityReserved added in v0.3.0

func IsAuthorityReserved(err error) bool

func IsBlobFeeCapTooLow added in v0.3.0

func IsBlobFeeCapTooLow(err error) bool

func IsBlobLimitExceeded added in v0.3.0

func IsBlobLimitExceeded(err error) bool

func IsBlobSidecarConvFailed added in v0.3.0

func IsBlobSidecarConvFailed(err error) bool

func IsBlobTxCreate added in v0.3.0

func IsBlobTxCreate(err error) bool

func IsEIP155Required added in v0.3.0

func IsEIP155Required(err error) bool

func IsEmptyAuthList added in v0.3.0

func IsEmptyAuthList(err error) bool

func IsFeeCapTooLow added in v0.3.0

func IsFeeCapTooLow(err error) bool

func IsFeeCapVeryHigh added in v0.3.0

func IsFeeCapVeryHigh(err error) bool

func IsFloorDataGas added in v0.3.0

func IsFloorDataGas(err error) bool

func IsFutureReplacePending added in v0.3.0

func IsFutureReplacePending(err error) bool

func IsGasLimit added in v0.3.0

func IsGasLimit(err error) bool

func IsGasLimitReached added in v0.3.0

func IsGasLimitReached(err error) bool

func IsGasLimitTooHigh added in v0.3.0

func IsGasLimitTooHigh(err error) bool

func IsGasPriceTooLow added in v0.3.0

func IsGasPriceTooLow(err error) bool

func IsGasUintOverflow added in v0.3.0

func IsGasUintOverflow(err error) bool

func IsInflightLimit added in v0.3.0

func IsInflightLimit(err error) bool

func IsInsufficientFunds added in v0.3.0

func IsInsufficientFunds(err error) bool

func IsInsufficientFundsForTransfer added in v0.3.0

func IsInsufficientFundsForTransfer(err error) bool

func IsIntrinsicGas added in v0.3.0

func IsIntrinsicGas(err error) bool

func IsInvalidSender added in v0.3.0

func IsInvalidSender(err error) bool

func IsInvalidSig added in v0.3.0

func IsInvalidSig(err error) bool

func IsKZGVerificationError added in v0.3.0

func IsKZGVerificationError(err error) bool

func IsMaxInitCodeSize added in v0.3.0

func IsMaxInitCodeSize(err error) bool

func IsMissingBlobHashes added in v0.3.0

func IsMissingBlobHashes(err error) bool

func IsNegativeValue added in v0.3.0

func IsNegativeValue(err error) bool

func IsNonRetryable

func IsNonRetryable(err error) bool

IsNonRetryable reports whether err (or any error in its chain) is a NonRetryableError.

func IsNonceMax added in v0.3.0

func IsNonceMax(err error) bool

func IsNonceTooHigh added in v0.3.0

func IsNonceTooHigh(err error) bool

func IsNonceTooLow added in v0.3.0

func IsNonceTooLow(err error) bool

func IsOutOfOrderTxDelegated added in v0.3.0

func IsOutOfOrderTxDelegated(err error) bool

func IsOversizedData added in v0.3.0

func IsOversizedData(err error) bool

func IsPoolFull added in v0.3.0

func IsPoolFull(err error) bool

func IsReplaceUnderpriced added in v0.3.0

func IsReplaceUnderpriced(err error) bool

func IsSenderNoEOA added in v0.3.0

func IsSenderNoEOA(err error) bool

func IsSetCodeTxCreate added in v0.3.0

func IsSetCodeTxCreate(err error) bool

func IsTipAboveFeeCap added in v0.3.0

func IsTipAboveFeeCap(err error) bool

func IsTipVeryHigh added in v0.3.0

func IsTipVeryHigh(err error) bool

func IsTooManyBlobs added in v0.3.0

func IsTooManyBlobs(err error) bool

func IsTxFeeTooHigh added in v0.3.0

func IsTxFeeTooHigh(err error) bool

func IsTxTypeNotSupported added in v0.3.0

func IsTxTypeNotSupported(err error) bool

func IsUint256Overflow added in v0.3.0

func IsUint256Overflow(err error) bool

func IsUnderpriced added in v0.3.0

func IsUnderpriced(err error) bool

func IsUnexpectedProtection added in v0.3.0

func IsUnexpectedProtection(err error) bool

func NewNonRetryableError

func NewNonRetryableError(err error) error

NewNonRetryableError wraps err so that RpcQuery will not retry.

func RpcDial added in v0.1.1

func RpcDial(ctx context.Context, rpcH *GlobalRpc, rpcType RPCKind) (*ethclient.Client, func(), string, error)

func RpcExec added in v0.3.0

func RpcExec[T any](
	ctx context.Context,
	rpcH *GlobalRpc,
	attempts int,
	wait time.Duration,
	call func(ctx context.Context, rpc *ethclient.Client, attempt int, prevErr error) (T, error),
) (T, error)

RpcExec retries a write operation across RPC nodes. The callback's prevErr is either a *TxError (classified tx rejection), a *RpcError (infrastructure), or a plain error (unclassified). On the first attempt prevErr is nil.

func RpcQuery

func RpcQuery[T any](
	ctx context.Context,
	rpcH *GlobalRpc,
	attempts int,
	wait time.Duration,
	call func(ctx context.Context, rpc *ethclient.Client) (T, error),
) (T, error)

Types

type GlobalRpc

type GlobalRpc struct {
	Config RpcConfig
	// contains filtered or unexported fields
}

func NewGlobalRpc

func NewGlobalRpc(chainId int, configname, redisAddr, redisPw string, opts ...Option) (*GlobalRpc, error)

func (*GlobalRpc) GetAndLockRpc

func (gr *GlobalRpc) GetAndLockRpc(ctx context.Context, rpcType RPCKind, maxWaitSec int) (Receipt, error)

func (*GlobalRpc) NewNonceTracker added in v0.1.1

func (gr *GlobalRpc) NewNonceTracker(address common.Address) NonceProvider

func (*GlobalRpc) ReturnLock

func (gr *GlobalRpc) ReturnLock(rec Receipt)

type NonRetryableError

type NonRetryableError struct {
	Err error
}

NonRetryableError wraps an error to signal that RpcQuery should not retry. Use NewNonRetryableError to wrap errors that indicate a previous attempt already had a side effect (e.g. a transaction was submitted).

func (*NonRetryableError) Error

func (e *NonRetryableError) Error() string

func (*NonRetryableError) Unwrap

func (e *NonRetryableError) Unwrap() error

type NonceProvider added in v0.1.1

type NonceProvider interface {
	Seed(ctx context.Context, client *ethclient.Client) error
	Next() (uint64, error)
}

type Option added in v0.4.1

type Option func(*GlobalRpc)

func WithLogger added in v0.4.1

func WithLogger(l *slog.Logger) Option

type RPCKind

type RPCKind int
const (
	TypeHTTPS RPCKind = iota // Starts at 0
	TypeWSS                  // 1
)

func (RPCKind) String

func (t RPCKind) String() string

type Receipt

type Receipt struct {
	Url     string
	RpcType RPCKind
	// contains filtered or unexported fields
}

type RpcConfig

type RpcConfig struct {
	ChainId int      `json:"chainId"`
	Wss     []string `json:"wss"`
	Https   []string `json:"https"`
}

type RpcErrKind added in v0.3.0

type RpcErrKind int
const (
	RpcErrUnknown    RpcErrKind = iota
	RpcErrLock                  // couldn't acquire RPC lock
	RpcErrDial                  // couldn't connect to RPC node
	RpcErrConnection            // connection dropped during call
)

func (RpcErrKind) String added in v0.3.0

func (k RpcErrKind) String() string

type RpcError added in v0.3.0

type RpcError struct {
	Kind RpcErrKind
	Err  error
}

func (*RpcError) Error added in v0.3.0

func (e *RpcError) Error() string

func (*RpcError) Unwrap added in v0.3.0

func (e *RpcError) Unwrap() error

type TxErrKind added in v0.3.0

type TxErrKind int

The error are based on and extracted from the go-ethereum v1.17.1 error definitions

const (
	TxErrUnknown TxErrKind = iota // unrecognized error

	// nonce errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go)
	TxErrNonceTooLow  // "nonce too low"
	TxErrNonceTooHigh // "nonce too high" (blob pool nonce gap)
	TxErrNonceMax     // "nonce has max value" (EIP-2681)

	// pool errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/txpool/errors.go)
	TxErrAlreadyKnown       // "already known"
	TxErrUnderpriced        // "transaction underpriced" (fee too low to enter pool)
	TxErrReplaceUnderpriced // "replacement transaction underpriced"
	TxErrAccountLimit       // "account limit exceeded"
	TxErrPoolFull           // "txpool is full" (legacypool)
	TxErrAlreadyReserved    // "address already reserved" (blob/non-blob conflict)
	TxErrInflightLimit      // "in-flight transaction limit reached for delegated accounts"

	// gas errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go)
	TxErrIntrinsicGas    // "intrinsic gas too low"
	TxErrFloorDataGas    // "insufficient gas for floor data gas cost"
	TxErrGasLimitReached // "gas limit reached" (block gas pool exhausted)
	TxErrGasLimitTooHigh // "transaction gas limit too high"
	TxErrGasUintOverflow // "gas uint64 overflow"
	TxErrGasLimit        // "exceeds block gas limit"

	// fee errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go, https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/types/transaction.go)
	TxErrFeeCapTooLow   // "max fee per gas less than block base fee" or "fee cap less than base fee"
	TxErrTipAboveFeeCap // "max priority fee per gas higher than max fee per gas"
	TxErrTipVeryHigh    // "max priority fee per gas higher than 2^256-1"
	TxErrFeeCapVeryHigh // "max fee per gas higher than 2^256-1"
	TxErrGasPriceTooLow // "transaction gas price below minimum"

	// fund errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go)
	TxErrInsufficientFunds            // "insufficient funds for gas * price + value"
	TxErrInsufficientFundsForTransfer // "insufficient funds for transfer"

	// sender/type errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go, https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/types/transaction.go)
	TxErrTxTypeNotSupported // "transaction type not supported"
	TxErrSenderNoEOA        // "sender not an eoa"
	TxErrInvalidSender      // "invalid sender"
	TxErrInvalidSig         // "invalid transaction v, r, s values"

	// size/data errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go, https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/txpool/errors.go)
	TxErrMaxInitCodeSize // "max initcode size exceeded"
	TxErrOversizedData   // "oversized data"
	TxErrNegativeValue   // "negative value"

	// EIP-4844 blob errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go, https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/txpool/errors.go)
	TxErrBlobFeeCapTooLow  // "max fee per blob gas less than block blob gas fee"
	TxErrMissingBlobHashes // "blob transaction missing blob hashes"
	TxErrTooManyBlobs      // "blob transaction has too many blobs"
	TxErrBlobTxCreate      // "blob transaction of type create"
	TxErrBlobLimitExceeded // "transaction blob limit exceeded"

	// EIP-7702 errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/error.go)
	TxErrEmptyAuthList   // "EIP-7702 transaction with empty auth list"
	TxErrSetCodeTxCreate // "EIP-7702 transaction cannot be used to create contract"

	// legacypool/ethapi errors (https://github.com/ethereum/go-ethereum/blob/v1.17.1/core/txpool/legacypool/legacypool.go, https://github.com/ethereum/go-ethereum/blob/v1.17.1/internal/ethapi/api.go)
	TxErrFutureReplacePending  // "future transaction tries to replace pending"
	TxErrOutOfOrderTxDelegated // "gapped-nonce tx from delegated accounts"
	TxErrAuthorityReserved     // "authority already reserved"
	TxErrEIP155Required        // "only replay-protected (EIP-155) transactions allowed over RPC"
	TxErrTxFeeTooHigh          // "tx fee exceeds the configured cap"
	TxErrKZGVerificationError  // "KZG verification error"
	TxErrBlobSidecarConvFailed // "blob sidecar conversion failed"
	TxErrUint256Overflow       // "bigint overflow, too large for uint256"
	TxErrUnexpectedProtection  // "transaction type does not supported EIP-155 protected signatures"
)

func ClassifyTxErr added in v0.3.0

func ClassifyTxErr(err error) TxErrKind

func (TxErrKind) String added in v0.3.0

func (k TxErrKind) String() string

type TxError added in v0.3.0

type TxError struct {
	Kind TxErrKind
	Err  error
}

func (*TxError) Error added in v0.3.0

func (e *TxError) Error() string

func (*TxError) Unwrap added in v0.3.0

func (e *TxError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL