scale_rb 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82a0911194368a4fcefec7a414694346f087c0ded749d3865f6732f27c8afef0
4
- data.tar.gz: 14b34a748f835c527726ebd42440cced5ed8b5c0d954cb15c41e288ce3e5aff6
3
+ metadata.gz: 9857ea16c7327b90a598f40ce8afe919ee3db3a1fada67ba9e1a5ee768c50fdf
4
+ data.tar.gz: 7bd319b972c1c4aa13f4f24c6af525294bcbd8b6361423db66ac6038e44ad990
5
5
  SHA512:
6
- metadata.gz: 805efca604cc93794b5c1d4ee3dd1785d74d6a049b55f0ecc0503c85869c2759a37380b6735b8ecf874b6b33b31492f7b203870b7b414758740545821f98db19
7
- data.tar.gz: d5f577ec96adf7a51ef7044b0d5f8bc874e3a12daf66e3cc743677a6a095ddda909267b941dd15a06646dc941caeabe810fd960140c1c94261d173796ee40b9d
6
+ metadata.gz: db6452725e87b88cd0bdffe419588c8b9b8cfe892bdac01df226e9008bf02e45d56c9c25447a932d8ddf43bdfecf6ade0095710c9e056ee14ef2813f7b500b75
7
+ data.tar.gz: 2c6d01fd6726cc7ff5ed7c303b5e3c7ffe48347267a7b6989a6cda032741c47cfe339fa4c51871166b78f794a999cf8b0e6104cf44d013912bf5dbac0599cb45
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- scale_rb (0.1.14)
4
+ scale_rb (0.2.0)
5
5
  base58
6
6
  blake2b_rs (~> 0.1.4)
7
7
  xxhash
data/lib/address.rb CHANGED
@@ -1,105 +1,101 @@
1
1
  require 'base58'
2
2
 
3
3
  # Warning: Just for test
4
- class Address
5
- SS58_PREFIX = 'SS58PRE'
6
-
7
- TYPES = [
8
- # Polkadot Live (SS58, AccountId)
9
- 0, 1,
10
- # Polkadot Canary (SS58, AccountId)
11
- 2, 3,
12
- # Kulupu (SS58, Reserved)
13
- 16, 17,
14
- # Darwinia Live
15
- 18,
16
- # Dothereum (SS58, AccountId)
17
- 20, 21,
18
- # Generic Substrate wildcard (SS58, AccountId)
19
- 42, 43,
20
-
21
- # Schnorr/Ristretto 25519 ("S/R 25519") key
22
- 48,
23
- # Edwards Ed25519 key
24
- 49,
25
- # ECDSA SECP256k1 key
26
- 50,
27
-
28
- # Reserved for future address format extensions.
29
- *64..255
30
- ]
31
-
32
- class << self
33
- def array_to_hex_string(arr)
34
- body = arr.map { |i| i.to_s(16).rjust(2, '0') }.join
35
- "0x#{body}"
36
- end
37
-
38
- def decode(address, addr_type = 42, _ignore_checksum = true)
39
- decoded = Base58.base58_to_binary(address, :bitcoin)
40
- is_pubkey = decoded.size == 35
41
-
42
- size = decoded.size - (is_pubkey ? 2 : 1)
43
-
44
- prefix = decoded[0, 1].unpack1('C*')
45
-
46
- raise 'Invalid address type' unless TYPES.include?(addr_type)
47
-
48
- hash_bytes = make_hash(decoded[0, size])
49
- is_valid_checksum =
50
- if is_pubkey
51
- decoded[-2].unpack1('C*') == hash_bytes[0] && decoded[-1].unpack1('C*') == hash_bytes[1]
52
- else
53
- decoded[-1].unpack1('C*') == hash_bytes[0]
54
- end
55
-
56
- # raise "Invalid decoded address checksum" unless is_valid_checksum && ignore_checksum
57
-
58
- decoded[1...size].unpack1('H*')
59
- end
60
-
61
- def encode(pubkey, addr_type = 42)
62
- pubkey = pubkey[2..-1] if pubkey =~ /^0x/i
63
- key = [pubkey].pack('H*')
4
+ module ScaleRb
5
+ class Address
6
+ SS58_PREFIX = 'SS58PRE'
7
+
8
+ TYPES = [
9
+ # Polkadot Live (SS58, AccountId)
10
+ 0, 1,
11
+ # Polkadot Canary (SS58, AccountId)
12
+ 2, 3,
13
+ # Kulupu (SS58, Reserved)
14
+ 16, 17,
15
+ # Darwinia Live
16
+ 18,
17
+ # Dothereum (SS58, AccountId)
18
+ 20, 21,
19
+ # Generic Substrate wildcard (SS58, AccountId)
20
+ 42, 43,
21
+
22
+ # Schnorr/Ristretto 25519 ("S/R 25519") key
23
+ 48,
24
+ # Edwards Ed25519 key
25
+ 49,
26
+ # ECDSA SECP256k1 key
27
+ 50,
28
+
29
+ # Reserved for future address format extensions.
30
+ *64..255
31
+ ]
32
+
33
+ class << self
34
+ def decode(address, addr_type = 42, _ignore_checksum = true)
35
+ decoded = Base58.base58_to_binary(address, :bitcoin)
36
+ is_pubkey = decoded.size == 35
37
+
38
+ size = decoded.size - (is_pubkey ? 2 : 1)
39
+
40
+ prefix = decoded[0, 1].unpack1('C*')
41
+
42
+ raise 'Invalid address type' unless TYPES.include?(addr_type)
43
+
44
+ hash_bytes = make_hash(decoded[0, size])
45
+ is_valid_checksum =
46
+ if is_pubkey
47
+ decoded[-2].unpack1('C*') == hash_bytes[0] && decoded[-1].unpack1('C*') == hash_bytes[1]
48
+ else
49
+ decoded[-1].unpack1('C*') == hash_bytes[0]
50
+ end
51
+
52
+ # raise "Invalid decoded address checksum" unless is_valid_checksum && ignore_checksum
53
+
54
+ decoded[1...size].unpack1('H*')
55
+ end
64
56
 
