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.
- checksums.yaml +4 -4
- data/LICENSES/Apache-2.0.txt +201 -0
- data/LICENSES/BSD-3-Clause-Google-Protobuf.txt +29 -0
- data/README.md +46 -9
- data/THIRD_PARTY_NOTICES.md +41 -0
- data/lib/erpc_sdk/arbitrum.rb +62 -0
- data/lib/erpc_sdk/arc.rb +33 -0
- data/lib/erpc_sdk/base.rb +333 -0
- data/lib/erpc_sdk/bitcoin.rb +126 -0
- data/lib/erpc_sdk/bitcoin_codec.rb +574 -0
- data/lib/erpc_sdk/bitcoin_transport.rb +369 -0
- data/lib/erpc_sdk/bsc.rb +33 -0
- data/lib/erpc_sdk/client.rb +109 -1
- data/lib/erpc_sdk/config.rb +168 -5
- data/lib/erpc_sdk/errors.rb +31 -1
- data/lib/erpc_sdk/generated/rpc_network_contract.rb +3196 -0
- data/lib/erpc_sdk/hyperliquid.rb +33 -0
- data/lib/erpc_sdk/kaia.rb +33 -0
- data/lib/erpc_sdk/monad.rb +33 -0
- data/lib/erpc_sdk/near.rb +276 -0
- data/lib/erpc_sdk/near_transport.rb +345 -0
- data/lib/erpc_sdk/optimism.rb +33 -0
- data/lib/erpc_sdk/polygon.rb +33 -0
- data/lib/erpc_sdk/robinhood.rb +33 -0
- data/lib/erpc_sdk/soneium.rb +33 -0
- data/lib/erpc_sdk/sui.rb +5 -0
- data/lib/erpc_sdk/sui_grpc_web.rb +226 -0
- data/lib/erpc_sdk/sui_proto.rb +370 -0
- data/lib/erpc_sdk/transport.rb +28 -0
- data/lib/erpc_sdk/tron.rb +33 -0
- data/lib/erpc_sdk/version.rb +1 -1
- data/lib/erpc_sdk.rb +21 -0
- metadata +43 -7
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
require_relative "sui_proto"
|
|
7
|
+
|
|
8
|
+
module ERPC
|
|
9
|
+
class SuiGrpcWebEndpointConfig
|
|
10
|
+
attr_reader :base_url, :headers
|
|
11
|
+
|
|
12
|
+
def initialize(base_url:, headers: {})
|
|
13
|
+
@base_url = validate_url(base_url)
|
|
14
|
+
values = headers.to_h
|
|
15
|
+
raise ConfigError, "sui_grpc_web headers must contain string names and values", cause: nil unless values.all? { |name, value| name.is_a?(String) && value.is_a?(String) }
|
|
16
|
+
@headers = values.dup.freeze
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def inspect
|
|
21
|
+
uri = URI.parse(base_url); uri.path = "/"; uri.query = nil; uri.fragment = nil
|
|
22
|
+
"#<#{self.class} base_url=#{uri.to_s.inspect} header_names=#{headers.keys.inspect}>"
|
|
23
|
+
rescue URI::InvalidURIError
|
|
24
|
+
"#<#{self.class} base_url=\"[REDACTED]\" header_names=#{headers.keys.inspect}>"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias to_s inspect
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def validate_url(value)
|
|
32
|
+
input = value.to_s
|
|
33
|
+
uri = URI.parse(input)
|
|
34
|
+
valid = uri.absolute? && uri.host && %w[http https].include?(uri.scheme) && !input.include?("#") && !input.match?(%r{://[^/?#]*@})
|
|
35
|
+
raise ConfigError, "sui_grpc_web base_url must be an absolute HTTP(S) URL", cause: nil unless valid
|
|
36
|
+
input.freeze
|
|
37
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
38
|
+
raise ConfigError, "sui_grpc_web base_url must be an absolute HTTP(S) URL", cause: nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
SuiTransportFacts = Struct.new(:hosted_target, :method_names, :public_methods, keyword_init: true) do
|
|
43
|
+
def initialize(hosted_target:, method_names:, public_methods: {})
|
|
44
|
+
super(hosted_target: hosted_target.to_s.freeze,
|
|
45
|
+
method_names: method_names.map { |name| name.to_s.dup.freeze }.freeze,
|
|
46
|
+
public_methods: public_methods.each_with_object({}) do |(name, wire), result|
|
|
47
|
+
result[name.to_s.dup.freeze] = wire.to_s.dup.freeze
|
|
48
|
+
end.freeze)
|
|
49
|
+
freeze
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module SuiContract
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
def facts_from_contract(contract)
|
|
57
|
+
client = contract.fetch("clientContracts").find { |value| value["clientContractId"] == "client-sui-grpc" }
|
|
58
|
+
method_set_id = client.fetch("methodSetId")
|
|
59
|
+
hosted = client.fetch("endpointModes").find { |value| value["mode"] == "hosted" }
|
|
60
|
+
method_set = contract.fetch("methodSets").find { |value| value["methodSetId"] == method_set_id }
|
|
61
|
+
capabilities = method_set.fetch("capabilities")
|
|
62
|
+
selected = capabilities.each_with_object({}) do |item, result|
|
|
63
|
+
public_name = case item.fetch("capabilityId")
|
|
64
|
+
when "sui-get-service-info" then "get_service_info"
|
|
65
|
+
when "sui-get-object" then "get_object"
|
|
66
|
+
when "sui-get-balance" then "get_balance"
|
|
67
|
+
else raise KeyError
|
|
68
|
+
end
|
|
69
|
+
result[public_name] = item.fetch("wireOperation")
|
|
70
|
+
end
|
|
71
|
+
SuiTransportFacts.new(hosted_target: hosted.fetch("defaultTarget"), method_names: selected.values, public_methods: selected)
|
|
72
|
+
rescue KeyError, NoMethodError, TypeError
|
|
73
|
+
raise ConfigError, "Sui contract facts are unavailable", cause: nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
SUI_TRANSPORT_FACTS = SuiContract.facts_from_contract(RPC_NETWORK_CONTRACT)
|
|
78
|
+
SUI_GRPC_WEB_METHODS = %w[get_service_info get_object get_balance].map { |name| SUI_TRANSPORT_FACTS.public_methods.fetch(name) }.freeze
|
|
79
|
+
private_constant :SUI_TRANSPORT_FACTS
|
|
80
|
+
|
|
81
|
+
class SuiGrpcWebTransport
|
|
82
|
+
attr_reader :endpoint
|
|
83
|
+
|
|
84
|
+
def initialize(facts:, api_key: nil, endpoint: nil, direct_endpoint: nil, headers: {}, timeout: DEFAULT_TIMEOUT,
|
|
85
|
+
adapter: NetHttpAdapter.new, namespace: "sui.grpc")
|
|
86
|
+
@facts = facts
|
|
87
|
+
@api_key = api_key.is_a?(String) && !api_key.strip.empty? ? api_key.strip : nil
|
|
88
|
+
@timeout = timeout.to_f
|
|
89
|
+
raise ConfigError, "timeout must be positive", cause: nil unless @timeout.positive?
|
|
90
|
+
@adapter = adapter
|
|
91
|
+
@namespace = namespace.to_s.freeze
|
|
92
|
+
@mutex = Mutex.new
|
|
93
|
+
@next_id = 0
|
|
94
|
+
@direct = !direct_endpoint.nil?
|
|
95
|
+
if direct_endpoint
|
|
96
|
+
config = direct_endpoint.is_a?(SuiGrpcWebEndpointConfig) ? direct_endpoint : SuiGrpcWebEndpointConfig.new(base_url: direct_endpoint.http_url, headers: direct_endpoint.headers)
|
|
97
|
+
@target_prefix = config.base_url
|
|
98
|
+
@headers = config.headers.dup
|
|
99
|
+
@redactions = Redaction.send(:direct_variants_with_path, http_url: config.base_url, headers: config.headers)
|
|
100
|
+
@available = true
|
|
101
|
+
elsif @api_key
|
|
102
|
+
@target_prefix = endpoint || facts.hosted_target
|
|
103
|
+
@headers = headers.to_h.dup
|
|
104
|
+
@redactions = [@api_key]
|
|
105
|
+
@available = true
|
|
106
|
+
else
|
|
107
|
+
@target_prefix = nil
|
|
108
|
+
@headers = {}
|
|
109
|
+
@redactions = []
|
|
110
|
+
@available = false
|
|
111
|
+
end
|
|
112
|
+
@endpoint = if @available
|
|
113
|
+
uri = URI.parse(@target_prefix); uri.path = "/"; uri.query = nil; uri.fragment = nil; uri.to_s
|
|
114
|
+
else
|
|
115
|
+
""
|
|
116
|
+
end
|
|
117
|
+
rescue URI::InvalidURIError
|
|
118
|
+
raise ConfigError, "sui_grpc_web endpoint is invalid", cause: nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def inspect
|
|
122
|
+
"#<#{self.class} endpoint=#{endpoint.inspect} header_names=#{safe_header_names.inspect}>"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def request(operation, payload, decoder)
|
|
126
|
+
raise NotConfiguredError, @namespace unless @available
|
|
127
|
+
path = SUI_GRPC_WEB_METHODS.fetch(operation)
|
|
128
|
+
target = join(path)
|
|
129
|
+
result = nil
|
|
130
|
+
Timeout.timeout(@timeout) do
|
|
131
|
+
yielded = false
|
|
132
|
+
@adapter.request_stream(method: :post, url: target, headers: request_headers, body: payload, timeout: @timeout) do |status, chunks, response_headers|
|
|
133
|
+
yielded = true
|
|
134
|
+
raise HttpError, status unless status.to_i == 200
|
|
135
|
+
content_types = Array(response_headers && (response_headers["content-type"] || response_headers["Content-Type"])).flatten
|
|
136
|
+
raise InvalidResponseError.new("ERPC returned an invalid Sui response"), cause: nil unless content_types.length == 1 && content_types.first.to_s == SUI_GRPC_WEB_CONTENT_TYPE
|
|
137
|
+
result = decoder.call(SuiProto.decode_response(chunks))
|
|
138
|
+
end
|
|
139
|
+
raise TransportError, "Unable to reach ERPC", cause: nil unless yielded
|
|
140
|
+
end
|
|
141
|
+
result
|
|
142
|
+
rescue ::Timeout::Error
|
|
143
|
+
raise TimeoutError, @timeout, cause: nil
|
|
144
|
+
rescue InvalidResponseError, JsonRpcError, HttpError, NotConfiguredError, ConfigError, TimeoutError, TransportError
|
|
145
|
+
raise
|
|
146
|
+
rescue IOError, EOFError, SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
|
|
147
|
+
raise TransportError, "Unable to reach ERPC", cause: nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def join(path)
|
|
153
|
+
uri = URI.parse(@target_prefix)
|
|
154
|
+
base = uri.path.to_s.sub(%r{/+\z}, "")
|
|
155
|
+
uri.path = "#{base}/#{path.sub(%r{\A/+}, "")}"
|
|
156
|
+
if @direct
|
|
157
|
+
uri.to_s
|
|
158
|
+
else
|
|
159
|
+
query = URI.decode_www_form(uri.query.to_s).reject { |key, _| key.casecmp?("api-key") }
|
|
160
|
+
uri.query = URI.encode_www_form(query)
|
|
161
|
+
uri.fragment = nil
|
|
162
|
+
uri.to_s
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def request_headers
|
|
167
|
+
values = @headers.dup
|
|
168
|
+
unless @direct
|
|
169
|
+
values.delete_if { |key, _| key.to_s.casecmp?("authorization") }
|
|
170
|
+
values["authorization"] = "Bearer #{@api_key}"
|
|
171
|
+
end
|
|
172
|
+
values.delete_if { |key, _| %w[accept content-type x-grpc-web].include?(key.to_s.downcase) }
|
|
173
|
+
values["accept"] = SUI_GRPC_WEB_CONTENT_TYPE
|
|
174
|
+
values["content-type"] = SUI_GRPC_WEB_CONTENT_TYPE
|
|
175
|
+
values["x-grpc-web"] = "1"
|
|
176
|
+
values
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def safe_header_names
|
|
180
|
+
@headers.keys.map(&:to_s).sort
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class SuiGrpcWebPendingRequest
|
|
185
|
+
def initialize(&block); @block = block; end
|
|
186
|
+
def send; @block.call; end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
class SuiGrpcWebClient
|
|
190
|
+
attr_reader :endpoint
|
|
191
|
+
|
|
192
|
+
def initialize(transport)
|
|
193
|
+
@transport = transport
|
|
194
|
+
@endpoint = transport.endpoint
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def inspect
|
|
198
|
+
"#<#{self.class} endpoint=#{endpoint.inspect}>"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def get_service_info
|
|
202
|
+
SuiGrpcWebPendingRequest.new { @transport.request(0, SuiProto.service_info_request, SuiProto.method(:parse_service_info)) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def get_object(object_id, version = nil)
|
|
206
|
+
SuiGrpcWebPendingRequest.new { @transport.request(1, SuiProto.object_request(object_id, version), SuiProto.method(:parse_object_response)) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def get_balance(owner, coin_type)
|
|
210
|
+
SuiGrpcWebPendingRequest.new { @transport.request(2, SuiProto.balance_request(owner, coin_type), SuiProto.method(:parse_balance_response)) }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
class SuiClient
|
|
215
|
+
attr_reader :grpc
|
|
216
|
+
def initialize(transport); @grpc = SuiGrpcWebClient.new(transport); freeze; end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
module Sui
|
|
220
|
+
module_function
|
|
221
|
+
def create_client(transport); SuiClient.new(transport); end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private_constant :SuiTransportFacts, :SuiContract, :SUI_TRANSPORT_FACTS,
|
|
225
|
+
:SuiGrpcWebTransport, :SuiGrpcWebPendingRequest
|
|
226
|
+
end
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) Mysten Labs, Inc.
|
|
4
|
+
# Adapted handwritten bounded Sui schema projection.
|
|
5
|
+
#
|
|
6
|
+
# Source provenance: MystenLabs/sui-apis commit
|
|
7
|
+
# aa9ba2c13b2a9348059b38110e64af39849a0377; selected source paths are
|
|
8
|
+
# proto/sui/rpc/v2/ledger_service.proto, state_service.proto, object.proto,
|
|
9
|
+
# and proto/google/protobuf/timestamp.proto. This file implements only the
|
|
10
|
+
# three read-only unary messages used by this SDK; unrelated schema fields and
|
|
11
|
+
# general protobuf reflection/runtime support are intentionally omitted.
|
|
12
|
+
# SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
13
|
+
|
|
14
|
+
module ERPC
|
|
15
|
+
module SuiProto
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
MAX_U64 = 18_446_744_073_709_551_615
|
|
19
|
+
MAX_I64 = 9_223_372_036_854_775_807
|
|
20
|
+
MIN_I64 = -9_223_372_036_854_775_808
|
|
21
|
+
MAX_REQUEST_BYTES = 1_048_576
|
|
22
|
+
MAX_RESPONSE_BYTES = 8_388_608
|
|
23
|
+
MAX_TRAILER_BYTES = 8_192
|
|
24
|
+
MAX_RECURSION = 100
|
|
25
|
+
|
|
26
|
+
def validate_u64(value, message = "Sui value must be a u64")
|
|
27
|
+
unless value.is_a?(Integer) && !value.is_a?(TrueClass) && !value.is_a?(FalseClass) && value.between?(0, MAX_U64)
|
|
28
|
+
raise ConfigError, message, cause: nil
|
|
29
|
+
end
|
|
30
|
+
value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_input_string(value, label)
|
|
34
|
+
unless value.is_a?(String)
|
|
35
|
+
raise ConfigError, "#{label} must be a non-empty UTF-8 string", cause: nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
bytes = value.dup.force_encoding(Encoding::BINARY)
|
|
39
|
+
utf8 = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
40
|
+
unless !bytes.empty? && utf8.valid_encoding? && !utf8.match?(/[\x00-\x1f\x7f]/)
|
|
41
|
+
raise ConfigError, "#{label} must be a non-empty UTF-8 string", cause: nil
|
|
42
|
+
end
|
|
43
|
+
utf8
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def encode_varint(value)
|
|
47
|
+
validate_u64(value)
|
|
48
|
+
output = +"".b
|
|
49
|
+
loop do
|
|
50
|
+
byte = value & 0x7f
|
|
51
|
+
value >>= 7
|
|
52
|
+
output << (value.zero? ? byte : byte | 0x80)
|
|
53
|
+
break if value.zero?
|
|
54
|
+
end
|
|
55
|
+
output
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def encode_key(field, wire)
|
|
59
|
+
encode_varint((field << 3) | wire)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def encode_string(field, value)
|
|
63
|
+
bytes = value.to_s.encode(Encoding::UTF_8).b
|
|
64
|
+
encode_key(field, 2) + encode_varint(bytes.bytesize) + bytes
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def encode_u64(field, value)
|
|
68
|
+
encode_key(field, 0) + encode_varint(value)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def frame(payload)
|
|
72
|
+
raise ConfigError, "Sui request exceeds the 1 MiB framed limit", cause: nil if payload.bytesize + 5 > MAX_REQUEST_BYTES
|
|
73
|
+
"\x00".b + [payload.bytesize].pack("N") + payload
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def service_info_request
|
|
77
|
+
frame("".b)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def object_request(object_id, version = nil)
|
|
81
|
+
object_id = validate_input_string(object_id, "object_id")
|
|
82
|
+
payload = encode_string(1, object_id)
|
|
83
|
+
payload << encode_u64(2, validate_u64(version, "version must be a u64")) unless version.nil?
|
|
84
|
+
frame(payload)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def balance_request(owner, coin_type)
|
|
88
|
+
payload = encode_string(1, validate_input_string(owner, "owner"))
|
|
89
|
+
payload << encode_string(2, validate_input_string(coin_type, "coin_type"))
|
|
90
|
+
frame(payload)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def decode_response(chunks)
|
|
94
|
+
total = 0
|
|
95
|
+
body = +"".b
|
|
96
|
+
enum = if chunks.respond_to?(:each)
|
|
97
|
+
chunks
|
|
98
|
+
else
|
|
99
|
+
raise InvalidResponseError.new("ERPC returned an invalid Sui response"), cause: nil
|
|
100
|
+
end
|
|
101
|
+
enum.each do |chunk|
|
|
102
|
+
bytes = chunk.to_s.b
|
|
103
|
+
raise InvalidResponseError.new("ERPC returned an invalid Sui response"), cause: nil if total + bytes.bytesize > MAX_RESPONSE_BYTES
|
|
104
|
+
body << bytes
|
|
105
|
+
total += bytes.bytesize
|
|
106
|
+
end
|
|
107
|
+
decode_frames(body)
|
|
108
|
+
rescue InvalidResponseError
|
|
109
|
+
raise
|
|
110
|
+
rescue TransportError, TimeoutError
|
|
111
|
+
raise
|
|
112
|
+
rescue IOError, EOFError, SocketError, SystemCallError
|
|
113
|
+
raise TransportError, "Unable to reach ERPC", cause: nil
|
|
114
|
+
rescue ::Timeout::Error
|
|
115
|
+
raise
|
|
116
|
+
rescue StandardError
|
|
117
|
+
raise InvalidResponseError.new("ERPC returned an invalid Sui response"), cause: nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def decode_frames(body)
|
|
121
|
+
cursor = 0
|
|
122
|
+
data = nil
|
|
123
|
+
trailer = nil
|
|
124
|
+
until cursor == body.bytesize
|
|
125
|
+
raise_invalid! if body.bytesize - cursor < 5
|
|
126
|
+
marker = body.getbyte(cursor)
|
|
127
|
+
length = body.byteslice(cursor + 1, 4).unpack1("N")
|
|
128
|
+
cursor += 5
|
|
129
|
+
raise_invalid! if length > body.bytesize - cursor
|
|
130
|
+
payload = body.byteslice(cursor, length)
|
|
131
|
+
cursor += length
|
|
132
|
+
case marker
|
|
133
|
+
when 0x00
|
|
134
|
+
raise_invalid! unless data.nil? && trailer.nil?
|
|
135
|
+
data = payload
|
|
136
|
+
when 0x80
|
|
137
|
+
raise_invalid! unless !data.nil? && trailer.nil?
|
|
138
|
+
trailer = payload
|
|
139
|
+
else
|
|
140
|
+
raise_invalid!
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
raise_invalid! unless data && trailer
|
|
144
|
+
raise_invalid! if trailer.bytesize > MAX_TRAILER_BYTES
|
|
145
|
+
validate_trailer(trailer)
|
|
146
|
+
data
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_trailer(payload)
|
|
150
|
+
text = payload.dup.force_encoding(Encoding::UTF_8)
|
|
151
|
+
raise_invalid! unless text.valid_encoding? && text.end_with?("\r\n")
|
|
152
|
+
statuses = []
|
|
153
|
+
lines = text.split("\r\n", -1)
|
|
154
|
+
lines.pop
|
|
155
|
+
lines.each do |line|
|
|
156
|
+
name, separator, value = line.partition(":")
|
|
157
|
+
raise_invalid! if separator.empty? || name.empty? || !name.match?(/\A[a-z0-9!#\$%&'\*+\-.\^_`|~]+\z/)
|
|
158
|
+
raise_invalid! if value.match?(/[\x00-\x08\x0a\x0b\x0c\x0d\x0e-\x1f\x7f]/)
|
|
159
|
+
trimmed = value.sub(/\A[ \t]*/, "").sub(/[ \t]*\z/, "")
|
|
160
|
+
statuses << trimmed if name == "grpc-status"
|
|
161
|
+
end
|
|
162
|
+
raise_invalid! unless statuses.length == 1 && statuses.first == "0"
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def parse_service_info(payload)
|
|
167
|
+
fields = parse_message(payload, 0, { 1 => 2, 2 => 2, 3 => 0, 4 => 0, 5 => 2, 6 => 0, 7 => 0, 8 => 2 },
|
|
168
|
+
repeated_fields: [5], string_fields: [1, 2, 8])
|
|
169
|
+
required = [1, 2, 3, 4, 5, 6, 7]
|
|
170
|
+
raise_invalid! unless required.all? { |field| fields.key?(field) }
|
|
171
|
+
timestamp = parse_timestamp_fields(merge_embedded(fields.fetch(5), 1, { 1 => 0, 2 => 0 }))
|
|
172
|
+
SuiServiceInfo.new(
|
|
173
|
+
chain_id: string_field(fields.fetch(1)), chain: string_field(fields.fetch(2)), epoch: u64_field(fields.fetch(3)),
|
|
174
|
+
checkpoint_height: u64_field(fields.fetch(4)), timestamp_seconds: timestamp.fetch(:seconds), timestamp_nanos: timestamp.fetch(:nanos),
|
|
175
|
+
lowest_available_checkpoint: u64_field(fields.fetch(6)), lowest_available_checkpoint_objects: u64_field(fields.fetch(7)),
|
|
176
|
+
server: fields.key?(8) ? string_field(fields.fetch(8)) : nil
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_object_response(payload)
|
|
181
|
+
outer = parse_message(payload, 0, { 1 => 2 }, repeated_fields: [1]); raise_invalid! unless outer.key?(1)
|
|
182
|
+
fields = merge_embedded(outer.fetch(1), 1, { 2 => 2, 3 => 0, 4 => 2 }, string_fields: [2, 4])
|
|
183
|
+
required = [2, 3, 4]; raise_invalid! unless required.all? { |field| fields.key?(field) }
|
|
184
|
+
SuiObjectSummary.new(object_id: string_field(fields.fetch(2)), version: u64_field(fields.fetch(3)), digest: string_field(fields.fetch(4)))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def parse_balance_response(payload)
|
|
188
|
+
outer = parse_message(payload, 0, { 1 => 2 }, repeated_fields: [1]); raise_invalid! unless outer.key?(1)
|
|
189
|
+
fields = merge_embedded(outer.fetch(1), 1, { 1 => 2, 3 => 0, 4 => 0, 5 => 0 }, string_fields: [1])
|
|
190
|
+
required = [1, 3, 4, 5]; raise_invalid! unless required.all? { |field| fields.key?(field) }
|
|
191
|
+
balance = u64_field(fields.fetch(3)); address_balance = u64_field(fields.fetch(4)); coin_balance = u64_field(fields.fetch(5))
|
|
192
|
+
raise_invalid! unless address_balance <= MAX_U64 - coin_balance && address_balance + coin_balance == balance
|
|
193
|
+
SuiBalance.new(coin_type: string_field(fields.fetch(1)), balance: balance, address_balance: address_balance, coin_balance: coin_balance)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Protocol Buffers - Google's data interchange format
|
|
197
|
+
# Copyright 2008 Google Inc. All rights reserved.
|
|
198
|
+
# https://developers.google.com/protocol-buffers/
|
|
199
|
+
#
|
|
200
|
+
# Redistribution and use in source and binary forms, with or without
|
|
201
|
+
# modification, are permitted provided that the following conditions are
|
|
202
|
+
# met:
|
|
203
|
+
#
|
|
204
|
+
# * Redistributions of source code must retain the above copyright
|
|
205
|
+
# notice, this list of conditions and the following disclaimer.
|
|
206
|
+
# * Redistributions in binary form must reproduce the above
|
|
207
|
+
# copyright notice, this list of conditions and the following disclaimer
|
|
208
|
+
# in the documentation and/or other materials provided with the
|
|
209
|
+
# distribution.
|
|
210
|
+
# * Neither the name of Google Inc. nor the names of its
|
|
211
|
+
# contributors may be used to endorse or promote products derived from
|
|
212
|
+
# this software without specific prior written permission.
|
|
213
|
+
#
|
|
214
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
215
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
216
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
217
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
218
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
219
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
220
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
221
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
222
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
223
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
224
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
225
|
+
def parse_timestamp(payload, depth = 0)
|
|
226
|
+
fields = parse_message(payload, depth, { 1 => 0, 2 => 0 })
|
|
227
|
+
parse_timestamp_fields(fields)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def parse_timestamp_fields(fields)
|
|
231
|
+
seconds = fields.key?(1) ? signed_i64(last_scalar(fields.fetch(1))) : 0
|
|
232
|
+
nanos = fields.key?(2) ? signed_i32(last_scalar(fields.fetch(2))) : 0
|
|
233
|
+
raise_invalid! unless nanos.between?(0, 999_999_999)
|
|
234
|
+
{ seconds: seconds, nanos: nanos }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def parse_message(payload, depth = 0, schema = nil, repeated_fields: [], string_fields: [])
|
|
238
|
+
raise_invalid! if depth > MAX_RECURSION
|
|
239
|
+
fields = {}
|
|
240
|
+
index = 0
|
|
241
|
+
while index < payload.bytesize
|
|
242
|
+
key, index = read_varint(payload, index)
|
|
243
|
+
field = key >> 3; wire = key & 0x07
|
|
244
|
+
raise_invalid! if field.zero? || field > 536_870_911
|
|
245
|
+
raise_invalid! if schema && schema.key?(field) && schema.fetch(field) != wire
|
|
246
|
+
case wire
|
|
247
|
+
when 0
|
|
248
|
+
value, index = read_varint(payload, index); fields[field] = value
|
|
249
|
+
when 1
|
|
250
|
+
raise_invalid! if index + 8 > payload.bytesize
|
|
251
|
+
index += 8
|
|
252
|
+
when 2
|
|
253
|
+
length, index = read_varint(payload, index); raise_invalid! if length > payload.bytesize - index
|
|
254
|
+
value = payload.byteslice(index, length); index += length
|
|
255
|
+
validate_utf8_fragment(value) if string_fields.include?(field)
|
|
256
|
+
if repeated_fields.include?(field)
|
|
257
|
+
fields[field] ||= []
|
|
258
|
+
fields[field] << value
|
|
259
|
+
else
|
|
260
|
+
fields[field] = value
|
|
261
|
+
end
|
|
262
|
+
when 3
|
|
263
|
+
index = skip_group(payload, index, field, depth + 1)
|
|
264
|
+
when 4
|
|
265
|
+
raise_invalid!
|
|
266
|
+
when 5
|
|
267
|
+
raise_invalid! if index + 4 > payload.bytesize
|
|
268
|
+
index += 4
|
|
269
|
+
else
|
|
270
|
+
raise_invalid!
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
fields
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def merge_embedded(value, depth, schema, string_fields: [])
|
|
277
|
+
fragments = value.is_a?(Array) ? value : [value]
|
|
278
|
+
fragments.each_with_object({}) do |fragment, merged|
|
|
279
|
+
parsed = parse_message(fragment, depth, schema, string_fields: string_fields)
|
|
280
|
+
parsed.each { |field, field_value| merged[field] = field_value }
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def skip_group(payload, index, group_field, depth)
|
|
285
|
+
raise_invalid! if depth > MAX_RECURSION
|
|
286
|
+
while index < payload.bytesize
|
|
287
|
+
key, index = read_varint(payload, index); field = key >> 3; wire = key & 0x07
|
|
288
|
+
raise_invalid! if field.zero? || field > 536_870_911
|
|
289
|
+
return index if wire == 4 && field == group_field
|
|
290
|
+
case wire
|
|
291
|
+
when 0 then _value, index = read_varint(payload, index)
|
|
292
|
+
when 1 then raise_invalid! if index + 8 > payload.bytesize; index += 8
|
|
293
|
+
when 2 then length, index = read_varint(payload, index); raise_invalid! if length > payload.bytesize - index; index += length
|
|
294
|
+
when 3 then index = skip_group(payload, index, field, depth + 1)
|
|
295
|
+
when 5 then raise_invalid! if index + 4 > payload.bytesize; index += 4
|
|
296
|
+
else raise_invalid!
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
raise_invalid!
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def read_varint(payload, index)
|
|
303
|
+
value = 0; shift = 0; count = 0
|
|
304
|
+
loop do
|
|
305
|
+
raise_invalid! if index >= payload.bytesize || count >= 10
|
|
306
|
+
byte = payload.getbyte(index); index += 1; count += 1
|
|
307
|
+
if count == 10 && (byte & 0x7e) != 0 then raise_invalid! end
|
|
308
|
+
value |= (byte & 0x7f) << shift
|
|
309
|
+
return [value, index] if (byte & 0x80).zero?
|
|
310
|
+
shift += 7
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def signed_i64(value)
|
|
315
|
+
value > MAX_I64 ? value - (1 << 64) : value
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def signed_i32(value)
|
|
319
|
+
raw = value & 0xffff_ffff
|
|
320
|
+
raw >= 0x8000_0000 ? raw - (1 << 32) : raw
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def string_field(value)
|
|
324
|
+
value = last_scalar(value)
|
|
325
|
+
text = value.dup.force_encoding(Encoding::UTF_8)
|
|
326
|
+
raise_invalid! unless text.valid_encoding?
|
|
327
|
+
text
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def validate_utf8_fragment(value)
|
|
331
|
+
text = value.dup.force_encoding(Encoding::UTF_8)
|
|
332
|
+
raise_invalid! unless text.valid_encoding?
|
|
333
|
+
value
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def u64_field(value)
|
|
337
|
+
value = last_scalar(value)
|
|
338
|
+
raise_invalid! unless value.is_a?(Integer) && value.between?(0, MAX_U64)
|
|
339
|
+
value
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def last_scalar(value)
|
|
343
|
+
value.is_a?(Array) ? value.last : value
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def raise_invalid!
|
|
347
|
+
raise InvalidResponseError.new("ERPC returned an invalid Sui response"), cause: nil
|
|
348
|
+
end
|
|
349
|
+
private_class_method :raise_invalid!
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
SUI_GRPC_WEB_CONTENT_TYPE = "application/grpc-web+proto".freeze
|
|
353
|
+
|
|
354
|
+
SuiServiceInfo = Struct.new(:chain_id, :chain, :epoch, :checkpoint_height, :timestamp_seconds, :timestamp_nanos,
|
|
355
|
+
:lowest_available_checkpoint, :lowest_available_checkpoint_objects, :server, keyword_init: true) do
|
|
356
|
+
def initialize(**values); super; freeze; end
|
|
357
|
+
end
|
|
358
|
+
class SuiObjectSummary
|
|
359
|
+
attr_reader :version, :digest
|
|
360
|
+
def initialize(object_id:, version:, digest:)
|
|
361
|
+
@sui_object_id = object_id; @version = version; @digest = digest; freeze
|
|
362
|
+
end
|
|
363
|
+
def sui_object_id; @sui_object_id; end
|
|
364
|
+
end
|
|
365
|
+
SuiBalance = Struct.new(:coin_type, :balance, :address_balance, :coin_balance, keyword_init: true) do
|
|
366
|
+
def initialize(**values); super; freeze; end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private_constant :SuiProto
|
|
370
|
+
end
|
data/lib/erpc_sdk/transport.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "uri"
|
|
|
9
9
|
|
|
10
10
|
module ERPC
|
|
11
11
|
HttpResponse = Struct.new(:status, :body, keyword_init: true)
|
|
12
|
+
HttpStreamResponse = Struct.new(:status, :body, :close, keyword_init: true)
|
|
12
13
|
|
|
13
14
|
class NetHttpAdapter
|
|
14
15
|
def request(method:, url:, headers:, body: nil, timeout: DEFAULT_TIMEOUT)
|
|
@@ -44,6 +45,33 @@ module ERPC
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
# Execute a POST and yield the HTTP status plus an incremental body
|
|
49
|
+
# enumerator while Net::HTTP still owns the open response. Consumers must
|
|
50
|
+
# drain the enumerator inside the block; returning from the block closes
|
|
51
|
+
# the response and connection. This capability is intentionally additive;
|
|
52
|
+
# request and stream retain their released behavior.
|
|
53
|
+
def request_stream(method:, url:, headers:, body:, timeout: DEFAULT_TIMEOUT)
|
|
54
|
+
raise ArgumentError, "request_stream requires a block" unless block_given?
|
|
55
|
+
|
|
56
|
+
uri = URI.parse(url)
|
|
57
|
+
request = request_class(method).new(uri.request_uri, headers)
|
|
58
|
+
request.body = body unless body.nil?
|
|
59
|
+
result = nil
|
|
60
|
+
start(uri, timeout) do |http|
|
|
61
|
+
http.request(request) do |response|
|
|
62
|
+
chunks = Enumerator.new do |yielder|
|
|
63
|
+
response.read_body { |chunk| yielder << chunk }
|
|
64
|
+
end
|
|
65
|
+
result = yield(response.code.to_i, chunks, response.to_hash)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
result
|
|
69
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
|
|
70
|
+
raise TimeoutError, timeout, cause: nil
|
|
71
|
+
rescue IOError, EOFError, SocketError, SystemCallError, OpenSSL::SSL::SSLError
|
|
72
|
+
raise TransportError, "Unable to reach ERPC", cause: nil
|
|
73
|
+
end
|
|
74
|
+
|
|
47
75
|
private
|
|
48
76
|
|
|
49
77
|
def start(uri, timeout, &block)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module ERPC
|
|
6
|
+
TRON_TRANSPORT_FACTS = SealedEvmContract.facts_from_contract(RPC_NETWORK_CONTRACT, "client-tron-rpc")
|
|
7
|
+
TRON_RPC_METHODS = TRON_TRANSPORT_FACTS.method_names
|
|
8
|
+
private_constant :TRON_TRANSPORT_FACTS
|
|
9
|
+
|
|
10
|
+
class TronRpc < BaseRpc
|
|
11
|
+
alias_method :endpoint, :endpoint
|
|
12
|
+
alias_method :eth_chain_id, :eth_chain_id
|
|
13
|
+
alias_method :eth_get_balance, :eth_get_balance
|
|
14
|
+
alias_method :eth_call, :eth_call
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class TronClient
|
|
18
|
+
attr_reader :rpc
|
|
19
|
+
|
|
20
|
+
def initialize(transport)
|
|
21
|
+
@rpc = TronRpc.new(transport)
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Tron
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def create_client(transport)
|
|
30
|
+
TronClient.new(transport)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/erpc_sdk/version.rb
CHANGED