solana-ruby-kit 8.2.0 → 8.3.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 +82 -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 +95 -1
- data/lib/solana/ruby/kit/codecs/data_structures.rb +51 -8
- data/lib/solana/ruby/kit/codecs/numbers.rb +49 -50
- 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 +6 -0
- 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 +1 -1
- data/lib/solana/ruby/kit/instruction_plans/max_instructions.rb +1 -1
- 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 +2 -2
- 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 +1 -1
- 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 +3 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c281fe52274deb9bbf493d3848b3bcb64ecc42b11e4acc6b727ef5e301dcb252
|
|
4
|
+
data.tar.gz: 6fefa37ee1da4bc377bc58f609adad46c2940b3377a9333168df0f6ff83cef6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba02f18c40de9b8b9ab544551ac477e94b1f4dcd4b177e8d513680b3d406830991acb770f4f427b5e9d4ca4abaf0cdda28d5849d649e6e50037104fe653390a7
|
|
7
|
+
data.tar.gz: 5ccc70a501ac546deb6520394b9efedc8ddbd7b2278318ab0fdf3944352e9a66ac06d66bd8b8288b1bf4b10a9cb395266d23f3feec3d7708741421a83033af9d
|
data/README.md
CHANGED
|
@@ -360,20 +360,25 @@ Binary encoder/decoder framework for Solana on-chain data.
|
|
|
360
360
|
```ruby
|
|
361
361
|
Codecs = Solana::Ruby::Kit::Codecs
|
|
362
362
|
|
|
363
|
-
# Numbers
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
363
|
+
# Numbers. Every multi-byte codec takes `endian:`, defaulting to :little
|
|
364
|
+
# (which is what Solana uses on-chain).
|
|
365
|
+
u8 = Codecs.u8_codec
|
|
366
|
+
u16 = Codecs.u16_codec
|
|
367
|
+
u32 = Codecs.u32_codec
|
|
368
|
+
u64 = Codecs.u64_codec
|
|
369
|
+
u128 = Codecs.u128_codec
|
|
370
|
+
u256 = Codecs.u256_codec # 32 bytes
|
|
371
|
+
i8 = Codecs.i8_codec
|
|
372
|
+
i256 = Codecs.i256_codec # 32 bytes, two's complement
|
|
373
|
+
f32 = Codecs.f32_codec
|
|
374
|
+
be = Codecs.u16_codec(endian: :big)
|
|
375
|
+
|
|
376
|
+
u16.encode(1000) # => "\xe8\x03"
|
|
377
|
+
u16.decode("\xe8\x03") # => [1000, 2] # [value, bytes consumed]
|
|
373
378
|
|
|
374
379
|
# Strings
|
|
375
|
-
utf8 = Codecs.
|
|
376
|
-
bytes = Codecs.bytes_codec
|
|
380
|
+
utf8 = Codecs.utf8_codec
|
|
381
|
+
bytes = Codecs.bytes_codec(32)
|
|
377
382
|
|
|
378
383
|
# Data structures
|
|
379
384
|
struct_codec = Codecs.struct_codec([
|
|
@@ -382,6 +387,71 @@ struct_codec = Codecs.struct_codec([
|
|
|
382
387
|
])
|
|
383
388
|
```
|
|
384
389
|
|
|
390
|
+
#### UTF-8 options
|
|
391
|
+
|
|
392
|
+
By default the UTF-8 codec substitutes `U+FFFD` for malformed input, strips a
|
|
393
|
+
leading byte order mark, and strips null characters - the last of which is what
|
|
394
|
+
makes fixed-size padded strings read back cleanly, at the cost of being lossy
|
|
395
|
+
for strings that legitimately contain nulls.
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# Reject invalid UTF-8 instead of substituting the replacement character.
|
|
399
|
+
strict = Codecs.utf8_codec(fatal: true)
|
|
400
|
+
strict.encode("a\xC0\x80") # raises SolanaError (invalid UTF-8 string)
|
|
401
|
+
strict.decode("\xFF".b) # raises SolanaError (invalid UTF-8 bytes)
|
|
402
|
+
|
|
403
|
+
# Decode losslessly, keeping null characters and a leading byte order mark.
|
|
404
|
+
lossless = Codecs.utf8_codec(ignore_bom: true, remove_null_characters: false)
|
|
405
|
+
|
|
406
|
+
# The assertions are usable on their own.
|
|
407
|
+
Codecs.find_malformed_utf8_sequence_offset("a\xC0\x80".b) # => 1 (overlong)
|
|
408
|
+
Codecs.assert_is_well_formed_utf8_bytes("\xE8\xAA\x9E".b) # passes ("語")
|
|
409
|
+
Codecs.remove_null_characters("a\x00b") # => "ab"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Note that `ignore_bom` follows `TextDecoder`'s confusing spelling: the default,
|
|
413
|
+
`false`, *strips* the mark; `true` keeps it.
|
|
414
|
+
|
|
415
|
+
#### Requiring a size prefix
|
|
416
|
+
|
|
417
|
+
A prefixed collection decodes an exhausted buffer to an empty collection rather
|
|
418
|
+
than failing, so a program can append a collection to an existing account layout
|
|
419
|
+
and still read accounts written before the change. Formats that cannot accept
|
|
420
|
+
that leniency - borsh requires the prefix to be present - can opt into failing:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
Codecs.array_codec(u8).decode(''.b) # => [[], 0]
|
|
424
|
+
Codecs.array_codec(u8, require_size_prefix: true).decode(''.b) # raises SolanaError
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`map_codec` and `set_codec` take the same option. It has no effect on
|
|
428
|
+
fixed-count collections, which carry no prefix.
|
|
429
|
+
|
|
430
|
+
#### Tap combinators
|
|
431
|
+
|
|
432
|
+
Wrap a codec in one that observes a value - or the bytes - without changing it,
|
|
433
|
+
for validation guards or logging. A tap that raises aborts the operation.
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
guarded = Codecs.tap_encoder(u8.encoder) do |value|
|
|
437
|
+
raise ArgumentError, 'Value must not exceed 100' if value > 100
|
|
438
|
+
end
|
|
439
|
+
guarded.encode(42) # => "\x2a"
|
|
440
|
+
guarded.encode(200) # raises ArgumentError
|
|
441
|
+
|
|
442
|
+
# Both sides at once; decode_tap is optional.
|
|
443
|
+
Codecs.tap_codec(u8, encode_tap: ->(v) { log(v) }, decode_tap: ->(v) { log(v) })
|
|
444
|
+
|
|
445
|
+
# Observe the bytes instead of the value.
|
|
446
|
+
Codecs.tap_decoder_bytes(u8.decoder) do |bytes, offset|
|
|
447
|
+
raise ArgumentError, 'Expected a 0 or a 1' if bytes.bytes[offset] > 1
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Upstream's byte taps take `(bytes, pre_offset, post_offset)` because encoders
|
|
452
|
+
there write into a shared buffer. A Ruby `Encoder` returns a standalone byte
|
|
453
|
+
String, so the encode-side taps receive exactly the bytes that were written.
|
|
454
|
+
|
|
385
455
|
### `Solana::Ruby::Kit::RpcSubscriptions` — `@solana/rpc-subscriptions`
|
|
386
456
|
|
|
387
457
|
WebSocket-based subscription client.
|
|
@@ -61,7 +61,7 @@ module Solana::Ruby::Kit
|
|
|
61
61
|
ADDRESS_MIN_STR_LEN = T.let(32, Integer)
|
|
62
62
|
ADDRESS_MAX_STR_LEN = T.let(44, Integer)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
extend self
|
|
65
65
|
|
|
66
66
|
# Encodes raw bytes (binary String, length == 32) to a base58 address string.
|
|
67
67
|
# Mirrors `getAddressEncoder()` in TypeScript.
|
|
@@ -27,7 +27,7 @@ module Solana::Ruby::Kit
|
|
|
27
27
|
# sqrt(−1) mod p = 2^((p−1)/4) mod p
|
|
28
28
|
CURVE_SQRT_M1 = T.let(2.pow((CURVE_P - 1) / 4, CURVE_P), Integer)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
extend self
|
|
31
31
|
|
|
32
32
|
# Returns true if the 32-byte binary string represents a point on the
|
|
33
33
|
# Ed25519 twisted-Edwards curve.
|
|
@@ -38,7 +38,7 @@ module Solana::Ruby::Kit
|
|
|
38
38
|
# Marker bytes appended during hashing: UTF-8 "ProgramDerivedAddress".
|
|
39
39
|
PDA_MARKER_BYTES = T.let('ProgramDerivedAddress'.b, String)
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
extend self
|
|
42
42
|
|
|
43
43
|
# Returns true if the value is a well-formed ProgramDerivedAddress.
|
|
44
44
|
# Mirrors `isProgramDerivedAddress()` in TypeScript.
|
|
@@ -64,7 +64,7 @@ module Solana::Ruby::Kit
|
|
|
64
64
|
|
|
65
65
|
# ── Combinators ─────────────────────────────────────────────────────────────
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
extend self
|
|
68
68
|
|
|
69
69
|
# Return a Codec whose output is always exactly +size+ bytes
|
|
70
70
|
# (zero-padded on right, truncated if too large).
|
|
@@ -117,6 +117,100 @@ module Solana::Ruby::Kit
|
|
|
117
117
|
Codec.new(enc, dec)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
# ── Tap combinators ─────────────────────────────────────────────────────────
|
|
121
|
+
#
|
|
122
|
+
# Each of these wraps a codec in one that observes a value (or the bytes)
|
|
123
|
+
# without changing it, for validation guards, logging or other read-only
|
|
124
|
+
# side effects. A tap that raises aborts the operation and the error
|
|
125
|
+
# propagates to the caller.
|
|
126
|
+
#
|
|
127
|
+
# Where upstream's byte taps take +(bytes, pre_offset, post_offset)+ over a
|
|
128
|
+
# shared output buffer, a Ruby Encoder returns a standalone byte String, so
|
|
129
|
+
# the encode-side taps below receive exactly the bytes that were written -
|
|
130
|
+
# the window upstream's tap has to slice out for itself. The decode-side
|
|
131
|
+
# taps keep +(bytes, offset)+, which Ruby Decoders already receive.
|
|
132
|
+
|
|
133
|
+
# Observe each value before it is encoded, leaving it unchanged.
|
|
134
|
+
sig do
|
|
135
|
+
params(encoder: Encoder, tap_fn: T.proc.params(value: T.untyped).void).returns(Encoder)
|
|
136
|
+
end
|
|
137
|
+
def tap_encoder(encoder, &tap_fn)
|
|
138
|
+
Encoder.new(fixed_size: encoder.fixed_size, max_size: encoder.max_size) do |value|
|
|
139
|
+
tap_fn.call(value)
|
|
140
|
+
encoder.encode(value)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Observe each decoded value after it is decoded, leaving it unchanged.
|
|
145
|
+
sig do
|
|
146
|
+
params(decoder: Decoder, tap_fn: T.proc.params(value: T.untyped).void).returns(Decoder)
|
|
147
|
+
end
|
|
148
|
+
def tap_decoder(decoder, &tap_fn)
|
|
149
|
+
Decoder.new(fixed_size: decoder.fixed_size) do |bytes, offset|
|
|
150
|
+
value, consumed = decoder.decode(bytes, offset: offset)
|
|
151
|
+
tap_fn.call(value)
|
|
152
|
+
[value, consumed]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Observe a codec's values on both sides. +decode_tap+ is optional.
|
|
157
|
+
sig do
|
|
158
|
+
params(
|
|
159
|
+
codec: Codec,
|
|
160
|
+
encode_tap: T.proc.params(value: T.untyped).void,
|
|
161
|
+
decode_tap: T.nilable(T.proc.params(value: T.untyped).void)
|
|
162
|
+
).returns(Codec)
|
|
163
|
+
end
|
|
164
|
+
def tap_codec(codec, encode_tap:, decode_tap: nil)
|
|
165
|
+
enc = tap_encoder(codec.encoder) { |value| encode_tap.call(value) }
|
|
166
|
+
dec = decode_tap ? tap_decoder(codec.decoder) { |value| decode_tap.call(value) } : codec.decoder
|
|
167
|
+
Codec.new(enc, dec)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Observe the bytes an encoder produced, after they are written.
|
|
171
|
+
sig do
|
|
172
|
+
params(encoder: Encoder, tap_fn: T.proc.params(bytes: String).void).returns(Encoder)
|
|
173
|
+
end
|
|
174
|
+
def tap_encoder_bytes(encoder, &tap_fn)
|
|
175
|
+
Encoder.new(fixed_size: encoder.fixed_size, max_size: encoder.max_size) do |value|
|
|
176
|
+
bytes = encoder.encode(value)
|
|
177
|
+
tap_fn.call(bytes)
|
|
178
|
+
bytes
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Observe the raw bytes a decoder is about to read, before decoding.
|
|
183
|
+
sig do
|
|
184
|
+
params(
|
|
185
|
+
decoder: Decoder,
|
|
186
|
+
tap_fn: T.proc.params(bytes: String, offset: Integer).void
|
|
187
|
+
).returns(Decoder)
|
|
188
|
+
end
|
|
189
|
+
def tap_decoder_bytes(decoder, &tap_fn)
|
|
190
|
+
Decoder.new(fixed_size: decoder.fixed_size) do |bytes, offset|
|
|
191
|
+
tap_fn.call(bytes, offset)
|
|
192
|
+
decoder.decode(bytes, offset: offset)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Observe a codec's raw bytes on both sides. +decode_tap+ is optional.
|
|
197
|
+
sig do
|
|
198
|
+
params(
|
|
199
|
+
codec: Codec,
|
|
200
|
+
encode_tap: T.proc.params(bytes: String).void,
|
|
201
|
+
decode_tap: T.nilable(T.proc.params(bytes: String, offset: Integer).void)
|
|
202
|
+
).returns(Codec)
|
|
203
|
+
end
|
|
204
|
+
def tap_codec_bytes(codec, encode_tap:, decode_tap: nil)
|
|
205
|
+
enc = tap_encoder_bytes(codec.encoder) { |bytes| encode_tap.call(bytes) }
|
|
206
|
+
dec = if decode_tap
|
|
207
|
+
tap_decoder_bytes(codec.decoder) { |bytes, offset| decode_tap.call(bytes, offset) }
|
|
208
|
+
else
|
|
209
|
+
codec.decoder
|
|
210
|
+
end
|
|
211
|
+
Codec.new(enc, dec)
|
|
212
|
+
end
|
|
213
|
+
|
|
120
214
|
# Reverse the byte order of the encoded output (and input).
|
|
121
215
|
sig { params(codec: Codec).returns(Codec) }
|
|
122
216
|
def reverse_codec(codec)
|
|
@@ -63,8 +63,23 @@ module Solana::Ruby::Kit
|
|
|
63
63
|
|
|
64
64
|
# Encode/decode a variable-length array with a u32LE length prefix.
|
|
65
65
|
# When +size+ is given the array has a fixed element count (no prefix).
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
#
|
|
67
|
+
# When the size is stored as a prefix and there are not enough bytes left
|
|
68
|
+
# to read it, the decoder yields an empty array rather than failing. That
|
|
69
|
+
# is deliberate: it lets a program append a collection to an existing
|
|
70
|
+
# account layout and still decode accounts written before the change.
|
|
71
|
+
# Formats that cannot accept that leniency - borsh, for one, requires the
|
|
72
|
+
# prefix to be present - can pass +require_size_prefix: true+ to make a
|
|
73
|
+
# truncated buffer raise instead. The option has no effect when +size+ is
|
|
74
|
+
# given, since a fixed-count array carries no prefix.
|
|
75
|
+
sig do
|
|
76
|
+
params(
|
|
77
|
+
element_codec: Codec,
|
|
78
|
+
size: T.nilable(Integer),
|
|
79
|
+
require_size_prefix: T::Boolean
|
|
80
|
+
).returns(Codec)
|
|
81
|
+
end
|
|
82
|
+
def array_codec(element_codec, size: nil, require_size_prefix: false)
|
|
68
83
|
if size
|
|
69
84
|
fixed = element_codec.fixed_size ? size * T.must(element_codec.fixed_size) : nil
|
|
70
85
|
enc = Encoder.new(fixed_size: fixed) do |values|
|
|
@@ -90,6 +105,19 @@ module Solana::Ruby::Kit
|
|
|
90
105
|
header + body
|
|
91
106
|
end
|
|
92
107
|
dec = Decoder.new do |bytes, offset|
|
|
108
|
+
prefix_size = T.must(prefix.fixed_size)
|
|
109
|
+
remaining = [bytes.b.bytesize - offset, 0].max
|
|
110
|
+
if remaining < prefix_size
|
|
111
|
+
# The prefix is missing or truncated. By default that decodes to
|
|
112
|
+
# an empty collection having consumed nothing; under
|
|
113
|
+
# +require_size_prefix+ it is an error.
|
|
114
|
+
Kernel.raise SolanaError.new(
|
|
115
|
+
SolanaError::CODECS__INVALID_BYTE_LENGTH,
|
|
116
|
+
{ expected: prefix_size, actual: remaining }
|
|
117
|
+
) if require_size_prefix
|
|
118
|
+
|
|
119
|
+
next [[], 0]
|
|
120
|
+
end
|
|
93
121
|
len, prefix_bytes = prefix.decode(bytes, offset: offset)
|
|
94
122
|
result = []
|
|
95
123
|
consumed = prefix_bytes
|
|
@@ -106,10 +134,18 @@ module Solana::Ruby::Kit
|
|
|
106
134
|
|
|
107
135
|
# Encode/decode a Hash.
|
|
108
136
|
# Encoded as: [length prefix] + [key, value, key, value, ...]
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
# See +array_codec+ for what +require_size_prefix+ does.
|
|
138
|
+
sig do
|
|
139
|
+
params(
|
|
140
|
+
key_codec: Codec,
|
|
141
|
+
value_codec: Codec,
|
|
142
|
+
size: T.nilable(Integer),
|
|
143
|
+
require_size_prefix: T::Boolean
|
|
144
|
+
).returns(Codec)
|
|
145
|
+
end
|
|
146
|
+
def map_codec(key_codec, value_codec, size: nil, require_size_prefix: false)
|
|
111
147
|
pair_codec = tuple_codec([key_codec, value_codec])
|
|
112
|
-
array_codec(pair_codec, size: size).transform_decoder do |pairs|
|
|
148
|
+
array_codec(pair_codec, size: size, require_size_prefix: require_size_prefix).transform_decoder do |pairs|
|
|
113
149
|
pairs.each_with_object({}) { |(k, v), h| h[k] = v }
|
|
114
150
|
end.transform_encoder do |hash|
|
|
115
151
|
T.cast(hash, T::Hash[T.untyped, T.untyped]).map { |k, v| [k, v] }
|
|
@@ -117,9 +153,16 @@ module Solana::Ruby::Kit
|
|
|
117
153
|
end
|
|
118
154
|
|
|
119
155
|
# Encode/decode a Set (stored as an array of unique elements).
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
156
|
+
# See +array_codec+ for what +require_size_prefix+ does.
|
|
157
|
+
sig do
|
|
158
|
+
params(
|
|
159
|
+
element_codec: Codec,
|
|
160
|
+
size: T.nilable(Integer),
|
|
161
|
+
require_size_prefix: T::Boolean
|
|
162
|
+
).returns(Codec)
|
|
163
|
+
end
|
|
164
|
+
def set_codec(element_codec, size: nil, require_size_prefix: false)
|
|
165
|
+
array_codec(element_codec, size: size, require_size_prefix: require_size_prefix)
|
|
123
166
|
.transform_encoder { |s| T.cast(s, T::Set[T.untyped]).to_a }
|
|
124
167
|
.transform_decoder { |arr| Set.new(arr) }
|
|
125
168
|
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 ───────────────────────────────────────────────────────
|
|
@@ -15,14 +15,134 @@ module Solana::Ruby::Kit
|
|
|
15
15
|
# defeated the "directly available as Codecs.x" intent in codecs.rb.
|
|
16
16
|
extend self
|
|
17
17
|
|
|
18
|
+
# Byte order mark, U+FEFF, in its UTF-8 encoding.
|
|
19
|
+
BOM = T.let("\xEF\xBB\xBF".b.freeze, String)
|
|
20
|
+
|
|
21
|
+
# Remove every null character from a decoded string.
|
|
22
|
+
# Mirrors `removeNullCharacters`.
|
|
23
|
+
sig { params(value: String).returns(String) }
|
|
24
|
+
def remove_null_characters(value)
|
|
25
|
+
value.delete("\x00")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Find the first malformed UTF-8 sequence in +bytes+, starting at +offset+.
|
|
29
|
+
# Returns the byte offset at which it starts, or -1 when the bytes are
|
|
30
|
+
# well-formed.
|
|
31
|
+
#
|
|
32
|
+
# This rejects everything the Unicode standard rejects: unexpected
|
|
33
|
+
# continuation bytes, overlong encodings, encoded surrogates, code points
|
|
34
|
+
# above U+10FFFF and truncated sequences. Ruby's own +valid_encoding?+
|
|
35
|
+
# answers the same question, but only yes or no - this reports *where*,
|
|
36
|
+
# which is what the error context needs.
|
|
37
|
+
# Mirrors `findMalformedUtf8SequenceOffset`.
|
|
38
|
+
sig { params(bytes: String, offset: Integer).returns(Integer) }
|
|
39
|
+
def find_malformed_utf8_sequence_offset(bytes, offset = 0)
|
|
40
|
+
arr = T.cast(T.unsafe(bytes.b).unpack('C*'), T::Array[Integer])
|
|
41
|
+
length = arr.length
|
|
42
|
+
index = offset
|
|
43
|
+
while index < length
|
|
44
|
+
lead = T.must(arr[index])
|
|
45
|
+
min = 0x80
|
|
46
|
+
max = 0xBF
|
|
47
|
+
if lead < 0x80
|
|
48
|
+
index += 1
|
|
49
|
+
next
|
|
50
|
+
elsif lead >= 0xC2 && lead <= 0xDF
|
|
51
|
+
count = 1
|
|
52
|
+
elsif lead >= 0xE0 && lead <= 0xEF
|
|
53
|
+
count = 2
|
|
54
|
+
min = 0xA0 if lead == 0xE0 # Overlong.
|
|
55
|
+
max = 0x9F if lead == 0xED # Encoded surrogate.
|
|
56
|
+
elsif lead >= 0xF0 && lead <= 0xF4
|
|
57
|
+
count = 3
|
|
58
|
+
min = 0x90 if lead == 0xF0 # Overlong.
|
|
59
|
+
max = 0x8F if lead == 0xF4 # Above U+10FFFF.
|
|
60
|
+
else
|
|
61
|
+
return index
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
start = index
|
|
65
|
+
index += 1
|
|
66
|
+
count.times do |i|
|
|
67
|
+
return start if index >= length
|
|
68
|
+
|
|
69
|
+
byte = T.must(arr[index])
|
|
70
|
+
in_range = i.zero? ? byte >= min && byte <= max : byte >= 0x80 && byte <= 0xBF
|
|
71
|
+
return start unless in_range
|
|
72
|
+
|
|
73
|
+
index += 1
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
-1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Raise unless +bytes+, from +offset+, form well-formed UTF-8.
|
|
80
|
+
# Mirrors `assertIsWellFormedUtf8Bytes`.
|
|
81
|
+
sig { params(bytes: String, offset: Integer).void }
|
|
82
|
+
def assert_is_well_formed_utf8_bytes(bytes, offset = 0)
|
|
83
|
+
malformed = find_malformed_utf8_sequence_offset(bytes, offset)
|
|
84
|
+
return if malformed == -1
|
|
85
|
+
|
|
86
|
+
Kernel.raise SolanaError.new(
|
|
87
|
+
SolanaError::CODECS__INVALID_UTF8_BYTES,
|
|
88
|
+
{ bytes: bytes.b, offset: malformed }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Raise unless +value+ can be encoded as UTF-8 without loss.
|
|
93
|
+
# Mirrors `assertIsWellFormedUtf8String`.
|
|
94
|
+
#
|
|
95
|
+
# Upstream guards against lone surrogates, which a JavaScript string can
|
|
96
|
+
# hold because it is a sequence of UTF-16 code units. A Ruby String is a
|
|
97
|
+
# byte sequence with an encoding tag, so the equivalent flaw is a String
|
|
98
|
+
# whose bytes are not valid UTF-8. Upstream's reported +index+ counts
|
|
99
|
+
# UTF-16 code units; the +index+ here is a byte offset.
|
|
100
|
+
sig { params(value: String).void }
|
|
101
|
+
def assert_is_well_formed_utf8_string(value)
|
|
102
|
+
index = find_malformed_utf8_sequence_offset(value.b)
|
|
103
|
+
return if index == -1
|
|
104
|
+
|
|
105
|
+
Kernel.raise SolanaError.new(
|
|
106
|
+
SolanaError::CODECS__INVALID_UTF8_STRING,
|
|
107
|
+
{ index: index, value: value }
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
18
111
|
# UTF-8 string codec.
|
|
19
112
|
# When +size+ is given the encoded bytes are fixed to that length
|
|
20
113
|
# (zero-padded or truncated); otherwise the codec is variable-length
|
|
21
114
|
# and must be used inside a size-prefixed container.
|
|
22
|
-
|
|
23
|
-
|
|
115
|
+
#
|
|
116
|
+
# +fatal+ rejects invalid UTF-8 instead of passing it through: on encode a
|
|
117
|
+
# String whose bytes are not valid UTF-8, on decode a malformed byte
|
|
118
|
+
# sequence.
|
|
119
|
+
#
|
|
120
|
+
# +ignore_bom+ follows TextDecoder's confusing spelling: the default,
|
|
121
|
+
# +false+, *strips* a leading byte order mark; +true+ keeps it.
|
|
122
|
+
#
|
|
123
|
+
# +remove_null_characters+ strips every null character from the decoded
|
|
124
|
+
# string, which is what makes fixed-size padded strings read back cleanly.
|
|
125
|
+
# It is on by default, matching upstream, and makes the codec lossy for
|
|
126
|
+
# strings that legitimately contain nulls - pass +false+ for a lossless
|
|
127
|
+
# round trip.
|
|
128
|
+
sig do
|
|
129
|
+
params(
|
|
130
|
+
size: T.nilable(Integer),
|
|
131
|
+
fatal: T::Boolean,
|
|
132
|
+
ignore_bom: T::Boolean,
|
|
133
|
+
remove_null_characters: T::Boolean
|
|
134
|
+
).returns(Codec)
|
|
135
|
+
end
|
|
136
|
+
def utf8_codec(size: nil, fatal: false, ignore_bom: false, remove_null_characters: true)
|
|
137
|
+
strip_nulls = remove_null_characters
|
|
24
138
|
enc = Encoder.new(fixed_size: size) do |v|
|
|
25
|
-
|
|
139
|
+
str = v.to_s
|
|
140
|
+
# Transcode anything that is not already UTF-8 (or raw bytes); a
|
|
141
|
+
# UTF-8 to UTF-8 `encode` is a no-op and would not validate, which is
|
|
142
|
+
# what `fatal` is for.
|
|
143
|
+
str = str.encode(::Encoding::UTF_8) unless [::Encoding::UTF_8, ::Encoding::BINARY].include?(str.encoding)
|
|
144
|
+
raw = str.b
|
|
145
|
+
assert_is_well_formed_utf8_string(raw) if fatal
|
|
26
146
|
if size
|
|
27
147
|
raw.bytesize <= size ? raw.ljust(size, "\x00") : raw.byteslice(0, size) || ''.b
|
|
28
148
|
else
|
|
@@ -32,9 +152,15 @@ module Solana::Ruby::Kit
|
|
|
32
152
|
dec = Decoder.new(fixed_size: size) do |bytes, offset|
|
|
33
153
|
len = size || (bytes.bytesize - offset)
|
|
34
154
|
slice = bytes.b.byteslice(offset, len) || ''.b
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
155
|
+
assert_is_well_formed_utf8_bytes(slice) if fatal
|
|
156
|
+
slice = T.must(slice.byteslice(BOM.bytesize..)) if !ignore_bom && slice.start_with?(BOM)
|
|
157
|
+
str = slice.force_encoding(::Encoding::UTF_8)
|
|
158
|
+
# Non-fatal decoding substitutes U+FFFD for malformed sequences, as
|
|
159
|
+
# TextDecoder does. `scrub` is Ruby's version of exactly that, and it
|
|
160
|
+
# also keeps the null-stripping below from raising on invalid bytes.
|
|
161
|
+
str = str.scrub unless fatal
|
|
162
|
+
str = remove_null_characters(str) if strip_nulls
|
|
163
|
+
[str, len]
|
|
38
164
|
end
|
|
39
165
|
Codec.new(enc, dec)
|
|
40
166
|
end
|