65
- pubkey_bytes = key.bytes
57
+ def encode(pubkey, addr_type = 42)
58
+ pubkey = pubkey[2..-1] if pubkey =~ /^0x/i
59
+ key = [pubkey].pack('H*')
66
60
 
67
- checksum_length = case pubkey_bytes.length
68
- when 32, 33
69
- 2
70
- when 1, 2, 4, 8
71
- 1
72
- else
73
- raise 'Invalid pubkey length'
74
- end
61
+ pubkey_bytes = key.bytes
75
62
 
76
- ss58_format_bytes = if addr_type < 64
77
- [addr_type].pack('C*')
63
+ checksum_length = case pubkey_bytes.length
64
+ when 32, 33
65
+ 2
66
+ when 1, 2, 4, 8
67
+ 1
78
68
  else
79
- [
80
- ((ss58_format & 0b0000_0000_1111_1100) >> 2) | 0b0100_0000,
81
- (ss58_format >> 8) | ((ss58_format & 0b0000_0000_0000_0011) << 6)
82
- ].pack('C*')
69
+ raise 'Invalid pubkey length'
83
70
  end
84
71
 
85
- input_bytes = ss58_format_bytes.bytes + pubkey_bytes
86
- checksum = Blake2b.hex(SS58_PREFIX.bytes + input_bytes, 64).to_bytes
72
+ ss58_format_bytes = if addr_type < 64
73
+ [addr_type].pack('C*')
74
+ else
75
+ [
76
+ ((ss58_format & 0b0000_0000_1111_1100) >> 2) | 0b0100_0000,
77
+ (ss58_format >> 8) | ((ss58_format & 0b0000_0000_0000_0011) << 6)
78
+ ].pack('C*')
79
+ end
87
80
 
88
- Base58.binary_to_base58((input_bytes + checksum[0...checksum_length]).pack('C*'), :bitcoin)
89
- end
81
+ input_bytes = ss58_format_bytes.bytes + pubkey_bytes
82
+ checksum = Blake2b.hex(SS58_PREFIX.bytes + input_bytes, 64)._to_bytes
90
83
 
