solana-ruby-kit 8.2.0 → 8.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 +4 -4
- data/README.md +164 -12
- data/lib/solana/ruby/kit/accounts/maybe_account.rb +1 -1
- data/lib/solana/ruby/kit/addresses/address.rb +1 -1
- data/lib/solana/ruby/kit/addresses/curve.rb +1 -1
- data/lib/solana/ruby/kit/addresses/program_derived_address.rb +1 -1
- data/lib/solana/ruby/kit/addresses/public_key.rb +1 -1
- data/lib/solana/ruby/kit/codecs/codec.rb +115 -5
- data/lib/solana/ruby/kit/codecs/data_structures.rb +246 -47
- data/lib/solana/ruby/kit/codecs/numbers.rb +96 -61
- data/lib/solana/ruby/kit/codecs/strings.rb +132 -6
- data/lib/solana/ruby/kit/encoding/base58.rb +1 -1
- data/lib/solana/ruby/kit/errors.rb +18 -1
- data/lib/solana/ruby/kit/fast_stable_stringify.rb +1 -1
- data/lib/solana/ruby/kit/functional.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/instruction_plan.rb +31 -26
- data/lib/solana/ruby/kit/instruction_plans/message_packer_errors.rb +131 -0
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb +2 -2
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_planner.rb +11 -14
- data/lib/solana/ruby/kit/instruction_plans.rb +1 -1
- data/lib/solana/ruby/kit/instructions/accounts.rb +1 -1
- data/lib/solana/ruby/kit/instructions/instruction.rb +1 -1
- data/lib/solana/ruby/kit/instructions/roles.rb +1 -1
- data/lib/solana/ruby/kit/keys/key_pair.rb +1 -1
- data/lib/solana/ruby/kit/keys/private_key.rb +1 -1
- data/lib/solana/ruby/kit/keys/public_key.rb +1 -1
- data/lib/solana/ruby/kit/keys/signatures.rb +1 -1
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +1 -1
- data/lib/solana/ruby/kit/offchain_messages/message_v1.rb +1 -1
- data/lib/solana/ruby/kit/options/option.rb +1 -1
- data/lib/solana/ruby/kit/plugin_core.rb +1 -1
- data/lib/solana/ruby/kit/programs/associated_token_account.rb +1 -1
- data/lib/solana/ruby/kit/programs/stake_program.rb +1 -1
- data/lib/solana/ruby/kit/programs/system_program.rb +1 -1
- data/lib/solana/ruby/kit/programs.rb +1 -1
- data/lib/solana/ruby/kit/promises.rb +1 -1
- data/lib/solana/ruby/kit/resource_limit_estimation.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/commitment.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/lamports.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/sol.rb +1 -1
- data/lib/solana/ruby/kit/signers/keypair_signer.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/async_iterable.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/bridge_store_to_async_iterable.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/reactive_action_store.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/reactive_stream_store.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/clock.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/rent.rb +1 -1
- data/lib/solana/ruby/kit/sysvars.rb +1 -1
- data/lib/solana/ruby/kit/transaction_confirmation.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/compiled_transaction_message.rb +8 -7
- data/lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/get_instructions.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/walk_instructions.rb +1 -1
- data/lib/solana/ruby/kit/transaction_messages/compute_budget.rb +1 -1
- data/lib/solana/ruby/kit/transaction_messages/resource_limit_validation.rb +1 -1
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +1 -1
- data/lib/solana/ruby/kit/transactions/compiler.rb +2 -2
- data/lib/solana/ruby/kit/transactions/transaction.rb +30 -10
- data/lib/solana/ruby/kit/version.rb +1 -1
- data/lib/solana/ruby/kit/wallet_standard.rb +10 -9
- data/solana-ruby-kit.gemspec +1 -1
- metadata +6 -6
- data/lib/solana/ruby/kit/instruction_plans/max_instructions.rb +0 -59
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
|
|
4
4
|
module Solana::Ruby::Kit
|
|
5
5
|
module Codecs
|
|
6
|
+
# A size strategy for +array_codec+ / +map_codec+ / +set_codec+ where the
|
|
7
|
+
# collection ends when the bytes at the next item position match a constant
|
|
8
|
+
# +sentinel+. Mirrors upstream's `ArrayLikeCodecSentinelSize`
|
|
9
|
+
# (`{ __kind: 'sentinel', sentinel, strategy? }`), itself a mirror of
|
|
10
|
+
# Codama's `sentinelCountNode`.
|
|
11
|
+
#
|
|
12
|
+
# Unlike a sentinel searched for in the byte stream, this one is compared at
|
|
13
|
+
# item boundaries only, so its bytes may occur *inside* an item without
|
|
14
|
+
# terminating the collection.
|
|
15
|
+
#
|
|
16
|
+
# +strategy+ controls whether the sentinel is written and required:
|
|
17
|
+
# - +:required+ (default) - written after the last item; decoding raises
|
|
18
|
+
# +CODECS__SENTINEL_MISSING_AT_END_OF_BYTES+ if the bytes run out first.
|
|
19
|
+
# - +:optional+ - written, but decoding also stops at the end of the bytes,
|
|
20
|
+
# to tolerate tightly sized or legacy data that lacks it.
|
|
21
|
+
# - +:omitted+ - never written; decoding stops at the end of the bytes, and
|
|
22
|
+
# consumes the sentinel if one happens to be there.
|
|
23
|
+
#
|
|
24
|
+
# Two invariants must hold for a round trip, and - as upstream - the codec
|
|
25
|
+
# does NOT enforce them:
|
|
26
|
+
# 1. No item may *begin* with the sentinel's bytes; decoding cannot tell
|
|
27
|
+
# such an item from the terminator and stops early.
|
|
28
|
+
# 2. Under +:optional+ / +:omitted+ the sentinel must be no wider than the
|
|
29
|
+
# smallest possible item, or a short trailing item is silently dropped:
|
|
30
|
+
# decoding stops once fewer bytes than the sentinel remain.
|
|
31
|
+
class SentinelSize < T::Struct
|
|
32
|
+
STRATEGIES = T.let(%i[required optional omitted].freeze, T::Array[Symbol])
|
|
33
|
+
|
|
34
|
+
const :sentinel, String
|
|
35
|
+
const :strategy, Symbol, default: :required
|
|
36
|
+
end
|
|
37
|
+
|
|
6
38
|
# Data-structure codecs — mirrors @solana/codecs-data-structures.
|
|
7
39
|
module DataStructures
|
|
8
40
|
extend T::Sig
|
|
@@ -15,7 +47,8 @@ module Solana::Ruby::Kit
|
|
|
15
47
|
|
|
16
48
|
# Encode/decode a fixed ordered list of named fields.
|
|
17
49
|
# +fields+ is an Array of [name, codec] pairs (name can be String or Symbol).
|
|
18
|
-
#
|
|
50
|
+
# Decodes to a Hash with Symbol keys. Encode takes a Hash keyed by either
|
|
51
|
+
# form, whichever way the field was named, so a decoded value re-encodes.
|
|
19
52
|
sig { params(fields: T::Array[[T.any(String, Symbol), Codec]]).returns(Codec) }
|
|
20
53
|
def struct_codec(fields)
|
|
21
54
|
fixed = fields.all? { |_, c| c.fixed_size }
|
|
@@ -23,7 +56,11 @@ module Solana::Ruby::Kit
|
|
|
23
56
|
|
|
24
57
|
enc = Encoder.new(fixed_size: total) do |value|
|
|
25
58
|
h = T.cast(value, T::Hash[T.untyped, T.untyped])
|
|
26
|
-
fields.map
|
|
59
|
+
fields.map do |name, codec|
|
|
60
|
+
# key? rather than `h[name] || ...`, which would skip a false value.
|
|
61
|
+
key = [name, name.to_sym, name.to_s].find { |k| h.key?(k) }
|
|
62
|
+
codec.encode(key.nil? ? nil : h[key])
|
|
63
|
+
end.join.b
|
|
27
64
|
end
|
|
28
65
|
dec = Decoder.new(fixed_size: total) do |bytes, offset|
|
|
29
66
|
result = {}
|
|
@@ -61,55 +98,69 @@ module Solana::Ruby::Kit
|
|
|
61
98
|
Codec.new(enc, dec)
|
|
62
99
|
end
|
|
63
100
|
|
|
64
|
-
# Encode/decode
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
# Encode/decode an array, one +element_codec+ value per item. +size+
|
|
102
|
+
# picks how the item count is stored - mirrors upstream's
|
|
103
|
+
# `ArrayLikeCodecSize`:
|
|
104
|
+
#
|
|
105
|
+
# - +nil+ (default): a u32 little-endian count prefix.
|
|
106
|
+
# - a Codec, e.g. +u16_codec+ or +compact_u16_codec+: the count prefix is
|
|
107
|
+
# written and read with that codec instead.
|
|
108
|
+
# - an Integer: a fixed item count with no prefix. Encoding an array of
|
|
109
|
+
# any other length raises +CODECS__INVALID_NUMBER_OF_ITEMS+.
|
|
110
|
+
# - +:remainder+: no prefix; decoding reads items until the bytes run out,
|
|
111
|
+
# so the array must be the last thing in the buffer.
|
|
112
|
+
# - a SentinelSize: no prefix; the array ends at a sentinel. See
|
|
113
|
+
# SentinelSize for the strategies and the invariants you must uphold.
|
|
114
|
+
#
|
|
115
|
+
# When the count is a prefix and there are not enough bytes left to read
|
|
116
|
+
# it, the decoder yields an empty array rather than failing. That is
|
|
117
|
+
# deliberate: it lets a program append a collection to an existing
|
|
118
|
+
# account layout and still decode accounts written before the change.
|
|
119
|
+
# Formats that cannot accept that leniency - borsh, for one, requires the
|
|
120
|
+
# prefix to be present - can pass +require_size_prefix: true+ to make a
|
|
121
|
+
# truncated buffer raise instead. The option has no effect on the other
|
|
122
|
+
# strategies, none of which carries a prefix.
|
|
123
|
+
#
|
|
124
|
+
# +description+ names the codec in errors; it defaults to +'array'+.
|
|
125
|
+
sig do
|
|
126
|
+
params(
|
|
127
|
+
element_codec: Codec,
|
|
128
|
+
size: T.nilable(T.any(Integer, Symbol, Codec, SentinelSize)),
|
|
129
|
+
require_size_prefix: T::Boolean,
|
|
130
|
+
description: T.nilable(String)
|
|
131
|
+
).returns(Codec)
|
|
132
|
+
end
|
|
133
|
+
def array_codec(element_codec, size: nil, require_size_prefix: false, description: nil)
|
|
134
|
+
description ||= 'array'
|
|
135
|
+
case size
|
|
136
|
+
when nil then ArrayLikeSize.prefixed(element_codec, Numbers.u32_codec, require_size_prefix)
|
|
137
|
+
when Codec then ArrayLikeSize.prefixed(element_codec, size, require_size_prefix)
|
|
138
|
+
when Integer then ArrayLikeSize.fixed_count(element_codec, size, description)
|
|
139
|
+
when SentinelSize then ArrayLikeSize.sentinel(element_codec, size, description)
|
|
140
|
+
when :remainder then ArrayLikeSize.remainder(element_codec)
|
|
84
141
|
else
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
header = prefix.encode(arr.length)
|
|
89
|
-
body = arr.map { |v| element_codec.encode(v) }.join.b
|
|
90
|
-
header + body
|
|
91
|
-
end
|
|
92
|
-
dec = Decoder.new do |bytes, offset|
|
|
93
|
-
len, prefix_bytes = prefix.decode(bytes, offset: offset)
|
|
94
|
-
result = []
|
|
95
|
-
consumed = prefix_bytes
|
|
96
|
-
len.times do
|
|
97
|
-
val, n = element_codec.decode(bytes, offset: offset + consumed)
|
|
98
|
-
result << val
|
|
99
|
-
consumed += n
|
|
100
|
-
end
|
|
101
|
-
[result, consumed]
|
|
102
|
-
end
|
|
103
|
-
Codec.new(enc, dec)
|
|
142
|
+
Kernel.raise ArgumentError,
|
|
143
|
+
"Unknown array size #{size.inspect}; expected nil, a number Codec, an Integer, " \
|
|
144
|
+
':remainder or a SentinelSize'
|
|
104
145
|
end
|
|
105
146
|
end
|
|
106
147
|
|
|
107
148
|
# Encode/decode a Hash.
|
|
108
149
|
# Encoded as: [length prefix] + [key, value, key, value, ...]
|
|
109
|
-
|
|
110
|
-
|
|
150
|
+
# See +array_codec+ for what +size+ and +require_size_prefix+ do; +size+
|
|
151
|
+
# counts entries, and a SentinelSize is compared at entry (key)
|
|
152
|
+
# boundaries.
|
|
153
|
+
sig do
|
|
154
|
+
params(
|
|
155
|
+
key_codec: Codec,
|
|
156
|
+
value_codec: Codec,
|
|
157
|
+
size: T.nilable(T.any(Integer, Symbol, Codec, SentinelSize)),
|
|
158
|
+
require_size_prefix: T::Boolean
|
|
159
|
+
).returns(Codec)
|
|
160
|
+
end
|
|
161
|
+
def map_codec(key_codec, value_codec, size: nil, require_size_prefix: false)
|
|
111
162
|
pair_codec = tuple_codec([key_codec, value_codec])
|
|
112
|
-
array_codec(pair_codec, size: size).transform_decoder do |pairs|
|
|
163
|
+
array_codec(pair_codec, size: size, require_size_prefix: require_size_prefix).transform_decoder do |pairs|
|
|
113
164
|
pairs.each_with_object({}) { |(k, v), h| h[k] = v }
|
|
114
165
|
end.transform_encoder do |hash|
|
|
115
166
|
T.cast(hash, T::Hash[T.untyped, T.untyped]).map { |k, v| [k, v] }
|
|
@@ -117,9 +168,16 @@ module Solana::Ruby::Kit
|
|
|
117
168
|
end
|
|
118
169
|
|
|
119
170
|
# Encode/decode a Set (stored as an array of unique elements).
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
# See +array_codec+ for what +size+ and +require_size_prefix+ do.
|
|
172
|
+
sig do
|
|
173
|
+
params(
|
|
174
|
+
element_codec: Codec,
|
|
175
|
+
size: T.nilable(T.any(Integer, Symbol, Codec, SentinelSize)),
|
|
176
|
+
require_size_prefix: T::Boolean
|
|
177
|
+
).returns(Codec)
|
|
178
|
+
end
|
|
179
|
+
def set_codec(element_codec, size: nil, require_size_prefix: false)
|
|
180
|
+
array_codec(element_codec, size: size, require_size_prefix: require_size_prefix)
|
|
123
181
|
.transform_encoder { |s| T.cast(s, T::Set[T.untyped]).to_a }
|
|
124
182
|
.transform_decoder { |arr| Set.new(arr) }
|
|
125
183
|
end
|
|
@@ -177,5 +235,146 @@ module Solana::Ruby::Kit
|
|
|
177
235
|
Codec.new(enc, dec)
|
|
178
236
|
end
|
|
179
237
|
end
|
|
238
|
+
|
|
239
|
+
# The per-strategy builders behind +array_codec+ (and so +map_codec+ and
|
|
240
|
+
# +set_codec+). Call those instead. Kept out of DataStructures on purpose:
|
|
241
|
+
# everything there is re-exported as a public +Codecs.*+ helper, and these
|
|
242
|
+
# are an implementation detail.
|
|
243
|
+
module ArrayLikeSize
|
|
244
|
+
extend T::Sig
|
|
245
|
+
extend self
|
|
246
|
+
|
|
247
|
+
# A count prefix written and read with +prefix+.
|
|
248
|
+
sig { params(element_codec: Codec, prefix: Codec, require_size_prefix: T::Boolean).returns(Codec) }
|
|
249
|
+
def prefixed(element_codec, prefix, require_size_prefix)
|
|
250
|
+
enc = Encoder.new do |values|
|
|
251
|
+
arr = T.cast(values, T::Array[T.untyped])
|
|
252
|
+
prefix.encode(arr.length) + arr.map { |v| element_codec.encode(v) }.join.b
|
|
253
|
+
end
|
|
254
|
+
dec = Decoder.new do |bytes, offset|
|
|
255
|
+
# A fixed-size prefix needs all its bytes; a variable-size one (e.g.
|
|
256
|
+
# compact_u16) needs at least one, and raises for itself if the rest
|
|
257
|
+
# are missing.
|
|
258
|
+
prefix_size = prefix.fixed_size || 1
|
|
259
|
+
remaining = [bytes.bytesize - offset, 0].max
|
|
260
|
+
if remaining < prefix_size
|
|
261
|
+
# The prefix is missing or truncated. By default that decodes to an
|
|
262
|
+
# empty collection having consumed nothing; under
|
|
263
|
+
# +require_size_prefix+ it is an error.
|
|
264
|
+
Kernel.raise SolanaError.new(
|
|
265
|
+
SolanaError::CODECS__INVALID_BYTE_LENGTH,
|
|
266
|
+
{ expected: prefix_size, actual: remaining }
|
|
267
|
+
) if require_size_prefix
|
|
268
|
+
|
|
269
|
+
next [[], 0]
|
|
270
|
+
end
|
|
271
|
+
count, consumed = prefix.decode(bytes, offset: offset)
|
|
272
|
+
result = Array.new(Kernel.Integer(count)) do
|
|
273
|
+
val, n = element_codec.decode(bytes, offset: offset + consumed)
|
|
274
|
+
consumed += n
|
|
275
|
+
val
|
|
276
|
+
end
|
|
277
|
+
[result, consumed]
|
|
278
|
+
end
|
|
279
|
+
Codec.new(enc, dec)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Exactly +count+ items and no prefix. Fixed-size when the items are - and
|
|
283
|
+
# always for a count of zero, whatever the item.
|
|
284
|
+
sig { params(element_codec: Codec, count: Integer, description: String).returns(Codec) }
|
|
285
|
+
def fixed_count(element_codec, count, description)
|
|
286
|
+
item_size = element_codec.fixed_size
|
|
287
|
+
fixed = count.zero? ? 0 : (item_size && count * item_size)
|
|
288
|
+
enc = Encoder.new(fixed_size: fixed) do |values|
|
|
289
|
+
arr = T.cast(values, T::Array[T.untyped])
|
|
290
|
+
unless arr.length == count
|
|
291
|
+
Kernel.raise SolanaError.new(
|
|
292
|
+
SolanaError::CODECS__INVALID_NUMBER_OF_ITEMS,
|
|
293
|
+
{ codec_description: description, expected: count, actual: arr.length }
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
arr.map { |v| element_codec.encode(v) }.join.b
|
|
297
|
+
end
|
|
298
|
+
dec = Decoder.new(fixed_size: fixed) do |bytes, offset|
|
|
299
|
+
consumed = 0
|
|
300
|
+
result = Array.new(count) do
|
|
301
|
+
val, n = element_codec.decode(bytes, offset: offset + consumed)
|
|
302
|
+
consumed += n
|
|
303
|
+
val
|
|
304
|
+
end
|
|
305
|
+
[result, consumed]
|
|
306
|
+
end
|
|
307
|
+
Codec.new(enc, dec)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# No prefix; decoding reads items until the bytes run out.
|
|
311
|
+
sig { params(element_codec: Codec).returns(Codec) }
|
|
312
|
+
def remainder(element_codec)
|
|
313
|
+
enc = Encoder.new do |values|
|
|
314
|
+
T.cast(values, T::Array[T.untyped]).map { |v| element_codec.encode(v) }.join.b
|
|
315
|
+
end
|
|
316
|
+
dec = Decoder.new do |bytes, offset|
|
|
317
|
+
result = []
|
|
318
|
+
consumed = 0
|
|
319
|
+
while offset + consumed < bytes.bytesize
|
|
320
|
+
val, n = element_codec.decode(bytes, offset: offset + consumed)
|
|
321
|
+
# An item that consumes nothing would never reach the end.
|
|
322
|
+
Kernel.raise ArgumentError, 'a :remainder array item decoded zero bytes' if n.zero?
|
|
323
|
+
|
|
324
|
+
result << val
|
|
325
|
+
consumed += n
|
|
326
|
+
end
|
|
327
|
+
[result, consumed]
|
|
328
|
+
end
|
|
329
|
+
Codec.new(enc, dec)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Ends at a sentinel instead of carrying a count. Always variable-size:
|
|
333
|
+
# the item count is only known by reading up to the sentinel.
|
|
334
|
+
sig { params(element_codec: Codec, size: SentinelSize, description: String).returns(Codec) }
|
|
335
|
+
def sentinel(element_codec, size, description)
|
|
336
|
+
sentinel = size.sentinel.b
|
|
337
|
+
strategy = size.strategy
|
|
338
|
+
# Raised at construction, as upstream does for both the encoder and the
|
|
339
|
+
# decoder: an empty sentinel matches everywhere and delimits nothing.
|
|
340
|
+
Kernel.raise SolanaError.new(SolanaError::CODECS__SENTINEL_MUST_NOT_BE_EMPTY) if sentinel.empty?
|
|
341
|
+
unless SentinelSize::STRATEGIES.include?(strategy)
|
|
342
|
+
Kernel.raise ArgumentError,
|
|
343
|
+
"Unknown sentinel strategy #{strategy.inspect}; expected one of #{SentinelSize::STRATEGIES.inspect}"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
enc = Encoder.new do |values|
|
|
347
|
+
body = T.cast(values, T::Array[T.untyped]).map { |v| element_codec.encode(v) }.join.b
|
|
348
|
+
strategy == :omitted ? body : body + sentinel
|
|
349
|
+
end
|
|
350
|
+
dec = Decoder.new do |bytes, offset|
|
|
351
|
+
result = []
|
|
352
|
+
consumed = 0
|
|
353
|
+
Kernel.loop do
|
|
354
|
+
position = offset + consumed
|
|
355
|
+
if position + sentinel.bytesize > bytes.bytesize
|
|
356
|
+
# Not enough bytes remain to hold the sentinel.
|
|
357
|
+
if strategy == :required
|
|
358
|
+
Kernel.raise SolanaError.new(
|
|
359
|
+
SolanaError::CODECS__SENTINEL_MISSING_AT_END_OF_BYTES,
|
|
360
|
+
{ codec_description: description, hex_sentinel: sentinel.unpack1('H*'), sentinel: sentinel }
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
break
|
|
364
|
+
end
|
|
365
|
+
if Bytes.contains_bytes?(bytes, sentinel, offset: position)
|
|
366
|
+
# The sentinel is present; consume it and stop.
|
|
367
|
+
consumed += sentinel.bytesize
|
|
368
|
+
break
|
|
369
|
+
end
|
|
370
|
+
val, n = element_codec.decode(bytes, offset: position)
|
|
371
|
+
result << val
|
|
372
|
+
consumed += n
|
|
373
|
+
end
|
|
374
|
+
[result, consumed]
|
|
375
|
+
end
|
|
376
|
+
Codec.new(enc, dec)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
180
379
|
end
|
|
181
380
|
end
|
|
@@ -19,6 +19,43 @@ module Solana::Ruby::Kit
|
|
|
19
19
|
# defeated the "directly available as Codecs.x" intent in codecs.rb.
|
|
20
20
|
extend self
|
|
21
21
|
|
|
22
|
+
# ── Multi-byte integers ──────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
# Shared implementation behind u128 / i128 / u256 / i256.
|
|
25
|
+
#
|
|
26
|
+
# Upstream splits these values into 64-bit words because a JS `DataView`
|
|
27
|
+
# has no accessor wider than 64 bits. Ruby's Integer is arbitrary
|
|
28
|
+
# precision, so there is nothing to split: the value is shifted out one
|
|
29
|
+
# byte at a time and reassembled the same way, which keeps a single
|
|
30
|
+
# implementation correct for any +byte_count+.
|
|
31
|
+
#
|
|
32
|
+
# +signed+ selects two's-complement interpretation over +byte_count * 8+
|
|
33
|
+
# bits, so negative values round-trip.
|
|
34
|
+
sig { params(byte_count: Integer, signed: T::Boolean, endian: Symbol).returns(Codec) }
|
|
35
|
+
def big_int_codec(byte_count, signed:, endian: :little)
|
|
36
|
+
bits = byte_count * 8
|
|
37
|
+
enc = Encoder.new(fixed_size: byte_count) do |v|
|
|
38
|
+
n = Kernel.Integer(v)
|
|
39
|
+
n += (1 << bits) if signed && n.negative?
|
|
40
|
+
out = Array.new(byte_count) do
|
|
41
|
+
byte = n & 0xFF
|
|
42
|
+
n >>= 8
|
|
43
|
+
byte
|
|
44
|
+
end
|
|
45
|
+
# `out` is least-significant-byte first.
|
|
46
|
+
(endian == :little ? out : out.reverse).pack('C*')
|
|
47
|
+
end
|
|
48
|
+
dec = Decoder.new(fixed_size: byte_count) do |bytes, offset|
|
|
49
|
+
slice = bytes.b.byteslice(offset, byte_count) || ("\x00" * byte_count).b
|
|
50
|
+
arr = T.cast(T.unsafe(slice).unpack('C*'), T::Array[Integer])
|
|
51
|
+
arr = arr.reverse if endian == :little
|
|
52
|
+
n = arr.reduce(0) { |acc, byte| (acc << 8) | byte }
|
|
53
|
+
n -= (1 << bits) if signed && n >= (1 << (bits - 1))
|
|
54
|
+
[n, byte_count]
|
|
55
|
+
end
|
|
56
|
+
Codec.new(enc, dec)
|
|
57
|
+
end
|
|
58
|
+
|
|
22
59
|
# ── Unsigned integers ────────────────────────────────────────────────────
|
|
23
60
|
|
|
24
61
|
sig { returns(Codec) }
|
|
@@ -62,29 +99,12 @@ module Solana::Ruby::Kit
|
|
|
62
99
|
|
|
63
100
|
sig { params(endian: Symbol).returns(Codec) }
|
|
64
101
|
def u128_codec(endian: :little)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
else
|
|
72
|
-
bytes = []
|
|
73
|
-
16.times { bytes.unshift(n & 0xFF); n >>= 8 }
|
|
74
|
-
bytes.pack('C*')
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
dec = Decoder.new(fixed_size: 16) do |bytes, offset|
|
|
78
|
-
slice = bytes.b.byteslice(offset, 16) || ("\x00" * 16).b
|
|
79
|
-
arr = T.cast(T.unsafe(slice).unpack('C*'), T::Array[Integer])
|
|
80
|
-
n = if endian == :little
|
|
81
|
-
arr.reverse.reduce(0) { |acc, b| (acc << 8) | b }
|
|
82
|
-
else
|
|
83
|
-
arr.reduce(0) { |acc, b| (acc << 8) | b }
|
|
84
|
-
end
|
|
85
|
-
[n, 16]
|
|
86
|
-
end
|
|
87
|
-
Codec.new(enc, dec)
|
|
102
|
+
big_int_codec(16, signed: false, endian: endian)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
106
|
+
def u256_codec(endian: :little)
|
|
107
|
+
big_int_codec(32, signed: false, endian: endian)
|
|
88
108
|
end
|
|
89
109
|
|
|
90
110
|
# ── Signed integers ──────────────────────────────────────────────────────
|
|
@@ -130,33 +150,12 @@ module Solana::Ruby::Kit
|
|
|
130
150
|
|
|
131
151
|
sig { params(endian: Symbol).returns(Codec) }
|
|
132
152
|
def i128_codec(endian: :little)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
16.times { bytes << (n & 0xFF); n >>= 8 }
|
|
140
|
-
bytes.pack('C*')
|
|
141
|
-
else
|
|
142
|
-
bytes = []
|
|
143
|
-
16.times { bytes.unshift(n & 0xFF); n >>= 8 }
|
|
144
|
-
bytes.pack('C*')
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
dec = Decoder.new(fixed_size: 16) do |bytes, offset|
|
|
148
|
-
slice = bytes.b.byteslice(offset, 16) || ("\x00" * 16).b
|
|
149
|
-
arr = T.cast(T.unsafe(slice).unpack('C*'), T::Array[Integer])
|
|
150
|
-
n = if endian == :little
|
|
151
|
-
arr.reverse.reduce(0) { |acc, b| (acc << 8) | b }
|
|
152
|
-
else
|
|
153
|
-
arr.reduce(0) { |acc, b| (acc << 8) | b }
|
|
154
|
-
end
|
|
155
|
-
# Convert from unsigned to signed 128-bit
|
|
156
|
-
n -= (1 << 128) if n >= (1 << 127)
|
|
157
|
-
[n, 16]
|
|
158
|
-
end
|
|
159
|
-
Codec.new(enc, dec)
|
|
153
|
+
big_int_codec(16, signed: true, endian: endian)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
157
|
+
def i256_codec(endian: :little)
|
|
158
|
+
big_int_codec(32, signed: true, endian: endian)
|
|
160
159
|
end
|
|
161
160
|
|
|
162
161
|
# ── Floating point ───────────────────────────────────────────────────────
|
|
@@ -201,21 +200,57 @@ module Solana::Ruby::Kit
|
|
|
201
200
|
bytes.pack('C*')
|
|
202
201
|
end
|
|
203
202
|
dec = Decoder.new do |bytes, offset|
|
|
204
|
-
b
|
|
205
|
-
n
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
b = bytes.b
|
|
204
|
+
n = 0
|
|
205
|
+
decoded = (1..3).each do |byte_count|
|
|
206
|
+
# The chain must terminate within the bytes that remain; a buffer
|
|
207
|
+
# that ends mid-chain is truncated, not an implicit zero byte.
|
|
208
|
+
remaining = b.bytesize - offset
|
|
209
|
+
if remaining < byte_count
|
|
210
|
+
Kernel.raise SolanaError.new(
|
|
211
|
+
SolanaError::CODECS__INVALID_BYTE_LENGTH,
|
|
212
|
+
{ codec_description: 'shortU16', expected: byte_count, actual: [remaining, 0].max }
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
byte = T.must(b.getbyte(offset + byte_count - 1))
|
|
217
|
+
n |= (byte & 0x7F) << ((byte_count - 1) * 7)
|
|
218
|
+
next unless (byte & 0x80).zero?
|
|
219
|
+
|
|
220
|
+
Numbers.assert_short_u16_in_range(n)
|
|
221
|
+
break [n, byte_count]
|
|
214
222
|
end
|
|
215
|
-
|
|
223
|
+
# `each` only returns its range when no byte terminated the chain.
|
|
224
|
+
next decoded if decoded.is_a?(Array)
|
|
225
|
+
|
|
226
|
+
Numbers.raise_short_u16_too_long
|
|
216
227
|
end
|
|
217
228
|
Codec.new(enc, dec)
|
|
218
229
|
end
|
|
230
|
+
|
|
231
|
+
# Three terminated shortU16 bytes can hold up to 2^21 - 1, but only the
|
|
232
|
+
# u16 domain is valid. Shared with the wire-format readers in
|
|
233
|
+
# WalletStandard and TransactionIntrospection, which decode shortU16
|
|
234
|
+
# inline so they can raise their own truncation errors.
|
|
235
|
+
sig { params(value: Integer).void }
|
|
236
|
+
def assert_short_u16_in_range(value)
|
|
237
|
+
return if value <= 0xFFFF
|
|
238
|
+
|
|
239
|
+
Kernel.raise SolanaError.new(
|
|
240
|
+
SolanaError::CODECS__NUMBER_OUT_OF_RANGE,
|
|
241
|
+
{ codec_description: 'shortU16', min: 0, max: 0xFFFF, value: value }
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Raised when all three shortU16 bytes carry a continuation bit: the
|
|
246
|
+
# encoding would need a fourth byte, which the format does not allow.
|
|
247
|
+
sig { returns(T.noreturn) }
|
|
248
|
+
def raise_short_u16_too_long
|
|
249
|
+
Kernel.raise SolanaError.new(
|
|
250
|
+
SolanaError::CODECS__INVALID_BYTE_LENGTH,
|
|
251
|
+
{ codec_description: 'shortU16', expected: 3, actual: 4 }
|
|
252
|
+
)
|
|
253
|
+
end
|
|
219
254
|
end
|
|
220
255
|
end
|
|
221
256
|
end
|