erpc-sdk 0.4.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +101 -0
- data/lib/erpc_sdk/client.rb +136 -0
- data/lib/erpc_sdk/config.rb +99 -0
- data/lib/erpc_sdk/errors.rb +74 -0
- data/lib/erpc_sdk/rest.rb +370 -0
- data/lib/erpc_sdk/rpc.rb +269 -0
- data/lib/erpc_sdk/transport.rb +225 -0
- data/lib/erpc_sdk/version.rb +5 -0
- data/lib/erpc_sdk/websocket.rb +458 -0
- data/lib/erpc_sdk.rb +10 -0
- metadata +100 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module ERPC
|
|
7
|
+
module Validation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def mapping(value)
|
|
11
|
+
raise InvalidResponseError unless value.is_a?(Hash) && value.keys.all? { |key| key.is_a?(String) }
|
|
12
|
+
|
|
13
|
+
value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def string(value)
|
|
17
|
+
raise InvalidResponseError unless value.is_a?(String)
|
|
18
|
+
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def number(value, integer: false, nonnegative: false)
|
|
23
|
+
valid = value.is_a?(Numeric) && value.finite?
|
|
24
|
+
valid &&= value.is_a?(Integer) if integer
|
|
25
|
+
valid &&= value >= 0 if nonnegative
|
|
26
|
+
raise InvalidResponseError unless valid
|
|
27
|
+
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def datetime(value)
|
|
32
|
+
text = string(value)
|
|
33
|
+
Time.iso8601(text)
|
|
34
|
+
text
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
raise InvalidResponseError
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class AccountClient
|
|
41
|
+
def initialize(transport)
|
|
42
|
+
@transport = transport
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_token_balance
|
|
46
|
+
data = Validation.mapping(@transport.get("/v3/erpc/token-balance"))
|
|
47
|
+
unless %w[business developer free pro].include?(data["plan"])
|
|
48
|
+
raise InvalidResponseError, "ERPC returned an invalid token balance"
|
|
49
|
+
end
|
|
50
|
+
Validation.number(data["max_tokens"], integer: true)
|
|
51
|
+
Validation.number(data["remaining_tokens"], integer: true)
|
|
52
|
+
Validation.string(data["next_refill_at"]) unless data["next_refill_at"].nil?
|
|
53
|
+
data
|
|
54
|
+
rescue InvalidResponseError
|
|
55
|
+
raise InvalidResponseError, "ERPC returned an invalid token balance"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class UsageClient
|
|
60
|
+
YEAR_MONTH = /\A\d{4}-(0[1-9]|1[0-2])\z/
|
|
61
|
+
private_constant :YEAR_MONTH
|
|
62
|
+
|
|
63
|
+
def initialize(transport)
|
|
64
|
+
@transport = transport
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_monthly_api_key_usage(year_month = nil)
|
|
68
|
+
if !year_month.nil? && !YEAR_MONTH.match?(year_month)
|
|
69
|
+
raise ConfigError, "year_month must use YYYY-MM format"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
envelope = Validation.mapping(@transport.get("/v3/user/api-keys/usage", "yearMonth" => year_month))
|
|
73
|
+
raise InvalidResponseError unless envelope["success"] == true
|
|
74
|
+
|
|
75
|
+
usage = Validation.mapping(envelope["message"])
|
|
76
|
+
validate_usage(usage)
|
|
77
|
+
usage
|
|
78
|
+
rescue InvalidResponseError
|
|
79
|
+
raise InvalidResponseError, "ERPC returned an invalid monthly API key usage response"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def validate_usage(usage)
|
|
85
|
+
raise InvalidResponseError unless YEAR_MONTH.match?(Validation.string(usage["yearMonth"]))
|
|
86
|
+
|
|
87
|
+
%w[keyCount totalCount totalCredits].each { |field| Validation.number(usage[field]) }
|
|
88
|
+
raise InvalidResponseError unless [true, false].include?(usage["hasStrandedUsage"])
|
|
89
|
+
Validation.string(usage["updatedAt"]) unless usage["updatedAt"].nil?
|
|
90
|
+
validate_chains(usage["chains"])
|
|
91
|
+
raise InvalidResponseError unless usage["apiKeys"].is_a?(Array)
|
|
92
|
+
|
|
93
|
+
usage["apiKeys"].each do |raw|
|
|
94
|
+
item = Validation.mapping(raw)
|
|
95
|
+
Validation.string(item["apiKeyLast4"])
|
|
96
|
+
%w[apiKeyLength count credits].each { |field| Validation.number(item[field]) }
|
|
97
|
+
Validation.number(item["keyId"]) unless item["keyId"].nil?
|
|
98
|
+
Validation.string(item["updatedAt"]) unless item["updatedAt"].nil?
|
|
99
|
+
validate_chains(item["chains"])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_chains(value)
|
|
104
|
+
raise InvalidResponseError unless value.is_a?(Array)
|
|
105
|
+
|
|
106
|
+
value.each do |raw_chain|
|
|
107
|
+
chain = Validation.mapping(raw_chain)
|
|
108
|
+
Validation.string(chain["chain"])
|
|
109
|
+
%w[count credits].each { |field| Validation.number(chain[field]) }
|
|
110
|
+
Validation.string(chain["updatedAt"]) unless chain["updatedAt"].nil?
|
|
111
|
+
raise InvalidResponseError unless chain["methods"].is_a?(Array)
|
|
112
|
+
|
|
113
|
+
chain["methods"].each do |raw_method|
|
|
114
|
+
method = Validation.mapping(raw_method)
|
|
115
|
+
Validation.string(method["method"])
|
|
116
|
+
%w[count creditCost credits].each { |field| Validation.number(method[field]) }
|
|
117
|
+
Validation.string(method["updatedAt"]) unless method["updatedAt"].nil?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class PriceClient
|
|
124
|
+
def initialize(transport)
|
|
125
|
+
@transport = transport
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def get_price_feeds(asset_type: nil, query: nil)
|
|
129
|
+
value = @transport.get("/v2/price_feeds", "query" => query, "asset_type" => asset_type)
|
|
130
|
+
raise InvalidResponseError, "ERPC returned invalid price feed metadata" unless value.is_a?(Array)
|
|
131
|
+
|
|
132
|
+
value.each do |raw|
|
|
133
|
+
metadata = Validation.mapping(raw)
|
|
134
|
+
Validation.string(metadata["id"])
|
|
135
|
+
attributes = metadata["attributes"]
|
|
136
|
+
next if attributes.nil?
|
|
137
|
+
unless attributes.is_a?(Hash) && attributes.all? { |key, item| key.is_a?(String) && item.is_a?(String) }
|
|
138
|
+
raise InvalidResponseError, "ERPC returned invalid price feed metadata"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
value
|
|
142
|
+
rescue InvalidResponseError
|
|
143
|
+
raise InvalidResponseError, "ERPC returned invalid price feed metadata"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def get_latest_price_updates(ids:, encoding: nil, parsed: nil, ignore_invalid_price_ids: nil)
|
|
147
|
+
value = @transport.get(
|
|
148
|
+
"/v2/updates/price/latest",
|
|
149
|
+
update_query(ids, encoding, parsed, ignore_invalid_price_ids)
|
|
150
|
+
)
|
|
151
|
+
price_update(value)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def get_price_updates_at_timestamp(publish_time, ids:, encoding: nil, parsed: nil,
|
|
155
|
+
ignore_invalid_price_ids: nil)
|
|
156
|
+
value = @transport.get(
|
|
157
|
+
"/v2/updates/price/#{URLs.escape_path(publish_time)}",
|
|
158
|
+
update_query(ids, encoding, parsed, ignore_invalid_price_ids)
|
|
159
|
+
)
|
|
160
|
+
price_update(value)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def get_latest_publisher_stake_caps(encoding: nil, parsed: nil)
|
|
164
|
+
Validation.mapping(
|
|
165
|
+
@transport.get(
|
|
166
|
+
"/v2/updates/publisher_stake_caps/latest",
|
|
167
|
+
"encoding" => encoding,
|
|
168
|
+
"parsed" => parsed
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def stream_price_updates(ids:, encoding: nil, parsed: nil, ignore_invalid_price_ids: nil,
|
|
174
|
+
allow_unordered: nil, benchmarks_only: nil)
|
|
175
|
+
query = update_query(ids, encoding, parsed, ignore_invalid_price_ids).merge(
|
|
176
|
+
"allow_unordered" => allow_unordered,
|
|
177
|
+
"benchmarks_only" => benchmarks_only
|
|
178
|
+
)
|
|
179
|
+
chunks = @transport.stream("/v2/updates/price/stream", query)
|
|
180
|
+
Enumerator.new do |yielder|
|
|
181
|
+
buffer = +""
|
|
182
|
+
chunks.each do |chunk|
|
|
183
|
+
buffer << chunk
|
|
184
|
+
while (boundary = buffer.match(/\r?\n\r?\n/))
|
|
185
|
+
block = buffer.slice!(0, boundary.end(0))
|
|
186
|
+
event = parse_sse_block(block.sub(/\r?\n\r?\n\z/, ""))
|
|
187
|
+
yielder << event if event
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
event = parse_sse_block(buffer)
|
|
191
|
+
yielder << event if event
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def update_query(ids, encoding, parsed, ignore_invalid_price_ids)
|
|
198
|
+
{
|
|
199
|
+
"ids[]" => ids,
|
|
200
|
+
"encoding" => encoding,
|
|
201
|
+
"parsed" => parsed,
|
|
202
|
+
"ignore_invalid_price_ids" => ignore_invalid_price_ids
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def price_update(value)
|
|
207
|
+
update = Validation.mapping(value)
|
|
208
|
+
binary = Validation.mapping(update["binary"])
|
|
209
|
+
data = binary["data"]
|
|
210
|
+
valid = data.is_a?(Array) && data.all? { |item| item.is_a?(String) }
|
|
211
|
+
valid &&= binary["encoding"].is_a?(String)
|
|
212
|
+
valid &&= update["parsed"].is_a?(Array) if update.key?("parsed")
|
|
213
|
+
raise InvalidResponseError, "ERPC returned an invalid price update" unless valid
|
|
214
|
+
|
|
215
|
+
update
|
|
216
|
+
rescue InvalidResponseError
|
|
217
|
+
raise InvalidResponseError, "ERPC returned an invalid price update"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def parse_sse_block(block)
|
|
221
|
+
data = []
|
|
222
|
+
result = {}
|
|
223
|
+
block.each_line(chomp: true) do |line|
|
|
224
|
+
next if line.empty? || line.start_with?(":")
|
|
225
|
+
|
|
226
|
+
field, separator, raw = line.partition(":")
|
|
227
|
+
value = separator.empty? ? "" : raw.sub(/\A /, "")
|
|
228
|
+
data << value if field == "data"
|
|
229
|
+
result[field] = value if %w[event id].include?(field)
|
|
230
|
+
end
|
|
231
|
+
return nil if data.empty?
|
|
232
|
+
|
|
233
|
+
result["data"] = price_update(JSON.parse(data.join("\n")))
|
|
234
|
+
result
|
|
235
|
+
rescue JSON::ParserError, InvalidResponseError
|
|
236
|
+
raise InvalidResponseError, "ERPC returned malformed stream data"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
class CloudCatalogClient
|
|
241
|
+
RESOURCE_KINDS = %w[bare-metal solana-grpc solana-shredstream vps].freeze
|
|
242
|
+
RESOURCE_MODES = %w[dedicated direct shared].freeze
|
|
243
|
+
|
|
244
|
+
def initialize(transport)
|
|
245
|
+
@transport = transport
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def list
|
|
249
|
+
envelope = Validation.mapping(@transport.get("/v4/cloud/catalog"))
|
|
250
|
+
message = Validation.mapping(envelope["message"])
|
|
251
|
+
offerings = message["offerings"]
|
|
252
|
+
raise InvalidResponseError unless envelope["success"] == true && offerings.is_a?(Array)
|
|
253
|
+
|
|
254
|
+
offerings.each do |raw|
|
|
255
|
+
item = Validation.mapping(raw)
|
|
256
|
+
%w[id name description].each { |field| Validation.string(item[field]) }
|
|
257
|
+
raise InvalidResponseError unless RESOURCE_KINDS.include?(item["kind"])
|
|
258
|
+
raise InvalidResponseError if item["mode"] && !RESOURCE_MODES.include?(item["mode"])
|
|
259
|
+
%w[regions capabilities].each do |field|
|
|
260
|
+
array = item[field]
|
|
261
|
+
raise InvalidResponseError unless array.is_a?(Array) && array.all? { |entry| entry.is_a?(String) }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
offerings
|
|
265
|
+
rescue InvalidResponseError
|
|
266
|
+
raise InvalidResponseError, "ERPC returned an invalid Cloud catalog"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
class CloudCreditClient
|
|
271
|
+
ALERT_LEVELS = %w[critical normal suspended warning].freeze
|
|
272
|
+
|
|
273
|
+
def initialize(transport)
|
|
274
|
+
@transport = transport
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def get
|
|
278
|
+
envelope = Validation.mapping(@transport.get("/v4/cloud/credit"))
|
|
279
|
+
raise InvalidResponseError unless envelope["success"] == true
|
|
280
|
+
|
|
281
|
+
credit = Validation.mapping(envelope["message"])
|
|
282
|
+
raise InvalidResponseError unless ALERT_LEVELS.include?(credit["alertLevel"])
|
|
283
|
+
Validation.number(credit["balanceCents"], integer: true)
|
|
284
|
+
Validation.number(credit["burnRateCentsPerHour"], integer: true, nonnegative: true)
|
|
285
|
+
Validation.number(credit["timeToZeroHours"], nonnegative: true) unless credit["timeToZeroHours"].nil?
|
|
286
|
+
Validation.datetime(credit["quoteTimestamp"])
|
|
287
|
+
Validation.datetime(credit["quoteExpiresAt"])
|
|
288
|
+
credit
|
|
289
|
+
rescue InvalidResponseError
|
|
290
|
+
raise InvalidResponseError, "ERPC returned an invalid Cloud credit snapshot"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
class CloudResourcesClient
|
|
295
|
+
RESOURCE_KINDS = CloudCatalogClient::RESOURCE_KINDS
|
|
296
|
+
RESOURCE_MODES = CloudCatalogClient::RESOURCE_MODES
|
|
297
|
+
BILLING_STATUSES = %w[active grace-period inactive suspended].freeze
|
|
298
|
+
|
|
299
|
+
def initialize(transport)
|
|
300
|
+
@transport = transport
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def list
|
|
304
|
+
envelope = Validation.mapping(@transport.get("/v4/cloud/resources"))
|
|
305
|
+
message = Validation.mapping(envelope["message"])
|
|
306
|
+
resources = message["resources"]
|
|
307
|
+
raise InvalidResponseError unless envelope["success"] == true && resources.is_a?(Array)
|
|
308
|
+
|
|
309
|
+
resources.each { |resource| validate_resource(resource) }
|
|
310
|
+
resources
|
|
311
|
+
rescue InvalidResponseError
|
|
312
|
+
raise InvalidResponseError, "ERPC returned an invalid Cloud resource list"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def get(resource_id)
|
|
316
|
+
value = @transport.get("/v4/cloud/resources/#{URLs.escape_path(normalize_id(resource_id))}")
|
|
317
|
+
envelope = Validation.mapping(value)
|
|
318
|
+
message = Validation.mapping(envelope["message"])
|
|
319
|
+
raise InvalidResponseError unless envelope["success"] == true
|
|
320
|
+
|
|
321
|
+
validate_resource(message["resource"])
|
|
322
|
+
rescue InvalidResponseError
|
|
323
|
+
raise InvalidResponseError, "ERPC returned an invalid Cloud resource"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def get_status(resource_id)
|
|
327
|
+
path = "/v4/cloud/resources/#{URLs.escape_path(normalize_id(resource_id))}/status"
|
|
328
|
+
envelope = Validation.mapping(@transport.get(path))
|
|
329
|
+
raise InvalidResponseError unless envelope["success"] == true
|
|
330
|
+
|
|
331
|
+
status = Validation.mapping(envelope["message"])
|
|
332
|
+
Validation.string(status["id"])
|
|
333
|
+
Validation.string(status["status"])
|
|
334
|
+
validate_billing(status["billing"]) if status.key?("billing")
|
|
335
|
+
status
|
|
336
|
+
rescue InvalidResponseError
|
|
337
|
+
raise InvalidResponseError, "ERPC returned an invalid Cloud resource status"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
private
|
|
341
|
+
|
|
342
|
+
def normalize_id(value)
|
|
343
|
+
result = value.to_s.strip
|
|
344
|
+
raise ConfigError, "resource_id must not be empty" if result.empty?
|
|
345
|
+
|
|
346
|
+
result
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def validate_resource(value)
|
|
350
|
+
resource = Validation.mapping(value)
|
|
351
|
+
Validation.string(resource["id"])
|
|
352
|
+
Validation.string(resource["status"])
|
|
353
|
+
raise InvalidResponseError unless RESOURCE_KINDS.include?(resource["kind"])
|
|
354
|
+
raise InvalidResponseError if resource["mode"] && !RESOURCE_MODES.include?(resource["mode"])
|
|
355
|
+
%w[name region createdAt].each do |field|
|
|
356
|
+
Validation.string(resource[field]) unless resource[field].nil?
|
|
357
|
+
end
|
|
358
|
+
resource
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def validate_billing(value)
|
|
362
|
+
billing = Validation.mapping(value)
|
|
363
|
+
raise InvalidResponseError unless BILLING_STATUSES.include?(billing["status"])
|
|
364
|
+
Validation.number(billing["hourlyCredits"], nonnegative: true) if billing.key?("hourlyCredits")
|
|
365
|
+
%w[nextChargeAt graceEndsAt].each do |field|
|
|
366
|
+
Validation.datetime(billing[field]) if billing.key?(field)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
data/lib/erpc_sdk/rpc.rb
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ERPC
|
|
4
|
+
SOLANA_RPC_METHODS = [
|
|
5
|
+
"getAccountInfo",
|
|
6
|
+
"getBalance",
|
|
7
|
+
"getBlock",
|
|
8
|
+
"getBlockCommitment",
|
|
9
|
+
"getBlockHeight",
|
|
10
|
+
"getBlockProduction",
|
|
11
|
+
"getBlockTime",
|
|
12
|
+
"getBlocks",
|
|
13
|
+
"getBlocksWithLimit",
|
|
14
|
+
"getClusterNodes",
|
|
15
|
+
"getEpochInfo",
|
|
16
|
+
"getEpochSchedule",
|
|
17
|
+
"getFeeForMessage",
|
|
18
|
+
"getFirstAvailableBlock",
|
|
19
|
+
"getGenesisHash",
|
|
20
|
+
"getHealth",
|
|
21
|
+
"getHighestSnapshotSlot",
|
|
22
|
+
"getIdentity",
|
|
23
|
+
"getInflationGovernor",
|
|
24
|
+
"getInflationRate",
|
|
25
|
+
"getInflationReward",
|
|
26
|
+
"getLargestAccounts",
|
|
27
|
+
"getLatestBlockhash",
|
|
28
|
+
"getLeaderSchedule",
|
|
29
|
+
"getMaxRetransmitSlot",
|
|
30
|
+
"getMaxShredInsertSlot",
|
|
31
|
+
"getMinimumBalanceForRentExemption",
|
|
32
|
+
"getMultipleAccounts",
|
|
33
|
+
"getParsedTransaction",
|
|
34
|
+
"getPriorityFeeEstimate",
|
|
35
|
+
"getProgramAccounts",
|
|
36
|
+
"getProgramAccountsV2",
|
|
37
|
+
"getRecentPerformanceSamples",
|
|
38
|
+
"getRecentPrioritizationFees",
|
|
39
|
+
"getSignatureStatuses",
|
|
40
|
+
"getSignaturesForAddress",
|
|
41
|
+
"getSlot",
|
|
42
|
+
"getSlotLeader",
|
|
43
|
+
"getSlotLeaders",
|
|
44
|
+
"getStakeMinimumDelegation",
|
|
45
|
+
"getSupply",
|
|
46
|
+
"getTokenAccountBalance",
|
|
47
|
+
"getTokenAccountsByDelegate",
|
|
48
|
+
"getTokenAccountsByOwner",
|
|
49
|
+
"getTokenLargestAccounts",
|
|
50
|
+
"getTokenSupply",
|
|
51
|
+
"getTransaction",
|
|
52
|
+
"getTransactionCount",
|
|
53
|
+
"getVersion",
|
|
54
|
+
"getVoteAccounts",
|
|
55
|
+
"isBlockhashValid",
|
|
56
|
+
"minimumLedgerSlot",
|
|
57
|
+
"requestAirdrop",
|
|
58
|
+
"sendTransaction",
|
|
59
|
+
"simulateTransaction"
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
SOLANA_DAS_METHODS = [
|
|
63
|
+
"getAsset",
|
|
64
|
+
"getAssetBatch",
|
|
65
|
+
"getAssetProof",
|
|
66
|
+
"getAssetProofBatch",
|
|
67
|
+
"getAssetsByAuthority",
|
|
68
|
+
"getAssetsByCreator",
|
|
69
|
+
"getAssetsByGroup",
|
|
70
|
+
"getAssetsByOwner",
|
|
71
|
+
"getNftEditions",
|
|
72
|
+
"getSignaturesForAsset",
|
|
73
|
+
"getTokenAccounts",
|
|
74
|
+
"getTokensByDelegate",
|
|
75
|
+
"getTokensByOwner",
|
|
76
|
+
"searchAssets"
|
|
77
|
+
].freeze
|
|
78
|
+
|
|
79
|
+
SOLANA_HISTORY_METHODS = [
|
|
80
|
+
"getTransactionsForAddress",
|
|
81
|
+
"getTransfersByAddress"
|
|
82
|
+
].freeze
|
|
83
|
+
|
|
84
|
+
SOLANA_LEADER_METHODS = [
|
|
85
|
+
"getLeaderSlots",
|
|
86
|
+
"getValidatorsInformation"
|
|
87
|
+
].freeze
|
|
88
|
+
|
|
89
|
+
SOLANA_ANALYTICS_METHODS = [
|
|
90
|
+
"jetEpochSummary",
|
|
91
|
+
"jetProgramStats",
|
|
92
|
+
"jetSlotStats",
|
|
93
|
+
"jetTopPrograms",
|
|
94
|
+
"jetTpsTimeseries"
|
|
95
|
+
].freeze
|
|
96
|
+
|
|
97
|
+
SOLANA_ENHANCED_SUBSCRIPTION_METHODS = [
|
|
98
|
+
"accountSubscribe",
|
|
99
|
+
"accountUnsubscribe",
|
|
100
|
+
"transactionSubscribe",
|
|
101
|
+
"transactionUnsubscribe"
|
|
102
|
+
].freeze
|
|
103
|
+
|
|
104
|
+
ETHEREUM_RPC_METHODS = [
|
|
105
|
+
"eth_accounts",
|
|
106
|
+
"eth_baseFee",
|
|
107
|
+
"eth_blobBaseFee",
|
|
108
|
+
"eth_blockNumber",
|
|
109
|
+
"eth_call",
|
|
110
|
+
"eth_callMany",
|
|
111
|
+
"eth_capabilities",
|
|
112
|
+
"eth_chainId",
|
|
113
|
+
"eth_createAccessList",
|
|
114
|
+
"eth_estimateGas",
|
|
115
|
+
"eth_feeHistory",
|
|
116
|
+
"eth_gasPrice",
|
|
117
|
+
"eth_getAccount",
|
|
118
|
+
"eth_getBalance",
|
|
119
|
+
"eth_getBlockByHash",
|
|
120
|
+
"eth_getBlockByNumber",
|
|
121
|
+
"eth_getBlockReceipts",
|
|
122
|
+
"eth_getBlockTransactionCountByHash",
|
|
123
|
+
"eth_getBlockTransactionCountByNumber",
|
|
124
|
+
"eth_getCode",
|
|
125
|
+
"eth_getFilterChanges",
|
|
126
|
+
"eth_getFilterLogs",
|
|
127
|
+
"eth_getLogs",
|
|
128
|
+
"eth_getProof",
|
|
129
|
+
"eth_getRawTransactionByHash",
|
|
130
|
+
"eth_getStorageAt",
|
|
131
|
+
"eth_getTransactionByBlockHashAndIndex",
|
|
132
|
+
"eth_getTransactionByBlockNumberAndIndex",
|
|
133
|
+
"eth_getTransactionByHash",
|
|
134
|
+
"eth_getTransactionBySenderAndNonce",
|
|
135
|
+
"eth_getTransactionCount",
|
|
136
|
+
"eth_getTransactionReceipt",
|
|
137
|
+
"eth_getUncleCountByBlockHash",
|
|
138
|
+
"eth_getUncleCountByBlockNumber",
|
|
139
|
+
"eth_maxPriorityFeePerGas",
|
|
140
|
+
"eth_newBlockFilter",
|
|
141
|
+
"eth_newFilter",
|
|
142
|
+
"eth_newPendingTransactionFilter",
|
|
143
|
+
"eth_sendRawTransaction",
|
|
144
|
+
"eth_signTransaction",
|
|
145
|
+
"eth_simulateV1",
|
|
146
|
+
"eth_submitWork",
|
|
147
|
+
"eth_syncing",
|
|
148
|
+
"eth_uninstallFilter",
|
|
149
|
+
"net_listening",
|
|
150
|
+
"net_peerCount",
|
|
151
|
+
"net_version",
|
|
152
|
+
"txpool_content",
|
|
153
|
+
"txpool_contentFrom",
|
|
154
|
+
"txpool_inspect",
|
|
155
|
+
"txpool_status",
|
|
156
|
+
"web3_clientVersion",
|
|
157
|
+
"web3_sha3"
|
|
158
|
+
].freeze
|
|
159
|
+
|
|
160
|
+
ETHEREUM_SUBSCRIPTION_METHODS = [
|
|
161
|
+
"eth_subscribe",
|
|
162
|
+
"eth_unsubscribe"
|
|
163
|
+
].freeze
|
|
164
|
+
|
|
165
|
+
HEAVY_SOLANA_METHODS = %w[
|
|
166
|
+
getPriorityFeeEstimate
|
|
167
|
+
getProgramAccounts
|
|
168
|
+
getProgramAccountsV2
|
|
169
|
+
getTokenLargestAccounts
|
|
170
|
+
].freeze
|
|
171
|
+
private_constant :HEAVY_SOLANA_METHODS
|
|
172
|
+
|
|
173
|
+
class PendingRpcRequest
|
|
174
|
+
def initialize(transport, method, params, decoder)
|
|
175
|
+
@transport = transport
|
|
176
|
+
@method = method
|
|
177
|
+
@params = params
|
|
178
|
+
@decoder = decoder
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def send
|
|
182
|
+
@decoder.call(@transport.request(@method, @params))
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class PendingRpcBatchRequest
|
|
187
|
+
def initialize(transport, calls)
|
|
188
|
+
@transport = transport
|
|
189
|
+
@calls = calls.map(&:dup).freeze
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def send
|
|
193
|
+
@transport.batch(@calls)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
class RpcNamespace
|
|
198
|
+
attr_reader :endpoint
|
|
199
|
+
|
|
200
|
+
def initialize(transport, methods, parameter_mode:, batch_policy: :any)
|
|
201
|
+
@transport = transport
|
|
202
|
+
@endpoint = transport.endpoint
|
|
203
|
+
@methods = methods.to_h { |method| [method, true] }.freeze
|
|
204
|
+
@aliases = methods.to_h { |method| [snake_case(method), method] }.freeze
|
|
205
|
+
@parameter_mode = parameter_mode
|
|
206
|
+
@batch_policy = batch_policy
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def request(method, params = nil, decoder: nil)
|
|
210
|
+
unless @methods.key?(method)
|
|
211
|
+
raise ConfigError, "#{method.inspect} is not in this namespace; use raw for forward-compatible methods"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
PendingRpcRequest.new(@transport, method, params, decoder || ->(value) { value })
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def raw(method, params = nil, decoder: nil)
|
|
218
|
+
raise ConfigError, "method must not be empty" if method.to_s.empty?
|
|
219
|
+
|
|
220
|
+
PendingRpcRequest.new(@transport, method, params, decoder || ->(value) { value })
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def batch(calls)
|
|
224
|
+
if @batch_policy == :unsupported && !calls.empty?
|
|
225
|
+
raise BatchPolicyError, "Leader RPC methods do not support batching"
|
|
226
|
+
end
|
|
227
|
+
if @batch_policy == :solana_standard
|
|
228
|
+
methods = calls.map { |call| call[:method] || call["method"] }
|
|
229
|
+
has_heavy = methods.any? { |method| HEAVY_SOLANA_METHODS.include?(method) }
|
|
230
|
+
has_standard = methods.any? { |method| !HEAVY_SOLANA_METHODS.include?(method) }
|
|
231
|
+
if has_heavy && has_standard
|
|
232
|
+
raise BatchPolicyError, "Solana indexed and standard RPC methods cannot share a batch"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
PendingRpcBatchRequest.new(@transport, calls)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def method_missing(name, *arguments, &block)
|
|
240
|
+
return super if block
|
|
241
|
+
|
|
242
|
+
string_name = name.to_s
|
|
243
|
+
method = @methods.key?(string_name) ? string_name : @aliases[string_name]
|
|
244
|
+
return super unless method
|
|
245
|
+
|
|
246
|
+
params = if @parameter_mode == :positional
|
|
247
|
+
arguments
|
|
248
|
+
elsif arguments.empty?
|
|
249
|
+
nil
|
|
250
|
+
elsif arguments.length == 1 && arguments.first.is_a?(Hash)
|
|
251
|
+
arguments.first
|
|
252
|
+
else
|
|
253
|
+
raise ConfigError, "named RPC methods require one Hash argument"
|
|
254
|
+
end
|
|
255
|
+
request(method, params)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def respond_to_missing?(name, include_private = false)
|
|
259
|
+
string_name = name.to_s
|
|
260
|
+
@methods.key?(string_name) || @aliases.key?(string_name) || super
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
private
|
|
264
|
+
|
|
265
|
+
def snake_case(value)
|
|
266
|
+
value.gsub(/(?<=[a-z0-9])(?=[A-Z])/, "_").downcase
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|