91
- def make_hash(body)
92
- Blake2b.hex("#{SS58_PREFIX}#{body}".bytes, 64)
93
- end
84
+ Base58.binary_to_base58((input_bytes + checksum[0...checksum_length]).pack('C*'), :bitcoin)
85
+ end
94
86
 
95
- def is_ss58_address?(address)
96
- begin
97
- decode(address)
98
- rescue StandardError
99
- return false
87
+ def make_hash(body)
88
+ Blake2b.hex("#{SS58_PREFIX}#{body}".bytes, 64)
89
+ end
90
+
91
+ def is_ss58_address?(address)
92
+ begin
93
+ decode(address)
94
+ rescue StandardError
95
+ return false
96
+ end
97
+ true
100
98
  end
101
- true
102
99
  end
103
100
  end
104
101
  end
105
-
@@ -1,100 +1,104 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class AbstractWsClient
4
- extend RpcRequestBuilder
5
- attr_accessor :metadata, :registry
6
-
7
- def initialize
8
- @id = 0
9
- @metadata = nil
10
- @registry = nil
11
- @callbacks = {}
12
- @subscription_callbacks = {}
13
- end
3
+ require_relative './rpc_request_builder'
4
+
5
+ module ScaleRb
6
+ class AbstractWsClient
7
+ extend RpcRequestBuilder
8
+ attr_accessor :metadata, :registry
9
+
10
+ def initialize
11
+ @id = 0
12
+ @metadata = nil
13
+ @registry = nil
14
+ @callbacks = {}
15
+ @subscription_callbacks = {}
16
+ end
14
17
 
15
- def send_json_rpc(_body)
16
- raise 'WsClient is a abstract base class for websocket client, please use its sub-class'
17
- end
18
+ def send_json_rpc(_body)
19
+ raise 'WsClient is a abstract base class for websocket client, please use its sub-class'
20
+ end
18
21
 
19
- # changes: [
20
- # [
21
- # "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7", # storage key
22
- # "0x0400000000000000d887690900000000020000" # change
23
- # ]
24
- # ]
25
- def process(resp)
26
- # handle id
27
- @callbacks[resp['id']]&.call(resp['id'], resp) if resp['id']
28
-
29
- # handle storage subscription
30
- return unless resp['params'] && resp['params']['subscription']
31
- return unless @metadata && @registry
32
-
33
- subscription = resp['params']['subscription']
34
- changes = resp['params']['result']['changes']
35
- block = resp['params']['result']['block']
36
- p "block: #{block}"
37
-
38
- return unless @subscription_callbacks[subscription]
39
-
40
- pallet_name, item_name, subscription_callback = @subscription_callbacks[subscription]
41
- storage_item = Metadata.get_storage_item(pallet_name, item_name, @metadata)
42
- storages = decode_storages(changes.map(&:last), storage_item, registry)
43
- subscription_callback.call(storages)
44
- end
22
+ # changes: [
23
+ # [
24
+ # "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7", # storage key
25
+ # "0x0400000000000000d887690900000000020000" # change
26
+ # ]
27
+ # ]
28
+ def process(resp)
29
+ # handle id
30
+ @callbacks[resp['id']]&.call(resp['id'], resp) if resp['id']
31
+
32
+ # handle storage subscription
33
+ return unless resp['params'] && resp['params']['subscription']
34
+ return unless @metadata && @registry
35
+
36
+ subscription = resp['params']['subscription']
37
+ changes = resp['params']['result']['changes']
38
+ block = resp['params']['result']['block']
39
+ p "block: #{block}"
40
+
41
+ return unless @subscription_callbacks[subscription]
42
+
43
+ pallet_name, item_name, subscription_callback = @subscription_callbacks[subscription]
44
+ storage_item = Metadata.get_storage_item(pallet_name, item_name, @metadata)
45
+ storages = decode_storages(changes.map(&:last), storage_item, registry)
46
+ subscription_callback.call(storages)
47
+ end
45
48
 
46
- def get_metadata(callback = nil)
47
- if callback.nil?
48
- callback = lambda do |id, resp|
49
- return unless resp['id'] && resp['result']
50
- return if resp['id'] != id
49
+ def get_metadata(callback = nil)
50
+ if callback.nil?
51
+ callback = lambda do |id, resp|
52
+ return unless resp['id'] && resp['result']
53
+ return if resp['id'] != id
51
54
 
