block_given 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -1
- data/README.md +92 -55
- data/lib/block_given/abi/codec/decoder.rb +111 -0
- data/lib/block_given/abi/codec.rb +170 -0
- data/lib/block_given/abi/coder.rb +122 -11
- data/lib/block_given/abi/custom_error.rb +33 -1
- data/lib/block_given/abi/event.rb +85 -3
- data/lib/block_given/abi/function.rb +94 -2
- data/lib/block_given/abi/interface.rb +99 -4
- data/lib/block_given/abi/parameter.rb +60 -3
- data/lib/block_given/abi/standards/erc1155.rb +41 -0
- data/lib/block_given/abi/standards/erc20.rb +33 -0
- data/lib/block_given/abi/standards/erc4626.rb +41 -0
- data/lib/block_given/abi/standards/erc721.rb +47 -0
- data/lib/block_given/abi/standards.rb +124 -0
- data/lib/block_given/abi/type.rb +138 -0
- data/lib/block_given/chain.rb +114 -2
- data/lib/block_given/client.rb +322 -20
- data/lib/block_given/configuration.rb +85 -3
- data/lib/block_given/connectors/alchemy.rb +38 -4
- data/lib/block_given/connectors/base.rb +32 -5
- data/lib/block_given/connectors/http.rb +91 -5
- data/lib/block_given/connectors/stub.rb +70 -4
- data/lib/block_given/contract.rb +422 -26
- data/lib/block_given/crypto/keccak.rb +152 -0
- data/lib/block_given/crypto/secp256k1.rb +168 -0
- data/lib/block_given/crypto.rb +22 -0
- data/lib/block_given/eip712.rb +199 -0
- data/lib/block_given/errors.rb +134 -14
- data/lib/block_given/event.rb +51 -1
- data/lib/block_given/normalizer.rb +27 -2
- data/lib/block_given/poller.rb +177 -15
- data/lib/block_given/receipt.rb +63 -3
- data/lib/block_given/rlp.rb +146 -0
- data/lib/block_given/signed_transaction.rb +148 -47
- data/lib/block_given/transaction.rb +103 -12
- data/lib/block_given/transaction_envelope/fields.rb +104 -0
- data/lib/block_given/transaction_envelope.rb +183 -0
- data/lib/block_given/utils.rb +143 -9
- data/lib/block_given/version.rb +2 -1
- data/lib/block_given/wallet.rb +216 -35
- data/lib/block_given.rb +67 -4
- metadata +19 -23
|
@@ -2,13 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
4
|
module Connectors
|
|
5
|
-
# Alchemy JSON-RPC connector
|
|
6
|
-
# connector instance can serve any Alchemy-supported network.
|
|
5
|
+
# Alchemy JSON-RPC connector, an {Http} transport whose endpoint is derived from the chain.
|
|
7
6
|
#
|
|
8
|
-
#
|
|
7
|
+
# The URL is `https://<chain.alchemy_network>.g.alchemy.com/v2/<api_key>`, so a single instance can
|
|
8
|
+
# serve every Alchemy-supported network declared in {Chains} (`alchemy_network` set). Retries, backoff,
|
|
9
|
+
# timeouts and headers behave exactly as in {Http}.
|
|
10
|
+
#
|
|
11
|
+
# The API key never leaks: {#inspect} shows only its first 4 characters and {#redact} strips it from any
|
|
12
|
+
# endpoint that ends up in a log line or an error message.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# connector = BlockGiven::Connectors::Alchemy.new(api_key: ENV["ALCHEMY_API_KEY"])
|
|
16
|
+
# BlockGiven::Client.new(chain: :base, connector: connector).block_number
|
|
17
|
+
# connector.inspect # => "#<BlockGiven::Connectors::Alchemy api_key=abcd…>"
|
|
9
18
|
class Alchemy < Http
|
|
19
|
+
# @!attribute [r] api_key
|
|
20
|
+
# @return [String] the Alchemy API key (as a String); do not log it, use {#inspect} or {#redact}
|
|
21
|
+
|
|
10
22
|
attr_reader :api_key
|
|
11
23
|
|
|
24
|
+
# Builds an Alchemy transport. There is no `url:` argument: the endpoint is computed per chain.
|
|
25
|
+
#
|
|
26
|
+
# @param api_key [String, #to_s] Alchemy API key
|
|
27
|
+
# @param timeout [Numeric] seconds for the open, read and write timeouts of each HTTP request
|
|
28
|
+
# @param retries [Integer] number of retries after the first failed attempt
|
|
29
|
+
# @param retry_delay [Numeric] seconds slept before the first retry; doubled on each further retry
|
|
30
|
+
# @param logger [Logger, nil] receives retry notices at debug level. Defaults to `BlockGiven.config.logger`.
|
|
31
|
+
# @param headers [Hash{String => String}] extra headers merged over the {Http} defaults
|
|
32
|
+
# @raise [ConfigurationError] when `api_key` is nil or empty
|
|
12
33
|
def initialize(api_key:, timeout: 30, retries: 3, retry_delay: 0.5, logger: nil, headers: {})
|
|
13
34
|
raise ConfigurationError, "Alchemy api_key is required" if api_key.nil? || api_key.to_s.empty?
|
|
14
35
|
|
|
@@ -16,6 +37,11 @@ module BlockGiven
|
|
|
16
37
|
super(url: nil, headers: headers, timeout: timeout, retries: retries, retry_delay: retry_delay, logger: logger)
|
|
17
38
|
end
|
|
18
39
|
|
|
40
|
+
# Alchemy endpoint for a chain: `https://<alchemy_network>.g.alchemy.com/v2/<api_key>`.
|
|
41
|
+
#
|
|
42
|
+
# @param chain [Chain] chain being queried; its `alchemy_network` (e.g. `"base-mainnet"`) selects the host
|
|
43
|
+
# @return [String] the full endpoint URL, including the API key
|
|
44
|
+
# @raise [ConfigurationError] when `chain` is nil or has no `alchemy_network`
|
|
19
45
|
def endpoint(chain)
|
|
20
46
|
raise ConfigurationError, "Alchemy connector needs a chain" if chain.nil?
|
|
21
47
|
raise ConfigurationError, "#{chain.name} is not available on Alchemy" unless chain.alchemy_network
|
|
@@ -23,9 +49,17 @@ module BlockGiven
|
|
|
23
49
|
"https://#{chain.alchemy_network}.g.alchemy.com/v2/#{api_key}"
|
|
24
50
|
end
|
|
25
51
|
|
|
52
|
+
# Short description with the API key masked (first 4 characters followed by an ellipsis).
|
|
53
|
+
#
|
|
54
|
+
# @return [String] e.g. `"#<BlockGiven::Connectors::Alchemy api_key=abcd…>"`
|
|
26
55
|
def inspect = "#<BlockGiven::Connectors::Alchemy api_key=#{redacted_key}>"
|
|
27
56
|
|
|
28
|
-
#
|
|
57
|
+
# Masks the API key inside an endpoint while keeping the network host visible.
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# connector.redact(connector.endpoint(chain)) # => "https://base-mainnet.g.alchemy.com/v2/abcd…"
|
|
61
|
+
# @param endpoint [String, #to_s] URL possibly containing the API key
|
|
62
|
+
# @return [String] the URL with every occurrence of the key replaced by its masked form
|
|
29
63
|
def redact(endpoint) = endpoint.to_s.sub(api_key, redacted_key)
|
|
30
64
|
|
|
31
65
|
private
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
|
+
# JSON-RPC transports: {Connectors::Base} defines the interface, {Connectors::Http} speaks JSON-RPC over HTTP(S),
|
|
5
|
+
# {Connectors::Alchemy} derives Alchemy endpoints from the chain and {Connectors::Stub} serves canned responses
|
|
6
|
+
# in tests. A connector is set globally with `BlockGiven.config.connector` or passed to {Client#initialize}.
|
|
4
7
|
module Connectors
|
|
5
|
-
#
|
|
6
|
-
#
|
|
8
|
+
# Abstract JSON-RPC transport every connector derives from.
|
|
9
|
+
#
|
|
10
|
+
# A connector only knows how to send a JSON-RPC method with its params and hand back the `result`.
|
|
11
|
+
# The {Chain} is passed on each call so that multi-network providers ({Alchemy}) can derive the endpoint
|
|
12
|
+
# from it; single-endpoint transports ({Http} with an explicit `url:`) may ignore it. Concrete
|
|
13
|
+
# connectors: {Http}, {Alchemy} and, for tests, {Stub}.
|
|
14
|
+
#
|
|
15
|
+
# @abstract Subclass and override {#request}; override {#batch} when the transport supports real batching.
|
|
7
16
|
class Base
|
|
8
|
-
#
|
|
17
|
+
# Sends one JSON-RPC request and returns its `result`.
|
|
18
|
+
#
|
|
19
|
+
# @abstract
|
|
20
|
+
# @param method [String] JSON-RPC method name, e.g. `"eth_blockNumber"`
|
|
21
|
+
# @param params [Array<Object>] positional JSON-RPC params
|
|
22
|
+
# @param chain [Chain, nil] chain being queried, used by multi-network transports to pick an endpoint
|
|
23
|
+
# @return [Object] the raw JSON-RPC `result` (String, Hash, Array, nil...), undecoded
|
|
24
|
+
# @raise [RpcError] when the node answers with a JSON-RPC error object
|
|
25
|
+
# @raise [NotImplementedError] when called on {Base} itself
|
|
9
26
|
def request(method, params = [], chain: nil)
|
|
10
27
|
raise NotImplementedError, "#{self.class}#request"
|
|
11
28
|
end
|
|
12
29
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
30
|
+
# Executes several calls and returns their results in order.
|
|
31
|
+
#
|
|
32
|
+
# This naive implementation issues one {#request} per call; transports with real JSON-RPC batching
|
|
33
|
+
# override it. A call that fails with an {RpcError} does not abort the batch: the error instance takes
|
|
34
|
+
# the place of its result.
|
|
35
|
+
#
|
|
36
|
+
# @param calls [Array<Array(String, Array)>] `[method, params]` pairs; a missing params entry means `[]`
|
|
37
|
+
# @param chain [Chain, nil] chain being queried, forwarded to {#request}
|
|
38
|
+
# @return [Array<Object, RpcError>] one raw result (or {RpcError}) per call, in the same order
|
|
15
39
|
def batch(calls, chain: nil)
|
|
16
40
|
calls.map do |(method, params)|
|
|
17
41
|
request(method, params || [], chain: chain)
|
|
@@ -20,6 +44,9 @@ module BlockGiven
|
|
|
20
44
|
end
|
|
21
45
|
end
|
|
22
46
|
|
|
47
|
+
# Short lowercase name of the transport, derived from the class name.
|
|
48
|
+
#
|
|
49
|
+
# @return [String] e.g. `"http"`, `"alchemy"` or `"stub"`
|
|
23
50
|
def name = self.class.name.split("::").last.downcase
|
|
24
51
|
end
|
|
25
52
|
end
|
|
@@ -6,18 +6,55 @@ require "uri"
|
|
|
6
6
|
|
|
7
7
|
module BlockGiven
|
|
8
8
|
module Connectors
|
|
9
|
-
# Generic JSON-RPC over HTTP(S) transport with retries and exponential backoff.
|
|
9
|
+
# Generic JSON-RPC 2.0 over HTTP(S) transport built on Net::HTTP, with retries and exponential backoff.
|
|
10
10
|
#
|
|
11
|
+
# Each request is a POST of a JSON body to the endpoint. Transient failures are retried up to `retries`
|
|
12
|
+
# times, sleeping `retry_delay * 2**(attempt - 1)` seconds between attempts: HTTP statuses listed in
|
|
13
|
+
# {RETRIABLE_STATUSES}, JSON-RPC error codes listed in {RETRIABLE_RPC_CODES} (never a
|
|
14
|
+
# {ContractRevertError}) and the network exceptions listed in {RETRIABLE_EXCEPTIONS}. Anything else is
|
|
15
|
+
# raised immediately as {HttpError} or {RpcError}.
|
|
16
|
+
#
|
|
17
|
+
# Endpoints frequently embed an API key in their path, so {#inspect}, log lines and error messages only
|
|
18
|
+
# ever show the redacted form produced by {#redact} (scheme and host, never the path).
|
|
19
|
+
#
|
|
20
|
+
# @example Explicit endpoint (local node, Infura, QuickNode...)
|
|
11
21
|
# BlockGiven::Connectors::Http.new(url: "http://127.0.0.1:8545")
|
|
12
|
-
# BlockGiven::Connectors::Http.new
|
|
22
|
+
# BlockGiven::Connectors::Http.new(url: "https://mainnet.infura.io/v3/KEY", timeout: 10, retries: 5)
|
|
23
|
+
# @example Public RPC of the chain
|
|
24
|
+
# BlockGiven::Connectors::Http.new # -> uses chain.rpc_urls.first of the chain passed on each request
|
|
13
25
|
class Http < Base
|
|
26
|
+
# HTTP status codes that trigger a retry (timeouts, rate limiting, server and gateway errors).
|
|
14
27
|
RETRIABLE_STATUSES = [408, 425, 429, 500, 502, 503, 504].freeze
|
|
28
|
+
# JSON-RPC error codes that trigger a retry: provider rate limit (-32005), internal error (-32603), 429.
|
|
15
29
|
RETRIABLE_RPC_CODES = [-32_005, -32_603, 429].freeze # rate limited / internal error
|
|
30
|
+
# Network-level exceptions that trigger a retry; they surface as {HttpError} once retries are exhausted.
|
|
16
31
|
RETRIABLE_EXCEPTIONS = [Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, Errno::ECONNREFUSED,
|
|
17
32
|
Errno::EHOSTUNREACH, EOFError, SocketError, OpenSSL::SSL::SSLError].freeze
|
|
18
33
|
|
|
34
|
+
# @!attribute [r] url
|
|
35
|
+
# @return [String, nil] fixed endpoint, or nil when the endpoint comes from the chain's `rpc_urls`
|
|
36
|
+
# @!attribute [r] headers
|
|
37
|
+
# @return [Hash{String => String}] request headers sent with every POST (`Content-Type`, `User-Agent`
|
|
38
|
+
# and the custom ones given to the constructor)
|
|
39
|
+
# @!attribute [r] timeout
|
|
40
|
+
# @return [Numeric] seconds applied to the open, read and write timeouts of each HTTP request
|
|
41
|
+
# @!attribute [r] retries
|
|
42
|
+
# @return [Integer] maximum number of retries after the first attempt
|
|
43
|
+
# @!attribute [r] retry_delay
|
|
44
|
+
# @return [Numeric] base delay in seconds before the first retry; doubled on each further retry
|
|
45
|
+
|
|
19
46
|
attr_reader :url, :headers, :timeout, :retries, :retry_delay
|
|
20
47
|
|
|
48
|
+
# Builds an HTTP transport.
|
|
49
|
+
#
|
|
50
|
+
# @param url [String, nil] JSON-RPC endpoint. When nil, {#endpoint} falls back to the first `rpc_urls`
|
|
51
|
+
# entry of the chain given on each request.
|
|
52
|
+
# @param headers [Hash{String => String}] extra headers merged over the defaults
|
|
53
|
+
# (`Content-Type: application/json`, `User-Agent: block_given/<version>`); may override them
|
|
54
|
+
# @param timeout [Numeric] seconds for the open, read and write timeouts of each HTTP request
|
|
55
|
+
# @param retries [Integer] number of retries after the first failed attempt (0 disables retrying)
|
|
56
|
+
# @param retry_delay [Numeric] seconds slept before the first retry; each further retry doubles it
|
|
57
|
+
# @param logger [Logger, nil] receives retry notices at debug level. Defaults to `BlockGiven.config.logger`.
|
|
21
58
|
def initialize(url: nil, headers: {}, timeout: 30, retries: 3, retry_delay: 0.5, logger: nil)
|
|
22
59
|
super()
|
|
23
60
|
@url = url
|
|
@@ -31,6 +68,11 @@ module BlockGiven
|
|
|
31
68
|
@mutex = Mutex.new
|
|
32
69
|
end
|
|
33
70
|
|
|
71
|
+
# Resolves the endpoint used for a request: the fixed `url`, or the chain's first public RPC url.
|
|
72
|
+
#
|
|
73
|
+
# @param chain [Chain, nil] chain being queried
|
|
74
|
+
# @return [String] the endpoint URL (may contain credentials: pass it through {#redact} before logging)
|
|
75
|
+
# @raise [ConfigurationError] when no `url` was given and the chain has no `rpc_urls`
|
|
34
76
|
def endpoint(chain)
|
|
35
77
|
return url if url
|
|
36
78
|
|
|
@@ -38,12 +80,38 @@ module BlockGiven
|
|
|
38
80
|
raise(ConfigurationError, "no RPC url: pass url: to the connector or use a chain with rpc_urls")
|
|
39
81
|
end
|
|
40
82
|
|
|
83
|
+
# Sends one JSON-RPC 2.0 request (with an auto-incremented id) and returns its `result`.
|
|
84
|
+
#
|
|
85
|
+
# Retriable failures are retried with exponential backoff, see the class description.
|
|
86
|
+
#
|
|
87
|
+
# @param method [String] JSON-RPC method name
|
|
88
|
+
# @param params [Array<Object>] positional JSON-RPC params
|
|
89
|
+
# @param chain [Chain, nil] chain being queried, used to resolve the endpoint when no `url` is fixed
|
|
90
|
+
# @return [Object] the raw JSON-RPC `result`
|
|
91
|
+
# @raise [ContractRevertError] when the node reports an EVM revert (never retried)
|
|
92
|
+
# @raise [RpcError] when the node answers with a JSON-RPC error, or the body is not a JSON object;
|
|
93
|
+
# retriable codes are raised only once retries are exhausted
|
|
94
|
+
# @raise [HttpError] on a non-2xx status, an invalid JSON body or a network error (after retries)
|
|
95
|
+
# @raise [ConfigurationError] when no endpoint can be resolved
|
|
41
96
|
def request(method, params = [], chain: nil)
|
|
42
97
|
payload = { jsonrpc: "2.0", id: next_id, method: method, params: params }
|
|
43
98
|
body = with_retries(method) { post(endpoint(chain), payload) }
|
|
44
99
|
handle_single(body, method)
|
|
45
100
|
end
|
|
46
101
|
|
|
102
|
+
# Sends several calls in one JSON-RPC batch request (a single HTTP POST with an array body).
|
|
103
|
+
#
|
|
104
|
+
# Responses are matched to calls by id, so the node may answer in any order. A call whose response
|
|
105
|
+
# carries an `error` (or is missing from the response) yields an {RpcError} instance in its slot instead
|
|
106
|
+
# of raising. Only transport-level failures of the whole batch are retried.
|
|
107
|
+
#
|
|
108
|
+
# @param calls [Array<Array(String, Array)>] `[method, params]` pairs; a missing params entry means `[]`
|
|
109
|
+
# @param chain [Chain, nil] chain being queried, used to resolve the endpoint when no `url` is fixed
|
|
110
|
+
# @return [Array<Object, RpcError>] one raw result (or {RpcError}) per call, in call order; `[]` for
|
|
111
|
+
# an empty `calls` (no request is sent)
|
|
112
|
+
# @raise [RpcError] when the node does not answer with a JSON array
|
|
113
|
+
# @raise [HttpError] on a non-2xx status, an invalid JSON body or a network error (after retries)
|
|
114
|
+
# @raise [ConfigurationError] when no endpoint can be resolved
|
|
47
115
|
def batch(calls, chain: nil)
|
|
48
116
|
return [] if calls.empty?
|
|
49
117
|
|
|
@@ -62,10 +130,21 @@ module BlockGiven
|
|
|
62
130
|
end
|
|
63
131
|
end
|
|
64
132
|
|
|
133
|
+
# Short description showing the redacted endpoint (or `chain default`), never the full URL.
|
|
134
|
+
#
|
|
135
|
+
# @return [String]
|
|
65
136
|
def inspect = "#<#{self.class.name} url=#{url ? redact(url).inspect : 'chain default'}>"
|
|
66
137
|
|
|
67
|
-
# Endpoint as it may appear in logs and error messages.
|
|
68
|
-
#
|
|
138
|
+
# Endpoint as it may appear in logs and error messages.
|
|
139
|
+
#
|
|
140
|
+
# RPC URLs usually carry the API key in their path (Infura, QuickNode, ...), so only the scheme and the
|
|
141
|
+
# host (plus a non-default port) are kept; a non-empty path is replaced by `/…`.
|
|
142
|
+
#
|
|
143
|
+
# @example
|
|
144
|
+
# connector.redact("https://mainnet.infura.io/v3/SECRET") # => "https://mainnet.infura.io/…"
|
|
145
|
+
# connector.redact("http://127.0.0.1:8545") # => "http://127.0.0.1:8545"
|
|
146
|
+
# @param endpoint [String, URI::Generic] URL to redact
|
|
147
|
+
# @return [String] the redacted URL, or `"<invalid url>"` when it cannot be parsed
|
|
69
148
|
def redact(endpoint)
|
|
70
149
|
uri = URI.parse(endpoint.to_s)
|
|
71
150
|
host = uri.port && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
|
|
@@ -95,10 +174,17 @@ module BlockGiven
|
|
|
95
174
|
body["result"]
|
|
96
175
|
end
|
|
97
176
|
|
|
98
|
-
# Internal signal
|
|
177
|
+
# Internal signal raised by {Http#handle_single} to retry on a retriable JSON-RPC error; the original
|
|
178
|
+
# {RpcError} is re-raised once retries are exhausted.
|
|
179
|
+
#
|
|
180
|
+
# @api private
|
|
99
181
|
class Retry < StandardError
|
|
182
|
+
# The retriable JSON-RPC error that triggered the retry.
|
|
183
|
+
#
|
|
184
|
+
# @return [RpcError]
|
|
100
185
|
attr_reader :cause_error
|
|
101
186
|
|
|
187
|
+
# @param cause_error [RpcError] the retriable JSON-RPC error; its message becomes this error's message
|
|
102
188
|
def initialize(cause_error)
|
|
103
189
|
@cause_error = cause_error
|
|
104
190
|
super(cause_error.message)
|
|
@@ -2,29 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
4
|
module Connectors
|
|
5
|
-
# In-memory connector for tests
|
|
6
|
-
# (consumed in order, last value repeats) or procs receiving the params.
|
|
5
|
+
# In-memory connector for tests: no network, canned responses, and a record of every call.
|
|
7
6
|
#
|
|
7
|
+
# Responses are registered per JSON-RPC method name and can be:
|
|
8
|
+
# - a static value, returned on every call;
|
|
9
|
+
# - a {Sequence} built with {Stub.sequence}, consumed in order, the last value repeating forever;
|
|
10
|
+
# - a Proc receiving the request params, for param-dependent answers;
|
|
11
|
+
# - an Exception instance, raised instead of returned (e.g. an {RpcError} to simulate a node error).
|
|
12
|
+
# A handler block given to {Stub#initialize} answers every method without a registered response. Methods with
|
|
13
|
+
# neither raise an {RpcError} with code -32601 ("method not found").
|
|
14
|
+
#
|
|
15
|
+
# @example Static values, a sequence and a proc
|
|
8
16
|
# stub = BlockGiven::Connectors::Stub.new(
|
|
17
|
+
# "eth_chainId" => "0x2105",
|
|
9
18
|
# "eth_blockNumber" => BlockGiven::Connectors::Stub.sequence("0x10", "0x11"),
|
|
10
|
-
# "eth_call" => ->(params) { "0x" + "0" * 63 + "1" }
|
|
19
|
+
# "eth_call" => ->(params) { params.first[:to] == usdc ? "0x" + "0" * 63 + "1" : "0x" }
|
|
11
20
|
# )
|
|
12
|
-
#
|
|
21
|
+
# client = BlockGiven::Client.new(chain: :base, connector: stub)
|
|
22
|
+
# client.block_number # => 16
|
|
23
|
+
# client.block_number # => 17 (and 17 again afterwards)
|
|
24
|
+
# stub.calls # => [["eth_blockNumber", []], ["eth_blockNumber", []]]
|
|
25
|
+
# stub.calls_for("eth_call") # => params of each eth_call
|
|
26
|
+
# @example Fallback handler and incremental stubbing
|
|
27
|
+
# stub = BlockGiven::Connectors::Stub.new { |method, params, chain| raise "unexpected #{method}" }
|
|
28
|
+
# stub.stub("eth_gasPrice", "0x3b9aca00").stub("eth_estimateGas") { |params| "0x5208" }
|
|
13
29
|
class Stub < Base
|
|
30
|
+
# Ordered list of canned responses: each call consumes the next value, the last one repeats forever.
|
|
31
|
+
# Build one with {Stub.sequence}.
|
|
14
32
|
class Sequence
|
|
33
|
+
# @param values [Array<Object>] responses in the order they should be served (copied, not mutated)
|
|
15
34
|
def initialize(values)
|
|
16
35
|
@values = values.dup
|
|
17
36
|
end
|
|
18
37
|
|
|
38
|
+
# Consumes and returns the next value; once a single value is left it is returned on every call.
|
|
39
|
+
#
|
|
40
|
+
# @return [Object, nil] the next response (nil for an empty sequence)
|
|
19
41
|
def next
|
|
20
42
|
@values.size > 1 ? @values.shift : @values.first
|
|
21
43
|
end
|
|
22
44
|
end
|
|
23
45
|
|
|
46
|
+
# Builds a {Sequence} of responses for a stubbed method.
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# stub.stub("eth_blockNumber", BlockGiven::Connectors::Stub.sequence("0x10", "0x11", "0x12"))
|
|
50
|
+
# @param values [Array<Object>] responses served in order; the last one repeats once reached
|
|
51
|
+
# @return [Sequence]
|
|
24
52
|
def self.sequence(*values) = Sequence.new(values)
|
|
25
53
|
|
|
54
|
+
# @!attribute [r] calls
|
|
55
|
+
# @return [Array<Array(String, Array)>] every request received so far as `[method, params]` pairs, in
|
|
56
|
+
# order, including those that raised; cleared by {#reset!}
|
|
57
|
+
|
|
26
58
|
attr_reader :calls
|
|
27
59
|
|
|
60
|
+
# Builds a stub connector with an initial set of canned responses and an optional fallback handler.
|
|
61
|
+
#
|
|
62
|
+
# @param responses [Hash{String, Symbol => Object}] method name to response (static value, {Sequence},
|
|
63
|
+
# Proc receiving the params, or Exception to raise); each entry goes through {#stub}
|
|
64
|
+
# @yield [method, params, chain] for every request whose method has no registered response
|
|
65
|
+
# @yieldparam method [String] JSON-RPC method name
|
|
66
|
+
# @yieldparam params [Array<Object>] request params
|
|
67
|
+
# @yieldparam chain [Chain, nil] chain passed by the client
|
|
68
|
+
# @yieldreturn [Object] the value to use as the JSON-RPC `result` (raised when it is an Exception)
|
|
28
69
|
def initialize(responses = {}, &handler)
|
|
29
70
|
super()
|
|
30
71
|
@responses = {}
|
|
@@ -33,11 +74,28 @@ module BlockGiven
|
|
|
33
74
|
responses.each { |method, value| stub(method, value) }
|
|
34
75
|
end
|
|
35
76
|
|
|
77
|
+
# Registers (or replaces) the response of one JSON-RPC method.
|
|
78
|
+
#
|
|
79
|
+
# @param method [String, Symbol] JSON-RPC method name
|
|
80
|
+
# @param value [Object, Sequence, Proc, Exception, nil] static value, {Sequence}, Proc receiving the params,
|
|
81
|
+
# or Exception instance to raise; ignored when a block is given
|
|
82
|
+
# @yield [params] when a block is given it computes the response on every call
|
|
83
|
+
# @yieldparam params [Array<Object>] request params
|
|
84
|
+
# @yieldreturn [Object] the value to use as the JSON-RPC `result` (raised when it is an Exception)
|
|
85
|
+
# @return [self] for chaining
|
|
36
86
|
def stub(method, value = nil, &block)
|
|
37
87
|
@responses[method.to_s] = block || value
|
|
38
88
|
self
|
|
39
89
|
end
|
|
40
90
|
|
|
91
|
+
# Records the call and returns the canned response for `method`.
|
|
92
|
+
#
|
|
93
|
+
# @param method [String, Symbol] JSON-RPC method name
|
|
94
|
+
# @param params [Array<Object>] request params, recorded and passed to Proc responses / the handler
|
|
95
|
+
# @param chain [Chain, nil] chain passed by the client, forwarded to the handler block only
|
|
96
|
+
# @return [Object] the resolved response (a {Sequence} is advanced, a Proc is called with `params`)
|
|
97
|
+
# @raise [Exception] the resolved response itself when it is an Exception instance
|
|
98
|
+
# @raise [RpcError] with code -32601 when the method has no response and no handler was given
|
|
41
99
|
def request(method, params = [], chain: nil)
|
|
42
100
|
@calls << [method.to_s, params]
|
|
43
101
|
value = resolve(method.to_s, params, chain)
|
|
@@ -46,7 +104,15 @@ module BlockGiven
|
|
|
46
104
|
value
|
|
47
105
|
end
|
|
48
106
|
|
|
107
|
+
# Params of every recorded call to one method, in order.
|
|
108
|
+
#
|
|
109
|
+
# @param method [String, Symbol] JSON-RPC method name
|
|
110
|
+
# @return [Array<Array<Object>>] one params Array per call (empty when the method was never called)
|
|
49
111
|
def calls_for(method) = calls.select { |(m, _)| m == method.to_s }.map(&:last)
|
|
112
|
+
|
|
113
|
+
# Forgets every recorded call; registered responses (and sequence positions) are kept.
|
|
114
|
+
#
|
|
115
|
+
# @return [Array] the emptied {#calls} Array
|
|
50
116
|
def reset! = @calls.clear
|
|
51
117
|
|
|
52
118
|
private
|