erpc-sdk 0.7.0 → 0.8.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/README.md +125 -5
- data/lib/erpc_sdk/bridge.rb +1507 -0
- data/lib/erpc_sdk/client.rb +210 -75
- data/lib/erpc_sdk/config.rb +107 -8
- data/lib/erpc_sdk/errors.rb +100 -9
- data/lib/erpc_sdk/generated/bridge_capabilities.rb +9 -0
- data/lib/erpc_sdk/generated/swap_execution_capabilities.rb +11 -0
- data/lib/erpc_sdk/swap.rb +679 -0
- data/lib/erpc_sdk/transport.rb +69 -20
- data/lib/erpc_sdk/version.rb +1 -1
- data/lib/erpc_sdk/websocket.rb +45 -13
- data/lib/erpc_sdk.rb +1 -0
- metadata +5 -2
data/lib/erpc_sdk/client.rb
CHANGED
|
@@ -27,53 +27,161 @@ module ERPC
|
|
|
27
27
|
|
|
28
28
|
def initialize(config, http_adapter: nil, websocket_factory: nil)
|
|
29
29
|
adapter = http_adapter || NetHttpAdapter.new
|
|
30
|
-
|
|
31
|
-
api_key: config.api_key,
|
|
32
|
-
endpoint: config.endpoint,
|
|
33
|
-
headers: config.headers,
|
|
34
|
-
timeout: config.timeout,
|
|
35
|
-
adapter: adapter
|
|
36
|
-
)
|
|
37
|
-
ethereum_transport = HttpJsonRpcTransport.new(
|
|
38
|
-
api_key: config.api_key,
|
|
39
|
-
endpoint: URLs.with_path(config.endpoint, "/eth"),
|
|
40
|
-
headers: config.headers,
|
|
41
|
-
timeout: config.timeout,
|
|
42
|
-
adapter: adapter
|
|
43
|
-
)
|
|
44
|
-
avalanche_transport = HttpJsonRpcTransport.new(
|
|
45
|
-
api_key: config.api_key,
|
|
46
|
-
endpoint: URLs.with_path(config.avalanche_endpoint, "/ava"),
|
|
47
|
-
headers: config.headers,
|
|
48
|
-
timeout: config.timeout,
|
|
49
|
-
adapter: adapter
|
|
50
|
-
)
|
|
51
|
-
avalanche_index_transport = lambda do |path|
|
|
30
|
+
unavailable_http = lambda do |namespace|
|
|
52
31
|
HttpJsonRpcTransport.new(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
headers: config.headers,
|
|
32
|
+
endpoint: URLs.unavailable_endpoint(namespace),
|
|
33
|
+
headers: {},
|
|
56
34
|
timeout: config.timeout,
|
|
57
|
-
adapter: adapter
|
|
35
|
+
adapter: adapter,
|
|
36
|
+
unavailable_namespace: namespace
|
|
58
37
|
)
|
|
59
38
|
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
39
|
+
legacy_http = lambda do |endpoint, namespace|
|
|
40
|
+
if config.api_key.nil?
|
|
41
|
+
unavailable_http.call(namespace)
|
|
42
|
+
else
|
|
43
|
+
HttpJsonRpcTransport.new(
|
|
44
|
+
api_key: config.api_key,
|
|
45
|
+
endpoint: endpoint,
|
|
46
|
+
headers: config.headers,
|
|
47
|
+
timeout: config.timeout,
|
|
48
|
+
adapter: adapter
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
direct_http = lambda do |override|
|
|
53
|
+
HttpJsonRpcTransport.new(
|
|
54
|
+
endpoint: override.http_url,
|
|
55
|
+
headers: override.headers,
|
|
56
|
+
timeout: config.timeout,
|
|
57
|
+
adapter: adapter,
|
|
58
|
+
direct: true,
|
|
59
|
+
redactions: Redaction.direct_variants(
|
|
60
|
+
http_url: override.http_url,
|
|
61
|
+
websocket_url: override.websocket_url,
|
|
62
|
+
headers: override.headers
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
selected_http = lambda do |override, endpoint, namespace|
|
|
67
|
+
override.nil? ? legacy_http.call(endpoint, namespace) : direct_http.call(override)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
unavailable_websocket = lambda do |namespace|
|
|
71
|
+
WebSocketJsonRpcTransport.new(
|
|
72
|
+
URLs.unavailable_endpoint(namespace),
|
|
73
|
+
nil,
|
|
74
|
+
config.timeout,
|
|
75
|
+
connection_factory: websocket_factory,
|
|
76
|
+
unavailable_namespace: namespace
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
legacy_websocket = lambda do |endpoint, namespace|
|
|
80
|
+
if config.api_key.nil?
|
|
81
|
+
unavailable_websocket.call(namespace)
|
|
82
|
+
else
|
|
83
|
+
WebSocketJsonRpcTransport.new(
|
|
84
|
+
endpoint,
|
|
85
|
+
config.api_key,
|
|
86
|
+
config.timeout,
|
|
87
|
+
connection_factory: websocket_factory
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
direct_websocket = lambda do |override, namespace|
|
|
92
|
+
if override.websocket_url.nil?
|
|
93
|
+
unavailable_websocket.call(namespace)
|
|
94
|
+
else
|
|
95
|
+
WebSocketJsonRpcTransport.new(
|
|
96
|
+
override.websocket_url,
|
|
97
|
+
nil,
|
|
98
|
+
config.timeout,
|
|
99
|
+
connection_factory: websocket_factory,
|
|
100
|
+
direct: true,
|
|
101
|
+
redactions: Redaction.direct_variants(
|
|
102
|
+
http_url: override.http_url,
|
|
103
|
+
websocket_url: override.websocket_url,
|
|
104
|
+
headers: override.headers
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
selected_websocket = lambda do |override, endpoint, namespace|
|
|
110
|
+
override.nil? ? legacy_websocket.call(endpoint, namespace) : direct_websocket.call(override, namespace)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
solana_transport = selected_http.call(config.solana_rpc, config.endpoint, "solana.rpc")
|
|
114
|
+
solana_aux_transport = if config.solana_rpc || config.api_key
|
|
115
|
+
solana_transport
|
|
116
|
+
else
|
|
117
|
+
unavailable_http.call("solana.das")
|
|
118
|
+
end
|
|
119
|
+
solana_history_transport = if config.solana_rpc || config.api_key
|
|
120
|
+
solana_transport
|
|
121
|
+
else
|
|
122
|
+
unavailable_http.call("solana.history")
|
|
123
|
+
end
|
|
124
|
+
solana_leaders_transport = if config.solana_rpc || config.api_key
|
|
125
|
+
solana_transport
|
|
126
|
+
else
|
|
127
|
+
unavailable_http.call("solana.leaders")
|
|
128
|
+
end
|
|
129
|
+
solana_analytics_transport = if config.solana_rpc || config.api_key
|
|
130
|
+
solana_transport
|
|
131
|
+
else
|
|
132
|
+
unavailable_http.call("solana.analytics")
|
|
133
|
+
end
|
|
134
|
+
ethereum_transport = selected_http.call(
|
|
135
|
+
config.ethereum_rpc,
|
|
136
|
+
URLs.with_path(config.endpoint, "/eth"),
|
|
137
|
+
"ethereum.rpc"
|
|
138
|
+
)
|
|
139
|
+
avalanche_transport = selected_http.call(
|
|
140
|
+
config.avalanche_c_rpc,
|
|
141
|
+
URLs.with_path(config.avalanche_endpoint, "/ava"),
|
|
142
|
+
"avalanche.rpc"
|
|
65
143
|
)
|
|
66
|
-
|
|
67
|
-
URLs.
|
|
68
|
-
|
|
69
|
-
config.timeout,
|
|
70
|
-
connection_factory: websocket_factory
|
|
144
|
+
avalanche_avax_transport = legacy_http.call(
|
|
145
|
+
URLs.with_path(config.avalanche_endpoint, "/ava"),
|
|
146
|
+
"avalanche.avax"
|
|
71
147
|
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
148
|
+
avalanche_x_chain_transport = if config.api_key
|
|
149
|
+
avalanche_avax_transport
|
|
150
|
+
else
|
|
151
|
+
unavailable_http.call("avalanche.x_chain")
|
|
152
|
+
end
|
|
153
|
+
avalanche_p_chain_transport = if config.api_key
|
|
154
|
+
avalanche_avax_transport
|
|
155
|
+
else
|
|
156
|
+
unavailable_http.call("avalanche.p_chain")
|
|
157
|
+
end
|
|
158
|
+
avalanche_proposer_vm_transport = if config.api_key
|
|
159
|
+
avalanche_avax_transport
|
|
160
|
+
else
|
|
161
|
+
unavailable_http.call("avalanche.proposer_vm")
|
|
162
|
+
end
|
|
163
|
+
avalanche_info_transport = if config.api_key
|
|
164
|
+
avalanche_avax_transport
|
|
165
|
+
else
|
|
166
|
+
unavailable_http.call("avalanche.info")
|
|
167
|
+
end
|
|
168
|
+
avalanche_index_transport = lambda do |path, namespace|
|
|
169
|
+
legacy_http.call(URLs.with_path(config.avalanche_endpoint, path), namespace)
|
|
170
|
+
end
|
|
171
|
+
solana_ws = selected_websocket.call(
|
|
172
|
+
config.solana_rpc,
|
|
173
|
+
config.api_key.nil? ? URLs.unavailable_endpoint("solana.subscriptions") : URLs.websocket(config.endpoint, config.api_key, ""),
|
|
174
|
+
"solana.subscriptions"
|
|
175
|
+
)
|
|
176
|
+
ethereum_ws = selected_websocket.call(
|
|
177
|
+
config.ethereum_rpc,
|
|
178
|
+
config.api_key.nil? ? URLs.unavailable_endpoint("ethereum.subscriptions") : URLs.websocket(config.endpoint, config.api_key, "/eth"),
|
|
179
|
+
"ethereum.subscriptions"
|
|
180
|
+
)
|
|
181
|
+
avalanche_ws = selected_websocket.call(
|
|
182
|
+
config.avalanche_c_rpc,
|
|
183
|
+
config.api_key.nil? ? URLs.unavailable_endpoint("avalanche.subscriptions") : URLs.websocket(config.avalanche_endpoint, config.api_key, "/ava-ws"),
|
|
184
|
+
"avalanche.subscriptions"
|
|
77
185
|
)
|
|
78
186
|
|
|
79
187
|
@solana = SolanaClient.new(
|
|
@@ -83,16 +191,16 @@ module ERPC
|
|
|
83
191
|
parameter_mode: :positional,
|
|
84
192
|
batch_policy: :solana_standard
|
|
85
193
|
),
|
|
86
|
-
das: RpcNamespace.new(
|
|
87
|
-
history: RpcNamespace.new(
|
|
194
|
+
das: RpcNamespace.new(solana_aux_transport, SOLANA_DAS_METHODS, parameter_mode: :named),
|
|
195
|
+
history: RpcNamespace.new(solana_history_transport, SOLANA_HISTORY_METHODS, parameter_mode: :positional),
|
|
88
196
|
leaders: RpcNamespace.new(
|
|
89
|
-
|
|
197
|
+
solana_leaders_transport,
|
|
90
198
|
SOLANA_LEADER_METHODS,
|
|
91
199
|
parameter_mode: :positional,
|
|
92
200
|
batch_policy: :unsupported
|
|
93
201
|
),
|
|
94
202
|
analytics: RpcNamespace.new(
|
|
95
|
-
|
|
203
|
+
solana_analytics_transport,
|
|
96
204
|
SOLANA_ANALYTICS_METHODS,
|
|
97
205
|
parameter_mode: :positional
|
|
98
206
|
),
|
|
@@ -104,33 +212,33 @@ module ERPC
|
|
|
104
212
|
)
|
|
105
213
|
@avalanche = AvalancheClient.new(
|
|
106
214
|
rpc: RpcNamespace.new(avalanche_transport, ETHEREUM_RPC_METHODS, parameter_mode: :positional),
|
|
107
|
-
avax: avalanche_namespace(
|
|
108
|
-
x_chain: avalanche_namespace(
|
|
109
|
-
p_chain: avalanche_namespace(
|
|
215
|
+
avax: avalanche_namespace(avalanche_avax_transport, AVALANCHE_AVAX_METHODS, "avax"),
|
|
216
|
+
x_chain: avalanche_namespace(avalanche_x_chain_transport, AVALANCHE_X_CHAIN_METHODS, "avm"),
|
|
217
|
+
p_chain: avalanche_namespace(avalanche_p_chain_transport, AVALANCHE_P_CHAIN_METHODS, "platform"),
|
|
110
218
|
proposer_vm: avalanche_namespace(
|
|
111
|
-
|
|
219
|
+
avalanche_proposer_vm_transport,
|
|
112
220
|
AVALANCHE_PROPOSER_VM_METHODS,
|
|
113
221
|
"proposervm"
|
|
114
222
|
),
|
|
115
|
-
info: avalanche_namespace(
|
|
223
|
+
info: avalanche_namespace(avalanche_info_transport, AVALANCHE_INFO_METHODS, "info"),
|
|
116
224
|
index: AvalancheIndexClient.new(
|
|
117
225
|
c_chain_blocks: avalanche_namespace(
|
|
118
|
-
avalanche_index_transport.call("/ava/ext/index/C/block"),
|
|
226
|
+
avalanche_index_transport.call("/ava/ext/index/C/block", "avalanche.index.c_chain_blocks"),
|
|
119
227
|
AVALANCHE_INDEX_METHODS,
|
|
120
228
|
"index"
|
|
121
229
|
),
|
|
122
230
|
p_chain_blocks: avalanche_namespace(
|
|
123
|
-
avalanche_index_transport.call("/ava/ext/index/P/block"),
|
|
231
|
+
avalanche_index_transport.call("/ava/ext/index/P/block", "avalanche.index.p_chain_blocks"),
|
|
124
232
|
AVALANCHE_INDEX_METHODS,
|
|
125
233
|
"index"
|
|
126
234
|
),
|
|
127
235
|
x_chain_blocks: avalanche_namespace(
|
|
128
|
-
avalanche_index_transport.call("/ava/ext/index/X/block"),
|
|
236
|
+
avalanche_index_transport.call("/ava/ext/index/X/block", "avalanche.index.x_chain_blocks"),
|
|
129
237
|
AVALANCHE_INDEX_METHODS,
|
|
130
238
|
"index"
|
|
131
239
|
),
|
|
132
240
|
x_chain_transactions: avalanche_namespace(
|
|
133
|
-
avalanche_index_transport.call("/ava/ext/index/X/tx"),
|
|
241
|
+
avalanche_index_transport.call("/ava/ext/index/X/tx", "avalanche.index.x_chain_transactions"),
|
|
134
242
|
AVALANCHE_INDEX_METHODS,
|
|
135
243
|
"index"
|
|
136
244
|
)
|
|
@@ -142,31 +250,58 @@ module ERPC
|
|
|
142
250
|
avalanche_transport: avalanche_transport
|
|
143
251
|
)
|
|
144
252
|
@price = PriceClient.new(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
253
|
+
if config.api_key.nil?
|
|
254
|
+
RestTransport.new(
|
|
255
|
+
endpoint: URLs.unavailable_endpoint("price"),
|
|
256
|
+
timeout: config.timeout,
|
|
257
|
+
adapter: adapter,
|
|
258
|
+
unavailable_namespace: "price"
|
|
259
|
+
)
|
|
260
|
+
else
|
|
261
|
+
RestTransport.new(
|
|
262
|
+
credential: config.api_key,
|
|
263
|
+
endpoint: config.endpoint,
|
|
264
|
+
headers: config.headers,
|
|
265
|
+
timeout: config.timeout,
|
|
266
|
+
adapter: adapter
|
|
267
|
+
)
|
|
268
|
+
end
|
|
152
269
|
)
|
|
153
270
|
@account = AccountClient.new(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
271
|
+
if config.api_key.nil?
|
|
272
|
+
RestTransport.new(
|
|
273
|
+
endpoint: URLs.unavailable_endpoint("account"),
|
|
274
|
+
timeout: config.timeout,
|
|
275
|
+
adapter: adapter,
|
|
276
|
+
unavailable_namespace: "account"
|
|
277
|
+
)
|
|
278
|
+
else
|
|
279
|
+
RestTransport.new(
|
|
280
|
+
credential: config.api_key,
|
|
281
|
+
endpoint: config.account_endpoint,
|
|
282
|
+
headers: config.headers,
|
|
283
|
+
timeout: config.timeout,
|
|
284
|
+
adapter: adapter
|
|
285
|
+
)
|
|
286
|
+
end
|
|
161
287
|
)
|
|
162
288
|
@usage = UsageClient.new(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
289
|
+
if config.api_key.nil?
|
|
290
|
+
RestTransport.new(
|
|
291
|
+
endpoint: URLs.unavailable_endpoint("usage"),
|
|
292
|
+
timeout: config.timeout,
|
|
293
|
+
adapter: adapter,
|
|
294
|
+
unavailable_namespace: "usage"
|
|
295
|
+
)
|
|
296
|
+
else
|
|
297
|
+
RestTransport.new(
|
|
298
|
+
credential: config.api_key,
|
|
299
|
+
endpoint: config.user_endpoint,
|
|
300
|
+
headers: config.headers,
|
|
301
|
+
timeout: config.timeout,
|
|
302
|
+
adapter: adapter
|
|
303
|
+
)
|
|
304
|
+
end
|
|
170
305
|
)
|
|
171
306
|
@closed = false
|
|
172
307
|
end
|
data/lib/erpc_sdk/config.rb
CHANGED
|
@@ -12,6 +12,33 @@ module ERPC
|
|
|
12
12
|
module URLs
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
+
DIRECT_HTTP_SCHEMES = %w[http https].freeze
|
|
16
|
+
DIRECT_WEBSOCKET_SCHEMES = %w[ws wss].freeze
|
|
17
|
+
|
|
18
|
+
def direct_endpoint(value, kind:, namespace: "direct endpoint")
|
|
19
|
+
schemes = kind == :websocket ? DIRECT_WEBSOCKET_SCHEMES : DIRECT_HTTP_SCHEMES
|
|
20
|
+
unless value.is_a?(String)
|
|
21
|
+
raise ConfigError, "#{namespace} must be an absolute #{kind == :websocket ? 'WS(S)' : 'HTTP(S)'} URL", cause: nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
input = value.strip
|
|
25
|
+
scheme_match = input.match?(/\A[a-z][a-z\d+.-]*:\/\//i)
|
|
26
|
+
authority = input[/\A[a-z][a-z\d+.-]*:\/\/([^\/?#]*)/i, 1]
|
|
27
|
+
valid = !input.empty? && scheme_match && !input.include?("#") && authority &&
|
|
28
|
+
!authority.empty? && !authority.include?("@")
|
|
29
|
+
begin
|
|
30
|
+
uri = URI.parse(input)
|
|
31
|
+
valid &&= uri.absolute? && !uri.host.to_s.empty? && schemes.include?(uri.scheme)
|
|
32
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
33
|
+
valid = false
|
|
34
|
+
end
|
|
35
|
+
unless valid
|
|
36
|
+
raise ConfigError, "#{namespace} must be an absolute #{kind == :websocket ? 'WS(S)' : 'HTTP(S)'} URL", cause: nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
input.freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
15
42
|
def normalize_endpoint(value, local_http_only: false)
|
|
16
43
|
uri = URI.parse(value.to_s)
|
|
17
44
|
unless uri.absolute? && uri.host && %w[http https].include?(uri.scheme)
|
|
@@ -29,7 +56,22 @@ module ERPC
|
|
|
29
56
|
uri.path = "/" if uri.path.empty?
|
|
30
57
|
uri.to_s
|
|
31
58
|
rescue URI::InvalidURIError, ArgumentError
|
|
32
|
-
raise ConfigError, "endpoint must be an absolute HTTP(S) URL"
|
|
59
|
+
raise ConfigError, "endpoint must be an absolute HTTP(S) URL", cause: nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def public_endpoint(endpoint)
|
|
63
|
+
uri = endpoint.is_a?(URI::Generic) ? endpoint.dup : URI.parse(endpoint.to_s)
|
|
64
|
+
uri.query = nil
|
|
65
|
+
uri.fragment = nil
|
|
66
|
+
uri.user = nil if uri.respond_to?(:user=)
|
|
67
|
+
uri.to_s
|
|
68
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
69
|
+
"[REDACTED]"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def unavailable_endpoint(namespace)
|
|
73
|
+
safe = namespace.to_s.gsub(/[^a-z\d._-]+/i, "-")
|
|
74
|
+
"https://unconfigured.invalid/#{safe}"
|
|
33
75
|
end
|
|
34
76
|
|
|
35
77
|
def with_path(endpoint, path)
|
|
@@ -56,14 +98,65 @@ module ERPC
|
|
|
56
98
|
end
|
|
57
99
|
end
|
|
58
100
|
|
|
101
|
+
class RpcEndpointConfig
|
|
102
|
+
attr_reader :http_url, :websocket_url, :headers
|
|
103
|
+
|
|
104
|
+
def initialize(http_url:, websocket_url: nil, headers: {})
|
|
105
|
+
@http_url = URLs.direct_endpoint(http_url, kind: :http, namespace: "http_url")
|
|
106
|
+
@websocket_url = if websocket_url.nil?
|
|
107
|
+
nil
|
|
108
|
+
else
|
|
109
|
+
URLs.direct_endpoint(websocket_url, kind: :websocket, namespace: "websocket_url")
|
|
110
|
+
end
|
|
111
|
+
unless headers.is_a?(Hash) || headers.respond_to?(:to_h)
|
|
112
|
+
raise ConfigError, "direct endpoint headers must be a mapping"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
values = headers.to_h
|
|
117
|
+
rescue StandardError
|
|
118
|
+
raise ConfigError, "direct endpoint headers must be a mapping", cause: nil
|
|
119
|
+
end
|
|
120
|
+
unless values.all? { |name, value| name.is_a?(String) && value.is_a?(String) }
|
|
121
|
+
raise ConfigError, "direct endpoint headers must contain string names and values"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@headers = values.dup.freeze
|
|
125
|
+
freeze
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def inspect
|
|
129
|
+
websocket = websocket_url.nil? ? "nil" : URLs.public_endpoint(websocket_url).inspect
|
|
130
|
+
"#<#{self.class} http_url=#{URLs.public_endpoint(http_url).inspect} " \
|
|
131
|
+
"websocket_url=#{websocket} " \
|
|
132
|
+
"header_names=#{headers.keys.inspect}>"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
alias to_s inspect
|
|
136
|
+
end
|
|
137
|
+
|
|
59
138
|
class ClientConfig
|
|
60
|
-
attr_reader :api_key, :endpoint, :account_endpoint, :user_endpoint, :headers, :timeout,
|
|
139
|
+
attr_reader :api_key, :endpoint, :account_endpoint, :user_endpoint, :headers, :timeout,
|
|
140
|
+
:avalanche_endpoint, :solana_rpc, :ethereum_rpc, :avalanche_c_rpc
|
|
61
141
|
|
|
62
|
-
def initialize(api_key
|
|
142
|
+
def initialize(api_key: nil, endpoint: DEFAULT_ENDPOINT, account_endpoint: DEFAULT_ACCOUNT_ENDPOINT,
|
|
63
143
|
user_endpoint: DEFAULT_USER_ENDPOINT, headers: {}, timeout: DEFAULT_TIMEOUT,
|
|
64
|
-
avalanche_endpoint: DEFAULT_AVALANCHE_ENDPOINT
|
|
65
|
-
|
|
66
|
-
|
|
144
|
+
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) }
|
|
148
|
+
raise ConfigError, "direct RPC overrides must use RpcEndpointConfig"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
@api_key = if api_key.nil?
|
|
152
|
+
nil
|
|
153
|
+
elsif api_key.is_a?(String)
|
|
154
|
+
api_key.strip
|
|
155
|
+
else
|
|
156
|
+
raise ConfigError, "api_key must be a string"
|
|
157
|
+
end
|
|
158
|
+
@api_key = nil if @api_key&.empty?
|
|
159
|
+
raise ConfigError, "api_key must not be empty" if @api_key.nil? && direct_endpoints.compact.empty?
|
|
67
160
|
raise ConfigError, "timeout must be positive" unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
68
161
|
|
|
69
162
|
@endpoint = URLs.normalize_endpoint(endpoint)
|
|
@@ -72,13 +165,19 @@ module ERPC
|
|
|
72
165
|
@user_endpoint = URLs.normalize_endpoint(user_endpoint)
|
|
73
166
|
@headers = headers.to_h.transform_keys(&:to_s).transform_values(&:to_s).freeze
|
|
74
167
|
@timeout = timeout.to_f
|
|
168
|
+
@solana_rpc = solana_rpc
|
|
169
|
+
@ethereum_rpc = ethereum_rpc
|
|
170
|
+
@avalanche_c_rpc = avalanche_c_rpc
|
|
75
171
|
end
|
|
76
172
|
|
|
77
173
|
def inspect
|
|
78
|
-
|
|
174
|
+
key = api_key.nil? ? nil : "[REDACTED]"
|
|
175
|
+
"#<#{self.class} api_key=#{key.inspect} endpoint=#{endpoint.inspect} " \
|
|
79
176
|
"avalanche_endpoint=#{avalanche_endpoint.inspect} " \
|
|
80
177
|
"account_endpoint=#{account_endpoint.inspect} user_endpoint=#{user_endpoint.inspect} " \
|
|
81
|
-
"header_names=#{headers.keys.inspect} timeout=#{timeout.inspect}
|
|
178
|
+
"header_names=#{headers.keys.inspect} timeout=#{timeout.inspect} " \
|
|
179
|
+
"solana_rpc=#{solana_rpc.inspect} ethereum_rpc=#{ethereum_rpc.inspect} " \
|
|
180
|
+
"avalanche_c_rpc=#{avalanche_c_rpc.inspect}>"
|
|
82
181
|
end
|
|
83
182
|
end
|
|
84
183
|
|
data/lib/erpc_sdk/errors.rb
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
|
+
require "base64"
|
|
4
5
|
|
|
5
6
|
module ERPC
|
|
6
7
|
class Error < StandardError; end
|
|
7
8
|
|
|
8
9
|
class ConfigError < Error; end
|
|
10
|
+
class NotConfiguredError < Error
|
|
11
|
+
attr_reader :namespace
|
|
12
|
+
|
|
13
|
+
def initialize(namespace)
|
|
14
|
+
@namespace = namespace.to_s.freeze
|
|
15
|
+
super("ERPC namespace #{@namespace.inspect} is not configured")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
9
18
|
class BatchPolicyError < Error; end
|
|
10
19
|
class InvalidResponseError < Error; end
|
|
11
20
|
class TransportError < Error; end
|
|
@@ -41,21 +50,30 @@ module ERPC
|
|
|
41
50
|
module Redaction
|
|
42
51
|
module_function
|
|
43
52
|
|
|
53
|
+
PERCENT_ESCAPE = /%[0-9A-Fa-f]{2}/.freeze
|
|
54
|
+
|
|
44
55
|
def text(value, credential)
|
|
45
56
|
result = value.to_s
|
|
46
|
-
variants(credential).each
|
|
57
|
+
variants(credential).each do |secret|
|
|
58
|
+
result = result.gsub(secret, "[REDACTED]") unless secret.empty?
|
|
59
|
+
next unless secret.match?(PERCENT_ESCAPE)
|
|
60
|
+
|
|
61
|
+
result = result.gsub(percent_escape_pattern(secret), "[REDACTED]")
|
|
62
|
+
end
|
|
47
63
|
result
|
|
48
64
|
end
|
|
49
65
|
|
|
50
|
-
def value(value, credential)
|
|
66
|
+
def value(value, credential, depth = 0)
|
|
67
|
+
return "[REDACTED]" if depth >= 32
|
|
68
|
+
|
|
51
69
|
case value
|
|
52
70
|
when String
|
|
53
71
|
text(value, credential)
|
|
54
72
|
when Array
|
|
55
|
-
value.map { |item| self.value(item, credential) }
|
|
73
|
+
value.map { |item| self.value(item, credential, depth + 1) }
|
|
56
74
|
when Hash
|
|
57
75
|
value.to_h do |key, item|
|
|
58
|
-
[text(key, credential), self.value(item, credential)]
|
|
76
|
+
[key.is_a?(String) ? text(key, credential) : key, self.value(item, credential, depth + 1)]
|
|
59
77
|
end
|
|
60
78
|
else
|
|
61
79
|
value
|
|
@@ -63,12 +81,85 @@ module ERPC
|
|
|
63
81
|
end
|
|
64
82
|
|
|
65
83
|
def variants(credential)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
values = credential.is_a?(Array) ? credential : [credential]
|
|
85
|
+
encoded = values.flat_map do |value|
|
|
86
|
+
next [] if value.nil? || value.to_s.empty?
|
|
87
|
+
|
|
88
|
+
text = value.to_s
|
|
89
|
+
percent_encoded = text.b.each_byte.map do |byte|
|
|
90
|
+
character = byte.chr
|
|
91
|
+
character.match?(/[A-Za-z0-9_.~-]/) ? character : format("%%%02X", byte)
|
|
92
|
+
end.join
|
|
93
|
+
[
|
|
94
|
+
text,
|
|
95
|
+
URI.encode_www_form_component(text),
|
|
96
|
+
percent_encoded
|
|
97
|
+
]
|
|
98
|
+
end
|
|
99
|
+
encoded.uniq.sort_by { |value| [-value.length, value] }
|
|
71
100
|
end
|
|
72
101
|
private_class_method :variants
|
|
102
|
+
|
|
103
|
+
def percent_escape_pattern(value)
|
|
104
|
+
escaped = Regexp.escape(value.to_s)
|
|
105
|
+
pattern = escaped.gsub(PERCENT_ESCAPE) do |escape|
|
|
106
|
+
"%#{percent_character_class(escape[1])}#{percent_character_class(escape[2])}"
|
|
107
|
+
end
|
|
108
|
+
Regexp.new(pattern)
|
|
109
|
+
end
|
|
110
|
+
private_class_method :percent_escape_pattern
|
|
111
|
+
|
|
112
|
+
def percent_character_class(value)
|
|
113
|
+
"[#{[value.upcase, value.downcase].uniq.join}]"
|
|
114
|
+
end
|
|
115
|
+
private_class_method :percent_character_class
|
|
116
|
+
|
|
117
|
+
def direct_variants(http_url:, websocket_url: nil, headers: {})
|
|
118
|
+
values = []
|
|
119
|
+
[http_url, websocket_url].compact.each do |endpoint|
|
|
120
|
+
query = URI.parse(endpoint).query.to_s
|
|
121
|
+
query.split("&").each do |component|
|
|
122
|
+
next if component.empty?
|
|
123
|
+
|
|
124
|
+
raw_value = component.include?("=") ? component.split("=", 2).last : component
|
|
125
|
+
next if raw_value.empty?
|
|
126
|
+
|
|
127
|
+
values << raw_value
|
|
128
|
+
begin
|
|
129
|
+
values << URI.decode_www_form_component(raw_value)
|
|
130
|
+
rescue ArgumentError
|
|
131
|
+
# The endpoint validator rejects invalid percent escapes. Keep
|
|
132
|
+
# the raw value as a defensive fallback for caller-built objects.
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
headers.each do |name, value|
|
|
138
|
+
next unless value.is_a?(String) && !value.empty?
|
|
139
|
+
|
|
140
|
+
values << value
|
|
141
|
+
next unless %w[authorization proxy-authorization].include?(name.to_s.downcase)
|
|
142
|
+
|
|
143
|
+
parts = value.strip.split(/\s+/, 2)
|
|
144
|
+
next unless parts.length == 2 && %w[bearer basic].include?(parts.first.downcase)
|
|
145
|
+
|
|
146
|
+
credential = parts.last
|
|
147
|
+
values << credential
|
|
148
|
+
next unless parts.first.casecmp?("basic")
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
decoded = Base64.strict_decode64(credential).force_encoding(Encoding::UTF_8)
|
|
152
|
+
values << decoded
|
|
153
|
+
if decoded.include?(":")
|
|
154
|
+
username, password = decoded.split(":", 2)
|
|
155
|
+
values << username << password
|
|
156
|
+
end
|
|
157
|
+
rescue ArgumentError, EncodingError
|
|
158
|
+
# Raw and encoded forms above still cover malformed Basic values.
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
variants(values)
|
|
163
|
+
end
|
|
73
164
|
end
|
|
74
165
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated by registry/generate-bridge-capabilities.mjs. Do not edit.
|
|
4
|
+
|
|
5
|
+
module ERPC
|
|
6
|
+
BRIDGE_CAPABILITIES_JSON = '[{"bridgeCapabilityId":"bridge-mayan-swift-v2-eurc-eth-sol","providerId":"mayan-swift-v2","capabilityKind":"external-provider-dynamic","sourceChainId":"eip155:1","destinationChainId":"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp","sourceTokenDeploymentId":"deployment-0011","destinationTokenDeploymentId":"deployment-0013","sourceTokenAddress":"0x1abaea1f7c830bd89acc67ec4af516284b1bc33c","destinationTokenAddress":"HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr","sourceTokenStandard":"erc20","destinationTokenStandard":"spl-token","sourceTokenDecimals":6,"destinationTokenDecimals":6,"sourceProviderChainName":"ethereum","destinationProviderChainName":"solana","sourceProviderChainId":1,"destinationProviderChainId":0,"sourceWormholeChainId":2,"destinationWormholeChainId":1,"sourceUsdcDeploymentId":"deployment-0008","sourceUsdcAddress":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","sourceUsdcStandard":"erc20","sourceUsdcDecimals":6,"swiftContract":"0x40ffe85a28dc9993541449464d7529a922142960","forwarderAddress":"0x337685fdab40d39bd02028545a4ffa7d287cc3e2","forwarderFunctionSelector":"0x30dedc57","jupiterProgramAddress":null,"builderEndpoint":"https://tx-builder.mayan.finance","explorerEndpoint":"https://explorer-api.mayan.finance/v3","dependencies":["mayan-hosted-quote-api","mayan-hosted-transaction-builder","mayan-hosted-source-swap-builder","swift-auction-solvers","relayers","wormhole-guardian-messaging","mayan-explorer-indexer"],"status":"active"},{"bridgeCapabilityId":"bridge-mayan-swift-v2-eurc-sol-eth","providerId":"mayan-swift-v2","capabilityKind":"external-provider-dynamic","sourceChainId":"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp","destinationChainId":"eip155:1","sourceTokenDeploymentId":"deployment-0013","destinationTokenDeploymentId":"deployment-0011","sourceTokenAddress":"HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr","destinationTokenAddress":"0x1abaea1f7c830bd89acc67ec4af516284b1bc33c","sourceTokenStandard":"spl-token","destinationTokenStandard":"erc20","sourceTokenDecimals":6,"destinationTokenDecimals":6,"sourceProviderChainName":"solana","destinationProviderChainName":"ethereum","sourceProviderChainId":0,"destinationProviderChainId":1,"sourceWormholeChainId":1,"destinationWormholeChainId":2,"sourceUsdcDeploymentId":"deployment-0010","sourceUsdcAddress":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v","sourceUsdcStandard":"spl-token","sourceUsdcDecimals":6,"swiftContract":"mayan34VedncxdK2XobtvWFDXQASUTBXhUVzt2kKgny","forwarderAddress":null,"forwarderFunctionSelector":null,"jupiterProgramAddress":"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4","builderEndpoint":"https://tx-builder.mayan.finance","explorerEndpoint":"https://explorer-api.mayan.finance/v3","dependencies":["mayan-hosted-quote-api","mayan-hosted-transaction-builder","mayan-hosted-source-swap-builder","swift-auction-solvers","relayers","wormhole-guardian-messaging","mayan-explorer-indexer","jupiter-v6-source-swap"],"status":"active"}]'.freeze
|
|
7
|
+
BRIDGE_CAPABILITIES_CONTENT_DIGEST = 'fe698f99218057f8cb40b3e24421d1d78e4e2cd7508cf6e3eb2457b329e5141b'.freeze
|
|
8
|
+
BRIDGE_CAPABILITIES_AS_OF_DATE = '2026-09-16'.freeze
|
|
9
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated by registry/generate-swap-execution-capabilities.mjs. Do not edit.
|
|
4
|
+
|
|
5
|
+
module ERPC
|
|
6
|
+
SWAP_EXECUTION_CAPABILITIES_JSON = '[{"swapExecutionCapabilityId":"swap-execution-0001","chainId":"eip155:1","dexDeploymentId":"dex-deployment-0001","poolDefinitionId":"pool-0001","factoryAddress":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f","routerAddress":"0x7a250d5630b4cf539739df2c5dacb4c659f2488d","routerKind":"uniswap-v2-router02","adapterKind":"evm-constant-product-v2","token0DeploymentId":"deployment-0008","token0Address":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","token0Standard":"erc20","token1DeploymentId":"deployment-0002","token1Address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","token1Standard":"erc20","wrappedNativeTokenDeploymentId":"deployment-0002","wrappedNativeTokenAddress":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","wrappedNativeFunctionSelector":"0xad5c4648","functionKind":"exact-input-erc20-to-erc20","functionSignature":"swapExactTokensForTokens(uint256,uint256,address[],address,uint256)","functionSelector":"0x38ed1739","status":"active"},{"swapExecutionCapabilityId":"swap-execution-0002","chainId":"eip155:43114","dexDeploymentId":"dex-deployment-0002","poolDefinitionId":"pool-0002","factoryAddress":"0x9ad6c38be94206ca50bb0d90783181662f0cfa10","routerAddress":"0x60ae616a2155ee3d9a68541ba4544862310933d4","routerKind":"joe-v1-router02","adapterKind":"evm-constant-product-v2","token0DeploymentId":"deployment-0004","token0Address":"0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7","token0Standard":"erc20","token1DeploymentId":"deployment-0009","token1Address":"0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e","token1Standard":"erc20","wrappedNativeTokenDeploymentId":"deployment-0004","wrappedNativeTokenAddress":"0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7","wrappedNativeFunctionSelector":"0x73b295c2","functionKind":"exact-input-erc20-to-erc20","functionSignature":"swapExactTokensForTokens(uint256,uint256,address[],address,uint256)","functionSelector":"0x38ed1739","status":"active"}]'.freeze
|
|
7
|
+
SWAP_EXECUTION_CAPABILITIES_CONTENT_DIGEST = '84424dc48687e2cb1dd0c9a2446f820efd2c311e71ce5acacfeb6e882811875f'.freeze
|
|
8
|
+
SWAP_EXECUTION_CAPABILITIES_AS_OF_DATE = '2026-09-16'.freeze
|
|
9
|
+
SWAP_EXECUTION_FUNCTION_SIGNATURE = 'swapExactTokensForTokens(uint256,uint256,address[],address,uint256)'.freeze
|
|
10
|
+
SWAP_EXECUTION_FUNCTION_SELECTOR = '0x38ed1739'.freeze
|
|
11
|
+
end
|