52
- metadata_hex = resp['result']
53
- metadata = Metadata.decode_metadata(metadata_hex.strip.to_bytes)
54
- return unless metadata
55
+ metadata_hex = resp['result']
56
+ metadata = Metadata.decode_metadata(metadata_hex.strip._to_bytes)
57
+ return unless metadata
55
58
 
56
- @metadata = metadata
57
- @registry = Metadata.build_registry(@metadata)
59
+ @metadata = metadata
60
+ @registry = Metadata.build_registry(@metadata)
61
+ end
58
62
  end
59
- end
60
63
 
61
- id = bind_id_to(callback)
62
- body = state_getMetadata(id)
63
- send_json_rpc(body)
64
- end
64
+ id = bind_id_to(callback)
65
+ body = state_getMetadata(id)
66
+ send_json_rpc(body)
67
+ end
65
68
 
66
- def subscribe_storage(pallet_name, item_name, subscription_callback, key = nil, registry = nil)
67
- callback = create_callback_for_subscribe_storage(pallet_name, item_name, subscription_callback)
68
- id = bind_id_to(callback)
69
- body = derived_state_subscribe_storage(id, pallet_name, item_name, key, registry)
70
- send_json_rpc(body)
71
- end
69
+ def subscribe_storage(pallet_name, item_name, subscription_callback, key = nil, registry = nil)
70
+ callback = create_callback_for_subscribe_storage(pallet_name, item_name, subscription_callback)
71
+ id = bind_id_to(callback)
72
+ body = derived_state_subscribe_storage(id, pallet_name, item_name, key, registry)
73
+ send_json_rpc(body)
74
+ end
72
75
 
73
- private
76
+ private
74
77
 
75
- def bind_id_to(callback)
76
- @callbacks[@id] = callback
77
- old = @id
78
- @id += 1
79
- old
80
- end
78
+ def bind_id_to(callback)
79
+ @callbacks[@id] = callback
80
+ old = @id
81
+ @id += 1
82
+ old
83
+ end
81
84
 
82
- def decode_storages(datas, storage_item, registry)
83
- datas.map do |data|
84
- StorageHelper.decode_storage2(data, storage_item, registry)
85
+ def decode_storages(datas, storage_item, registry)
86
+ datas.map do |data|
87
+ StorageHelper.decode_storage2(data, storage_item, registry)
88
+ end
85
89
  end
86
- end
87
90
 
88
- def create_callback_for_subscribe_storage(pallet_name, item_name, subscription_callback)
89
- lambda do |id, resp|
90
- return unless resp['id'] && resp['result']
91
- return if resp['id'] != id
91
+ def create_callback_for_subscribe_storage(pallet_name, item_name, subscription_callback)
92
+ lambda do |id, resp|
93
+ return unless resp['id'] && resp['result']
94
+ return if resp['id'] != id
92
95
 
93
- @subscription_callbacks[resp['result']] = [
94
- pallet_name,
95
- item_name,
96
- subscription_callback
97
- ]
96
+ @subscription_callbacks[resp['result']] = [
97
+ pallet_name,
98
+ item_name,
99
+ subscription_callback
100
+ ]
101
+ end
98
102
  end
99
103
  end
100
104
  end
@@ -3,7 +3,9 @@
3
3
  require 'uri'
4
4
  require 'net/http'
5
5
  require 'json'
6
+ require_relative './rpc_request_builder'
6
7
  require_relative './http_client_metadata'
8
+ require_relative './http_client_storage'
7
9
 
8
10
  # TODO: method_name = cmd.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
9
11
  module ScaleRb
@@ -60,176 +62,6 @@ module ScaleRb
60
62
  result = json_rpc_call(url, 'rpc_methods', [])
61
63
  result['methods']
62
64
  end
