erpc-sdk 0.9.1 → 0.11.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.
@@ -7,6 +7,39 @@ module ERPC
7
7
  DEFAULT_AVALANCHE_ENDPOINT = "https://ava-rpc.erpc.global"
8
8
  DEFAULT_ACCOUNT_ENDPOINT = "https://solana-rpc.erpc.global"
9
9
  DEFAULT_USER_ENDPOINT = "https://user-api.erpc.global"
10
+
11
+ def self.generated_public_default(client_contract_id, bootstrap_fallback)
12
+ return bootstrap_fallback unless defined?(RPC_NETWORK_CONTRACT)
13
+
14
+ client = RPC_NETWORK_CONTRACT.fetch("clientContracts").find do |candidate|
15
+ candidate.is_a?(Hash) && candidate["clientContractId"] == client_contract_id
16
+ end
17
+ hosted = client&.fetch("endpointModes", []).find do |candidate|
18
+ candidate.is_a?(Hash) && candidate["mode"] == "hosted"
19
+ end
20
+ value = hosted&.fetch("publicDefault")
21
+ raise ConfigError, "generated hosted endpoint is unavailable", cause: nil unless value.is_a?(String) && !value.empty?
22
+
23
+ value
24
+ rescue KeyError, NoMethodError, TypeError
25
+ raise ConfigError, "generated hosted endpoint is unavailable", cause: nil
26
+ end
27
+ private_class_method :generated_public_default
28
+
29
+ DEFAULT_BASE_ENDPOINT = generated_public_default("client-base-rpc", "https://base.erpc.global")
30
+ DEFAULT_ARBITRUM_ENDPOINT = generated_public_default("client-arbitrum-rpc", "https://arbitrum.erpc.global")
31
+ DEFAULT_POLYGON_ENDPOINT = generated_public_default("client-polygon-rpc", "https://polygon.erpc.global")
32
+ DEFAULT_BSC_ENDPOINT = generated_public_default("client-bsc-rpc", "https://bsc.erpc.global")
33
+ DEFAULT_OPTIMISM_ENDPOINT = generated_public_default("client-optimism-rpc", "https://optimism.erpc.global")
34
+ DEFAULT_KAIA_ENDPOINT = generated_public_default("client-kaia-rpc", "https://kaia.erpc.global")
35
+ DEFAULT_SONEIUM_ENDPOINT = generated_public_default("client-soneium-rpc", "https://soneium.erpc.global")
36
+ DEFAULT_MONAD_ENDPOINT = generated_public_default("client-monad-rpc", "https://monad.erpc.global")
37
+ DEFAULT_HYPERLIQUID_ENDPOINT = generated_public_default("client-hyperliquid-rpc", "https://hyperliquid.erpc.global")
38
+ DEFAULT_ARC_ENDPOINT = generated_public_default("client-arc-rpc", "https://arc.erpc.global")
39
+ DEFAULT_ROBINHOOD_ENDPOINT = generated_public_default("client-robinhood-rpc", "https://robinhood.erpc.global")
40
+ DEFAULT_TRON_ENDPOINT = generated_public_default("client-tron-rpc", "https://tron.erpc.global")
41
+ DEFAULT_NEAR_ENDPOINT = generated_public_default("client-near-rpc", "https://near.erpc.global")
42
+ DEFAULT_SUI_ENDPOINT = generated_public_default("client-sui-grpc", "https://sui.erpc.global")
10
43
  DEFAULT_TIMEOUT = 30.0
11
44
 
12
45
  module URLs
@@ -59,6 +92,34 @@ module ERPC
59
92
  raise ConfigError, "endpoint must be an absolute HTTP(S) URL", cause: nil
60
93
  end
61
94
 
