solana-ruby-kit 7.1.1.2 → 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 +106 -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 +12 -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 +107 -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/api/get_ag_genesis_cert.rb +72 -0
- data/lib/solana/ruby/kit/rpc/client.rb +2 -0
- 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 +3 -1
- data/lib/solana/ruby/kit/transaction_messages/resource_limit_validation.rb +70 -0
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +1 -1
- data/lib/solana/ruby/kit/transaction_messages.rb +1 -0
- 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 +3 -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
|
@@ -189,9 +189,17 @@ msg = TxMsg.append_instructions(msg, [instruction])
|
|
|
189
189
|
msg = TxMsg.prepend_instructions(msg, [priority_fee_ix])
|
|
190
190
|
|
|
191
191
|
# Compute unit limit (SetComputeUnitLimit instruction from the Compute Budget program)
|
|
192
|
+
# The limit is validated: it must be an integer in [0, MAX_COMPUTE_UNIT_LIMIT] (1,400,000).
|
|
193
|
+
# The runtime silently clamps a larger request rather than failing, so asking for more than
|
|
194
|
+
# the maximum would run on a budget you did not ask for — this raises instead.
|
|
192
195
|
msg = TxMsg.set_transaction_message_compute_unit_limit(200_000, msg)
|
|
193
196
|
TxMsg.get_transaction_message_compute_unit_limit(msg) # => 200_000
|
|
194
197
|
|
|
198
|
+
# The check is also available on its own, alongside one for heap frame sizes — a whole
|
|
199
|
+
# number of KiB in [MIN_HEAP_SIZE, MAX_HEAP_SIZE] (32 KiB to 256 KiB).
|
|
200
|
+
TxMsg.assert_is_valid_compute_unit_limit(200_000)
|
|
201
|
+
TxMsg.assert_is_valid_heap_size(64 * 1024)
|
|
202
|
+
|
|
195
203
|
# Loaded accounts data size limit
|
|
196
204
|
msg = TxMsg.set_transaction_message_loaded_accounts_data_size_limit(64_000, msg)
|
|
197
205
|
TxMsg.get_transaction_message_loaded_accounts_data_size_limit(msg) # => 64_000
|
|
@@ -274,6 +282,7 @@ rpc.get_block_time(slot)
|
|
|
274
282
|
rpc.get_inflation_reward([address])
|
|
275
283
|
rpc.get_signatures_for_address(address)
|
|
276
284
|
rpc.get_vote_accounts
|
|
285
|
+
rpc.get_ag_genesis_cert # Alpenglow genesis cert, or nil
|
|
277
286
|
rpc.simulate_transaction(encoded_tx)
|
|
278
287
|
rpc.send_transaction(encoded_tx)
|
|
279
288
|
rpc.request_airdrop(address, lamports) # devnet / testnet only
|
|
@@ -351,20 +360,25 @@ Binary encoder/decoder framework for Solana on-chain data.
|
|
|
351
360
|
```ruby
|
|
352
361
|
Codecs = Solana::Ruby::Kit::Codecs
|
|
353
362
|
|
|
354
|
-
# Numbers
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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]
|
|
364
378
|
|
|
365
379
|
# Strings
|
|
366
|
-
utf8 = Codecs.
|
|
367
|
-
bytes = Codecs.bytes_codec
|
|
380
|
+
utf8 = Codecs.utf8_codec
|
|
381
|
+
bytes = Codecs.bytes_codec(32)
|
|
368
382
|
|
|
369
383
|
# Data structures
|
|
370
384
|
struct_codec = Codecs.struct_codec([
|
|
@@ -373,6 +387,71 @@ struct_codec = Codecs.struct_codec([
|
|
|
373
387
|
])
|
|
374
388
|
```
|
|
375
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
|
+
|
|
376
455
|
### `Solana::Ruby::Kit::RpcSubscriptions` — `@solana/rpc-subscriptions`
|
|
377
456
|
|
|
378
457
|
WebSocket-based subscription client.
|
|
@@ -640,6 +719,21 @@ result = executor.call(transaction_plan)
|
|
|
640
719
|
# A one-argument lambda returning { transaction:, context: } — the shape this method
|
|
641
720
|
# required before v7.1.0 — is still accepted and adapted onto the flow above.
|
|
642
721
|
|
|
722
|
+
# ── 3b. Executing every leaf concurrently ────────────────────────────────────
|
|
723
|
+
# For work that has no execution dependencies — signing or serializing, say —
|
|
724
|
+
# create_transaction_plan_executor_with_concurrent_leaves takes the same callback
|
|
725
|
+
# contract but starts every leaf immediately, each on its own thread. It preserves
|
|
726
|
+
# the plan's nesting, order and divisibility in the result, supports non-divisible
|
|
727
|
+
# sequential plans, and cancels nothing: a leaf that raises does not stop the others,
|
|
728
|
+
# so every leaf runs to completion before the failure is reported. Because the leaves
|
|
729
|
+
# run at the same time, the callback must be safe to call concurrently with itself.
|
|
730
|
+
concurrent = Plans.create_transaction_plan_executor_with_concurrent_leaves(
|
|
731
|
+
execute_transaction_message: ->(_context, message) {
|
|
732
|
+
{ transaction: Kit::Transactions.compile_transaction_message(message) }
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
result = concurrent.call(transaction_plan)
|
|
736
|
+
|
|
643
737
|
# ── 4. Plan types ─────────────────────────────────────────────────────────────
|
|
644
738
|
Plans.single_instruction_plan(ix) # wrap one instruction
|
|
645
739
|
Plans.sequential_instruction_plan([ix1, ix2]) # ordered, divisible
|
|
@@ -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 ───────────────────────────────────────────────────────
|