63
-
64
- def get_metadata(url, at = nil)
65
- hex = state_getMetadata(url, at)
66
- Metadata.decode_metadata(hex.strip.to_bytes)
67
- end
68
-
69
- def query_storage_at(url, storage_keys, type_id, default, registry, at = nil)
70
- result = state_queryStorageAt(url, storage_keys, at)
71
- result.map do |item|
72
- item['changes'].map do |change|
73
- storage_key = change[0]
74
- data = change[1] || default
75
- storage = data.nil? ? nil : PortableCodec.decode(type_id, data.to_bytes, registry)[0]
76
- { storage_key: storage_key, storage: storage }
77
- end
78
- end.flatten
79
- end
80
-
81
- def get_storage_keys_by_partial_key(url, partial_storage_key, start_key = nil, at = nil)
82
- storage_keys = state_getKeysPaged(url, partial_storage_key, 1000, start_key, at)
83
- if storage_keys.length == 1000
84
- storage_keys + get_storage_keys_by_partial_key(url, partial_storage_key, storage_keys.last, at)
85
- else
86
- storage_keys
87
- end
88
- end
89
-
90
- def get_storages_by_partial_key(url, partial_storage_key, type_id_of_value, default, registry, at = nil)
91
- storage_keys = get_storage_keys_by_partial_key(url, partial_storage_key, partial_storage_key, at)
92
- storage_keys.each_slice(250).map do |slice|
93
- query_storage_at(
94
- url,
95
- slice,
96
- type_id_of_value,
97
- default,
98
- registry,
99
- at
100
- )
101
- end.flatten
102
- end
103
-
104
- # 1. Plain
105
- # key: nil
106
- # value: { type: 3, modifier: 'Default', callback: '' }
107
- #
108
- # 2. Map
109
- # key: { value: value, type: 0, hashers: ['Blake2128Concat'] }
110
- # value: { type: 3, modifier: 'Default', callback: '' }
111
- #
112
- # 3. Map, but key.value is nil
113
- # key: { value: nil, type: 0, hashers: ['Blake2128Concat'] }
114
- # value: { type: 3, modifier: 'Default', callback: '' }
115
- #
116
- # example:
117
- # 'System',
118
- # 'Account',
119
- # key = {
120
- # value: [['0x724d50824542b56f422588421643c4a162b90b5416ef063f2266a1eae6651641'.to_bytes]], # [AccountId]
121
- # type: 0,
122
- # hashers: ['Blake2128Concat']
123
- # },
124
- # value = {
125
- # type: 3,
126
- # modifier: 'Default',
127
- # callback: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
128
- # },
129
- # ..
130
- #
131
- # TODO: part of the key is provided, but not all
132
- def get_storage(url, pallet_name, item_name, key, value, registry, at = nil)
133
- if key
134
- if key[:value].nil? || key[:value].empty?
135
- # map, but no key's value provided. get all storages under the partial storage key
136
- partial_storage_key = StorageHelper.encode_storage_key(pallet_name, item_name).to_hex
137
- get_storages_by_partial_key(
138
- url,
139
- partial_storage_key,
140
- value[:type],
141
- value[:modifier] == 'Default' ? value[:fallback] : nil,
142
- registry,
143
- at
144
- )
145
- elsif key[:value].length != key[:hashers].length
146
- # map with multi part, but only provide part value
147
- partial_storage_key = StorageHelper.encode_storage_key(pallet_name, item_name, key, registry).to_hex
148
- get_storages_by_partial_key(
149
- url,
150
- partial_storage_key,
151
- value[:type],
152
- value[:modifier] == 'Default' ? value[:fallback] : nil,
153
- registry,
154
- at
155
- )
156
- end
157
- else
158
- storage_key = StorageHelper.encode_storage_key(pallet_name, item_name, key, registry).to_hex
159
- data = state_getStorage(url, storage_key, at)
160
- StorageHelper.decode_storage(data, value[:type], value[:modifier] == 'Optional', value[:fallback], registry)
161
- end
162
- end
163
-
164
- def get_storage2(url, pallet_name, item_name, value_of_key, metadata, at = nil)
165
- raise 'Metadata should not be nil' if metadata.nil?
166
-
167
- registry = Metadata.build_registry(metadata)
168
- item = Metadata.get_storage_item(pallet_name, item_name, metadata)
169
- raise "No such storage item: `#{pallet_name}`.`#{item_name}`" if item.nil?
170
-
171
- modifier = item._get(:modifier) # Default | Optional
172
- fallback = item._get(:fallback)
173
- type = item._get(:type)
174
-
175
- plain = type._get(:plain)
176
- map = type._get(:map)
177
- # debug
178
-
179
- key, value =
180
- if plain
181
- [
182
- nil,
183
- { type: plain, modifier: modifier, fallback: fallback }
184
- ]
185
- elsif map
186
- [
187
- { value: value_of_key, type: map._get(:key), hashers: map._get(:hashers) },
188
- { type: map._get(:value), modifier: modifier, fallback: fallback }
189
- ]
190
- else
191
- raise 'NoSuchStorageType'
192
- end
193
- get_storage(url, pallet_name, item_name, key, value, registry, at)
194
- end
195
-
196
- # get_storage3 is a more ruby style function
197
- #
198
- # pallet_name and storage_name is pascal style like 'darwinia_staking'
199
- def get_storage3(url, metadata, pallet_name, storage_name, key_part1: nil, key_part2: nil, at: nil)
200
- pallet_name = to_pascal pallet_name
201
- storage_name = to_pascal storage_name
202
- ScaleRb.logger.debug "#{pallet_name}.#{storage_name}(#{[key_part1, key_part2].compact.join(', ')})"
203
-
204
- key = [key_part1, key_part2].compact.map { |part_of_key| c(part_of_key) }
205
- ScaleRb.logger.debug "converted key: #{key}"
206
-
207
- get_storage2(
208
- url,
209
- pallet_name,
210
- storage_name,
211
- key,
212
- metadata,
213
- at
214
- )
215
- end
216
-
217
- private
218
-
219
- def to_pascal(str)
220
- str.split('_').collect(&:capitalize).join
221
- end
222
-
223
- # convert key to byte array
224
- def c(key)
225
- if key.start_with?('0x')
226
- key.to_bytes
227
- elsif key.to_i.to_s == key # check if key is a number
228
- key.to_i
229
- else
230
- key
231
- end
232
- end
233
65
  end