95
+ # New sealed-client hosted roots reject credentials and fragments before
96
+ # any diagnostic rendering. Query parameters are not part of a hosted
97
+ # root; endpoint-specific authentication is applied by its private
98
+ # transport.
99
+ def normalize_new_hosted_endpoint(value, namespace:)
100
+ unless value.is_a?(String)
101
+ raise ConfigError, "#{namespace} must be an absolute HTTP(S) URL", cause: nil
102
+ end
103
+
104
+ input = value.strip
105
+ begin
106
+ uri = URI.parse(input)
107
+ valid = uri.absolute? && uri.host && %w[http https].include?(uri.scheme) &&
108
+ !input.include?("#") && !input.match?(%r{://[^/?#]*@})
109
+ rescue URI::InvalidURIError, ArgumentError
110
+ valid = false
111
+ end
112
+ unless valid
113
+ raise ConfigError, "#{namespace} must be an absolute HTTP(S) URL", cause: nil
114
+ end
115
+
116
+ uri.query = nil
117
+ uri.fragment = nil
118
+ uri.path = uri.path.sub(%r{/+\z}, "")
119
+ uri.path = "/" if uri.path.empty?
120
+ uri.to_s.freeze
121
+ end
122
+
62
123
  def public_endpoint(endpoint)
63
124
  uri = endpoint.is_a?(URI::Generic) ? endpoint.dup : URI.parse(endpoint.to_s)
64
125
  uri.query = nil
@@ -137,14 +198,39 @@ module ERPC
137
198
 
138
199
  class ClientConfig
139
200
  attr_reader :api_key, :endpoint, :account_endpoint, :user_endpoint, :headers, :timeout,
140
- :avalanche_endpoint, :solana_rpc, :ethereum_rpc, :avalanche_c_rpc
201
+ :avalanche_endpoint, :solana_rpc, :ethereum_rpc, :avalanche_c_rpc,
202
+ :base_endpoint, :base_rpc, :arbitrum_endpoint, :arbitrum_rpc, :bitcoin_rpc,
203
+ :polygon_endpoint, :polygon_rpc, :bsc_endpoint, :bsc_rpc,
204
+ :optimism_endpoint, :optimism_rpc, :kaia_endpoint, :kaia_rpc,
205
+ :soneium_endpoint, :soneium_rpc, :monad_endpoint, :monad_rpc,
206
+ :hyperliquid_endpoint, :hyperliquid_rpc, :arc_endpoint, :arc_rpc,
207
+ :robinhood_endpoint, :robinhood_rpc, :tron_endpoint, :tron_rpc,
208
+ :near_endpoint, :near_rpc, :sui_endpoint, :sui_rpc, :sui_grpc_web
141
209
 
142
210
  def initialize(api_key: nil, endpoint: DEFAULT_ENDPOINT, account_endpoint: DEFAULT_ACCOUNT_ENDPOINT,
143
211
  user_endpoint: DEFAULT_USER_ENDPOINT, headers: {}, timeout: DEFAULT_TIMEOUT,
144
212
  avalanche_endpoint: DEFAULT_AVALANCHE_ENDPOINT, solana_rpc: nil,
145
- ethereum_rpc: nil, avalanche_c_rpc: nil)
146
- direct_endpoints = [solana_rpc, ethereum_rpc, avalanche_c_rpc]
147
- unless direct_endpoints.all? { |value| value.nil? || value.is_a?(RpcEndpointConfig) }
213
+ ethereum_rpc: nil, avalanche_c_rpc: nil,
214
+ base_endpoint: DEFAULT_BASE_ENDPOINT, base_rpc: nil,
215
+ arbitrum_endpoint: DEFAULT_ARBITRUM_ENDPOINT, arbitrum_rpc: nil,
216
+ bitcoin_rpc: nil,
217
+ polygon_endpoint: DEFAULT_POLYGON_ENDPOINT, polygon_rpc: nil,
218
+ bsc_endpoint: DEFAULT_BSC_ENDPOINT, bsc_rpc: nil,
219
+ optimism_endpoint: DEFAULT_OPTIMISM_ENDPOINT, optimism_rpc: nil,
220
+ kaia_endpoint: DEFAULT_KAIA_ENDPOINT, kaia_rpc: nil,
221
+ soneium_endpoint: DEFAULT_SONEIUM_ENDPOINT, soneium_rpc: nil,
222
+ monad_endpoint: DEFAULT_MONAD_ENDPOINT, monad_rpc: nil,
223
+ hyperliquid_endpoint: DEFAULT_HYPERLIQUID_ENDPOINT, hyperliquid_rpc: nil,
224
+ arc_endpoint: DEFAULT_ARC_ENDPOINT, arc_rpc: nil,
225
+ robinhood_endpoint: DEFAULT_ROBINHOOD_ENDPOINT, robinhood_rpc: nil,
226
+ tron_endpoint: DEFAULT_TRON_ENDPOINT, tron_rpc: nil,
227
+ near_endpoint: DEFAULT_NEAR_ENDPOINT, near_rpc: nil,
228
+ sui_endpoint: DEFAULT_SUI_ENDPOINT, sui_rpc: nil, sui_grpc_web: nil)
229
+ direct_endpoints = [solana_rpc, ethereum_rpc, avalanche_c_rpc, base_rpc, arbitrum_rpc, bitcoin_rpc,
230
+ polygon_rpc, bsc_rpc, optimism_rpc, kaia_rpc, soneium_rpc, monad_rpc,
231
+ hyperliquid_rpc, arc_rpc, robinhood_rpc, tron_rpc, near_rpc, sui_rpc, sui_grpc_web]
232
+ allowed_sui = defined?(SuiGrpcWebEndpointConfig) ? SuiGrpcWebEndpointConfig : Object
233
+ unless direct_endpoints.all? { |value| value.nil? || value.is_a?(RpcEndpointConfig) || value.is_a?(allowed_sui) }
148
234
  raise ConfigError, "direct RPC overrides must use RpcEndpointConfig"
149
235
  end
150
236
 
@@ -163,11 +249,57 @@ module ERPC
163
249
  @avalanche_endpoint = URLs.normalize_endpoint(avalanche_endpoint)
164
250
  @account_endpoint = URLs.normalize_endpoint(account_endpoint)
165
251
  @user_endpoint = URLs.normalize_endpoint(user_endpoint)
252
+ @base_endpoint = URLs.normalize_new_hosted_endpoint(base_endpoint, namespace: "base_endpoint")
253
+ @arbitrum_endpoint = URLs.normalize_new_hosted_endpoint(arbitrum_endpoint, namespace: "arbitrum_endpoint")
254
+ @polygon_endpoint = URLs.normalize_new_hosted_endpoint(polygon_endpoint, namespace: "polygon_endpoint")
255
+ @bsc_endpoint = URLs.normalize_new_hosted_endpoint(bsc_endpoint, namespace: "bsc_endpoint")
256
+ @optimism_endpoint = URLs.normalize_new_hosted_endpoint(optimism_endpoint, namespace: "optimism_endpoint")
257
+ @kaia_endpoint = URLs.normalize_new_hosted_endpoint(kaia_endpoint, namespace: "kaia_endpoint")
258
+ @soneium_endpoint = URLs.normalize_new_hosted_endpoint(soneium_endpoint, namespace: "soneium_endpoint")
259
+ @monad_endpoint = URLs.normalize_new_hosted_endpoint(monad_endpoint, namespace: "monad_endpoint")
260
+ @hyperliquid_endpoint = URLs.normalize_new_hosted_endpoint(hyperliquid_endpoint, namespace: "hyperliquid_endpoint")
261
+ @arc_endpoint = URLs.normalize_new_hosted_endpoint(arc_endpoint, namespace: "arc_endpoint")
262
+ @robinhood_endpoint = URLs.normalize_new_hosted_endpoint(robinhood_endpoint, namespace: "robinhood_endpoint")
263
+ @tron_endpoint = URLs.normalize_new_hosted_endpoint(tron_endpoint, namespace: "tron_endpoint")
264
+ @near_endpoint = URLs.normalize_new_hosted_endpoint(near_endpoint, namespace: "near_endpoint")
265
+ @sui_endpoint = URLs.normalize_new_hosted_endpoint(sui_endpoint, namespace: "sui_endpoint")
166
266
  @headers = headers.to_h.transform_keys(&:to_s).transform_values(&:to_s).freeze
167
267
  @timeout = timeout.to_f
168
268
  @solana_rpc = solana_rpc
169
269
  @ethereum_rpc = ethereum_rpc
170
270
  @avalanche_c_rpc = avalanche_c_rpc
271
+ @base_rpc = base_rpc
272
+ @arbitrum_rpc = arbitrum_rpc
273
+ @bitcoin_rpc = bitcoin_rpc
274
+ @polygon_rpc = polygon_rpc
275
+ @bsc_rpc = bsc_rpc
276
+ @optimism_rpc = optimism_rpc
277
+ @kaia_rpc = kaia_rpc
278
+ @soneium_rpc = soneium_rpc
279
+ @monad_rpc = monad_rpc
280
+ @hyperliquid_rpc = hyperliquid_rpc
281
+ @arc_rpc = arc_rpc
282
+ @robinhood_rpc = robinhood_rpc
283
+ @tron_rpc = tron_rpc
284
+ @near_rpc = near_rpc
285
+ @sui_rpc = sui_rpc
286
+ @sui_grpc_web = sui_grpc_web
287
+
288
+ {
289
+ "base_rpc" => base_rpc,
290
+ "arbitrum_rpc" => arbitrum_rpc,
291
+ "bitcoin_rpc" => bitcoin_rpc,
292
+ "polygon_rpc" => polygon_rpc, "bsc_rpc" => bsc_rpc,
293
+ "optimism_rpc" => optimism_rpc, "kaia_rpc" => kaia_rpc,
294
+ "soneium_rpc" => soneium_rpc, "monad_rpc" => monad_rpc,
295
+ "hyperliquid_rpc" => hyperliquid_rpc, "arc_rpc" => arc_rpc,
296
+ "robinhood_rpc" => robinhood_rpc, "tron_rpc" => tron_rpc,
297
+ "near_rpc" => near_rpc, "sui_rpc" => sui_rpc
298
+ }.each do |name, value|
299
+ next if value.nil? || value.websocket_url.nil?
300
+
301
+ raise ConfigError, "#{name} does not support WebSocket URLs", cause: nil
302
+ end
171
303
  end
172
304
 
173
305
  def inspect
@@ -177,7 +309,38 @@ module ERPC
177
309
  "account_endpoint=#{account_endpoint.inspect} user_endpoint=#{user_endpoint.inspect} " \
178
310
  "header_names=#{headers.keys.inspect} timeout=#{timeout.inspect} " \
179
311
  "solana_rpc=#{solana_rpc.inspect} ethereum_rpc=#{ethereum_rpc.inspect} " \
180
- "avalanche_c_rpc=#{avalanche_c_rpc.inspect}>"
312
+ "avalanche_c_rpc=#{avalanche_c_rpc.inspect} " \
313
+ "base_endpoint=#{new_origin(base_endpoint).inspect} " \
314
+ "base_rpc=#{new_direct_inspect(base_rpc)} " \
315
+ "arbitrum_endpoint=#{new_origin(arbitrum_endpoint).inspect} " \
316
+ "arbitrum_rpc=#{new_direct_inspect(arbitrum_rpc)} " \
317
+ "bitcoin_rpc=#{new_direct_inspect(bitcoin_rpc)}>"
318
+ end
319
+
320
+ private
321
+
322
+ def new_direct_inspect(value)
323
+ return "nil" if value.nil?
324
+
325
+ uri = URI.parse(value.http_url)
326
+ uri.path = "/"
327
+ uri.query = nil
328
+ uri.fragment = nil
329
+ "#<#{value.class} http_url=#{uri.to_s.inspect} websocket_url=nil " \
330
+ "header_names=#{value.headers.keys.inspect}>"
331
+ rescue URI::InvalidURIError, ArgumentError
332
+ "#<#{value.class} http_url=\"[REDACTED]\" websocket_url=nil header_names=#{value.headers.keys.inspect}>"
333
+ end
334
+
335
+ def new_origin(value)
336
+ uri = URI.parse(value.to_s)
337
+ uri.path = "/"
338
+ uri.query = nil
339
+ uri.fragment = nil
340
+ uri.user = nil if uri.respond_to?(:user=)
341
+ uri.to_s
342
+ rescue URI::InvalidURIError, ArgumentError
343
+ "[REDACTED]"
181
344
  end
182
345
  end
183
346
 
@@ -115,6 +115,35 @@ module ERPC
115
115
  private_class_method :percent_character_class
116
116
 
117
117
  def direct_variants(http_url:, websocket_url: nil, headers: {})
118
+ variants(direct_values(http_url: http_url, websocket_url: websocket_url, headers: headers))
119
+ end
120
+
121
+ def direct_variants_with_path(http_url:, websocket_url: nil, headers: {})
122
+ values = direct_values(http_url: http_url, websocket_url: websocket_url, headers: headers)
123
+ [http_url, websocket_url].compact.each do |endpoint|
124
+ path = URI.parse(endpoint).path.to_s
125
+ next if path.empty? || path == "/"
126
+
127
+ values << path
128
+ begin
129
+ values << URI.decode_www_form_component(path)
130
+ rescue ArgumentError
131
+ values << path
132
+ end
133
+ path.split("/").reject(&:empty?).each do |component|
134
+ values << component
135
+ begin
136
+ values << URI.decode_www_form_component(component)
137
+ rescue ArgumentError
138
+ values << component
139
+ end
140
+ end
141
+ end
142
+ variants(values)
143
+ end
144
+ private_class_method :direct_variants_with_path
145
+
146
+ def direct_values(http_url:, websocket_url:, headers:)
118
147
  values = []
119
148
  [http_url, websocket_url].compact.each do |endpoint|
120
149
  query = URI.parse(endpoint).query.to_s
@@ -159,7 +188,8 @@ module ERPC
159
188
  end
160
189
  end
161
190
 
162
- variants(values)
191
+ values
163
192
  end
193
+ private_class_method :direct_values
164
194
  end
165
195
  end