diff --git a/backend/internal/protocols/ethereum/jsonrpc.go b/backend/internal/protocols/ethereum/jsonrpc.go index ad9237d..088e2e5 100644 --- a/backend/internal/protocols/ethereum/jsonrpc.go +++ b/backend/internal/protocols/ethereum/jsonrpc.go @@ -16,14 +16,16 @@ import ( ) const ( - rpcTimeoutMS = 30000 - rpcMaxRetries = 3 - rpcRetryBaseMS = 2000 + rpcTimeoutMS = 30000 + rpcMaxRetries = 3 + rpcRetryBaseMS = 2000 + rpcMinIntervalMS = 1000 ) type JsonRpcEthereum struct { - rpcURL string - client *http.Client + rpcURL string + client *http.Client + lastCallAt time.Time } func NewJsonRpcEthereum(rpcURL string) (*JsonRpcEthereum, error) { @@ -58,6 +60,8 @@ type rpcError struct { } func (j *JsonRpcEthereum) callRPC(ctx context.Context, method string, params ...interface{}) (json.RawMessage, error) { + j.throttle(ctx) + if params == nil { params = []interface{}{} } @@ -72,7 +76,25 @@ func (j *JsonRpcEthereum) callRPC(ctx context.Context, method string, params ... return nil, fmt.Errorf("marshal RPC request: %w", err) } - return j.callWithRetry(ctx, method, body) + result, callErr := j.callWithRetry(ctx, method, body) + j.lastCallAt = time.Now() + + return result, callErr +} + +func (j *JsonRpcEthereum) throttle(ctx context.Context) { + if j.lastCallAt.IsZero() { + return + } + minInterval := time.Duration(rpcMinIntervalMS) * time.Millisecond + elapsed := time.Since(j.lastCallAt) + if elapsed >= minInterval { + return + } + select { + case <-ctx.Done(): + case <-time.After(minInterval - elapsed): + } } // callWithRetry executes a JSON-RPC POST with exponential backoff on transient errors. diff --git a/backend/poller b/backend/poller new file mode 100755 index 0000000..6c22333 Binary files /dev/null and b/backend/poller differ