234
66
  end
235
67
  end
@@ -4,14 +4,16 @@ require 'fileutils'
4
4
  module ScaleRb
5
5
  module HttpClient
6
6
  class << self
7
+ def get_metadata(url, at = nil)
8
+ hex = state_getMetadata(url, at)
9
+ Metadata.decode_metadata(hex.strip._to_bytes)
10
+ end
11
+
7
12
  # cached version of get_metadata
8
13
  # get metadata from cache first
9
- def get_metadata_cached(url, at: nil, dir: File.join(Dir.pwd, 'metadata'))
10
- # if at
11
- # require_block_hash_correct(url, at)
12
- # else
13
- # at = ScaleRb::HttpClient.chain_getFinalizedHead(url)
14
- # end
14
+ def get_metadata_cached(url, at: nil, dir: nil)
15
+ dir = ENV['SCALE_RB_METADATA_DIR'] || File.join(Dir.pwd, 'metadata') if dir.nil?
16
+
15
17
  at = ScaleRb::HttpClient.chain_getFinalizedHead(url) if at.nil?
16
18
  spec_name, spec_version = get_spec(url, at)
17
19
 
@@ -27,7 +29,7 @@ module ScaleRb
27
29
  metadata = ScaleRb::HttpClient.get_metadata(url, at)
28
30
 
29
31
  # cache it
30
- puts "caching metadata `#{spec_name}_#{spec_version}.json`"
32
+ ScaleRb.logger.debug "caching metadata `#{spec_name}_#{spec_version}.json`"
31
33
  save_metadata_to_file(
32
34
  spec_name: spec_name,
33
35
  spec_version: spec_version,
@@ -54,7 +56,7 @@ module ScaleRb
54
56
  file_path = File.join(dir, "#{spec_name}_#{spec_version}.json")
55
57
  return unless File.exist?(file_path)
56
58
 
57
- puts "found metadata `#{spec_name}_#{spec_version}.json` in cache"
59
+ ScaleRb.logger.debug "found metadata `#{spec_name}_#{spec_version}.json` in cache"
58
60
  JSON.parse(File.read(file_path))
59
61
  end
60
62