solana-ruby-kit 0.1.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 +7 -0
- data/lib/core_extensions/tapioca/name_patch.rb +32 -0
- data/lib/core_extensions/tapioca/required_ancestors.rb +13 -0
- data/lib/generators/solana/ruby/kit/install/install_generator.rb +17 -0
- data/lib/generators/solana/ruby/kit/install/templates/solana_ruby_kit.rb.tt +8 -0
- data/lib/solana/ruby/kit/accounts/account.rb +47 -0
- data/lib/solana/ruby/kit/accounts/maybe_account.rb +86 -0
- data/lib/solana/ruby/kit/accounts.rb +6 -0
- data/lib/solana/ruby/kit/addresses/address.rb +133 -0
- data/lib/solana/ruby/kit/addresses/curve.rb +112 -0
- data/lib/solana/ruby/kit/addresses/program_derived_address.rb +155 -0
- data/lib/solana/ruby/kit/addresses/public_key.rb +39 -0
- data/lib/solana/ruby/kit/addresses.rb +11 -0
- data/lib/solana/ruby/kit/codecs/bytes.rb +58 -0
- data/lib/solana/ruby/kit/codecs/codec.rb +135 -0
- data/lib/solana/ruby/kit/codecs/data_structures.rb +177 -0
- data/lib/solana/ruby/kit/codecs/decoder.rb +43 -0
- data/lib/solana/ruby/kit/codecs/encoder.rb +52 -0
- data/lib/solana/ruby/kit/codecs/numbers.rb +217 -0
- data/lib/solana/ruby/kit/codecs/strings.rb +116 -0
- data/lib/solana/ruby/kit/codecs.rb +25 -0
- data/lib/solana/ruby/kit/configuration.rb +48 -0
- data/lib/solana/ruby/kit/encoding/base58.rb +62 -0
- data/lib/solana/ruby/kit/errors.rb +226 -0
- data/lib/solana/ruby/kit/fast_stable_stringify.rb +62 -0
- data/lib/solana/ruby/kit/functional.rb +29 -0
- data/lib/solana/ruby/kit/instruction_plans/plans.rb +27 -0
- data/lib/solana/ruby/kit/instruction_plans.rb +47 -0
- data/lib/solana/ruby/kit/instructions/accounts.rb +80 -0
- data/lib/solana/ruby/kit/instructions/instruction.rb +71 -0
- data/lib/solana/ruby/kit/instructions/roles.rb +84 -0
- data/lib/solana/ruby/kit/instructions.rb +7 -0
- data/lib/solana/ruby/kit/keys/key_pair.rb +84 -0
- data/lib/solana/ruby/kit/keys/private_key.rb +39 -0
- data/lib/solana/ruby/kit/keys/public_key.rb +31 -0
- data/lib/solana/ruby/kit/keys/signatures.rb +171 -0
- data/lib/solana/ruby/kit/keys.rb +11 -0
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +107 -0
- data/lib/solana/ruby/kit/offchain_messages/message.rb +22 -0
- data/lib/solana/ruby/kit/offchain_messages.rb +16 -0
- data/lib/solana/ruby/kit/options/option.rb +132 -0
- data/lib/solana/ruby/kit/options.rb +5 -0
- data/lib/solana/ruby/kit/plugin_core.rb +58 -0
- data/lib/solana/ruby/kit/programs.rb +42 -0
- data/lib/solana/ruby/kit/promises.rb +85 -0
- data/lib/solana/ruby/kit/railtie.rb +18 -0
- data/lib/solana/ruby/kit/rpc/api/get_account_info.rb +76 -0
- data/lib/solana/ruby/kit/rpc/api/get_balance.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/get_block_height.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_epoch_info.rb +47 -0
- data/lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb +52 -0
- data/lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_program_accounts.rb +60 -0
- data/lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_slot.rb +30 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb +38 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb +48 -0
- data/lib/solana/ruby/kit/rpc/api/get_transaction.rb +36 -0
- data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +62 -0
- data/lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/request_airdrop.rb +35 -0
- data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +61 -0
- data/lib/solana/ruby/kit/rpc/api/simulate_transaction.rb +47 -0
- data/lib/solana/ruby/kit/rpc/client.rb +83 -0
- data/lib/solana/ruby/kit/rpc/transport.rb +137 -0
- data/lib/solana/ruby/kit/rpc.rb +13 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +51 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +52 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +38 -0
- data/lib/solana/ruby/kit/rpc_parsed_types.rb +16 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/account_notifications.rb +29 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/logs_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/program_notifications.rb +30 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/root_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/signature_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/slot_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/autopinger.rb +42 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/client.rb +80 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/subscription.rb +58 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/transport.rb +163 -0
- data/lib/solana/ruby/kit/rpc_subscriptions.rb +12 -0
- data/lib/solana/ruby/kit/rpc_types/account_info.rb +53 -0
- data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +56 -0
- data/lib/solana/ruby/kit/rpc_types/commitment.rb +52 -0
- data/lib/solana/ruby/kit/rpc_types/lamports.rb +43 -0
- data/lib/solana/ruby/kit/rpc_types.rb +8 -0
- data/lib/solana/ruby/kit/signers/keypair_signer.rb +126 -0
- data/lib/solana/ruby/kit/signers.rb +5 -0
- data/lib/solana/ruby/kit/subscribable/async_iterable.rb +80 -0
- data/lib/solana/ruby/kit/subscribable/data_publisher.rb +90 -0
- data/lib/solana/ruby/kit/subscribable.rb +13 -0
- data/lib/solana/ruby/kit/sysvars/addresses.rb +19 -0
- data/lib/solana/ruby/kit/sysvars/clock.rb +37 -0
- data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +34 -0
- data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +22 -0
- data/lib/solana/ruby/kit/sysvars/rent.rb +29 -0
- data/lib/solana/ruby/kit/sysvars.rb +33 -0
- data/lib/solana/ruby/kit/transaction_confirmation.rb +159 -0
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +168 -0
- data/lib/solana/ruby/kit/transaction_messages.rb +5 -0
- data/lib/solana/ruby/kit/transactions/transaction.rb +135 -0
- data/lib/solana/ruby/kit/transactions.rb +5 -0
- data/lib/solana/ruby/kit/version.rb +10 -0
- data/lib/solana/ruby/kit.rb +100 -0
- data/solana-ruby-kit.gemspec +29 -0
- metadata +311 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Codecs
|
|
6
|
+
# Low-level byte-string utilities.
|
|
7
|
+
# All methods work with binary-encoded Ruby Strings (encoding = ASCII-8BIT).
|
|
8
|
+
module Bytes
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Concatenate multiple binary strings into one.
|
|
14
|
+
sig { params(byte_strings: String).returns(String) }
|
|
15
|
+
def merge_bytes(*byte_strings)
|
|
16
|
+
result = ''.b
|
|
17
|
+
byte_strings.each { |bs| result << bs.b }
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Pad +bytes+ to exactly +size+ bytes.
|
|
22
|
+
# @param direction [:right, :left] which side to pad
|
|
23
|
+
sig do
|
|
24
|
+
params(bytes: String, size: Integer, direction: Symbol, pad_byte: String)
|
|
25
|
+
.returns(String)
|
|
26
|
+
end
|
|
27
|
+
def pad_bytes(bytes, size, direction: :right, pad_byte: "\x00")
|
|
28
|
+
b = bytes.b
|
|
29
|
+
return b if b.bytesize >= size
|
|
30
|
+
|
|
31
|
+
padding = (pad_byte.b * (size - b.bytesize))
|
|
32
|
+
direction == :left ? padding + b : b + padding
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Return exactly +size+ bytes from +bytes+, raising if sizes don't match.
|
|
36
|
+
sig { params(bytes: String, size: Integer).returns(String) }
|
|
37
|
+
def fix_bytes(bytes, size)
|
|
38
|
+
b = bytes.b
|
|
39
|
+
unless b.bytesize == size
|
|
40
|
+
Kernel.raise ArgumentError,
|
|
41
|
+
"Expected #{size} bytes, got #{b.bytesize}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
b
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Return true if +inner+ appears inside +outer+ starting at +offset+.
|
|
48
|
+
sig { params(outer: String, inner: String, offset: Integer).returns(T::Boolean) }
|
|
49
|
+
def contains_bytes?(outer, inner, offset: 0)
|
|
50
|
+
ob = outer.b
|
|
51
|
+
ib = inner.b
|
|
52
|
+
return false if offset + ib.bytesize > ob.bytesize
|
|
53
|
+
|
|
54
|
+
ob.byteslice(offset, ib.bytesize) == ib
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Codecs
|
|
6
|
+
extend T::Sig
|
|
7
|
+
# A Codec combines an Encoder and a Decoder for the same type.
|
|
8
|
+
# It also provides combinators that mirror @solana/codecs-core helpers:
|
|
9
|
+
# fix_codec_size, add_codec_size_prefix, offset_codec, reverse_codec
|
|
10
|
+
class Codec
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(Encoder) }
|
|
14
|
+
attr_reader :encoder
|
|
15
|
+
|
|
16
|
+
sig { returns(Decoder) }
|
|
17
|
+
attr_reader :decoder
|
|
18
|
+
|
|
19
|
+
sig { params(encoder: Encoder, decoder: Decoder).void }
|
|
20
|
+
def initialize(encoder, decoder)
|
|
21
|
+
@encoder = encoder
|
|
22
|
+
@decoder = decoder
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build a Codec from separate Encoder and Decoder.
|
|
26
|
+
sig { params(encoder: Encoder, decoder: Decoder).returns(Codec) }
|
|
27
|
+
def self.combine(encoder, decoder)
|
|
28
|
+
new(encoder, decoder)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Delegate encode / decode for convenience.
|
|
32
|
+
sig { params(value: T.untyped).returns(String) }
|
|
33
|
+
def encode(value) = @encoder.encode(value)
|
|
34
|
+
|
|
35
|
+
sig { params(bytes: String, offset: Integer).returns([T.untyped, Integer]) }
|
|
36
|
+
def decode(bytes, offset: 0) = @decoder.decode(bytes, offset: offset)
|
|
37
|
+
|
|
38
|
+
sig { returns(T.nilable(Integer)) }
|
|
39
|
+
def fixed_size = @encoder.fixed_size
|
|
40
|
+
|
|
41
|
+
# Return a new Codec whose Encoder maps values through +map_fn+ before
|
|
42
|
+
# encoding (pre-encode transform).
|
|
43
|
+
sig { params(map_fn: T.proc.params(value: T.untyped).returns(T.untyped)).returns(Codec) }
|
|
44
|
+
def transform_encoder(&map_fn)
|
|
45
|
+
original_enc = @encoder
|
|
46
|
+
new_enc = Encoder.new(fixed_size: original_enc.fixed_size, max_size: original_enc.max_size) do |value|
|
|
47
|
+
original_enc.encode(map_fn.call(value))
|
|
48
|
+
end
|
|
49
|
+
Codec.new(new_enc, @decoder)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return a new Codec whose Decoder maps decoded values through +map_fn+
|
|
53
|
+
# (post-decode transform).
|
|
54
|
+
sig { params(map_fn: T.proc.params(value: T.untyped).returns(T.untyped)).returns(Codec) }
|
|
55
|
+
def transform_decoder(&map_fn)
|
|
56
|
+
original_dec = @decoder
|
|
57
|
+
new_dec = Decoder.new(fixed_size: original_dec.fixed_size) do |bytes, offset|
|
|
58
|
+
value, consumed = original_dec.decode(bytes, offset: offset)
|
|
59
|
+
[map_fn.call(value), consumed]
|
|
60
|
+
end
|
|
61
|
+
Codec.new(@encoder, new_dec)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ── Combinators ─────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
# Return a Codec whose output is always exactly +size+ bytes
|
|
70
|
+
# (zero-padded on right, truncated if too large).
|
|
71
|
+
sig { params(codec: Codec, size: Integer).returns(Codec) }
|
|
72
|
+
def fix_codec_size(codec, size)
|
|
73
|
+
enc = Encoder.new(fixed_size: size) do |value|
|
|
74
|
+
raw = codec.encode(value)
|
|
75
|
+
if raw.bytesize < size
|
|
76
|
+
raw.b + ("\x00".b * (size - raw.bytesize))
|
|
77
|
+
else
|
|
78
|
+
raw.b[0, size] || ''.b
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
dec = Decoder.new(fixed_size: size) do |bytes, offset|
|
|
82
|
+
slice = bytes.b.byteslice(offset, size) || ''.b
|
|
83
|
+
value, = codec.decoder.decode(slice, offset: 0)
|
|
84
|
+
[value, size]
|
|
85
|
+
end
|
|
86
|
+
Codec.new(enc, dec)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Prefix encoded data with its byte length using +prefix_codec+
|
|
90
|
+
# (typically a u32 little-endian codec).
|
|
91
|
+
sig { params(codec: Codec, prefix_codec: Codec).returns(Codec) }
|
|
92
|
+
def add_codec_size_prefix(codec, prefix_codec)
|
|
93
|
+
enc = Encoder.new do |value|
|
|
94
|
+
data = codec.encode(value)
|
|
95
|
+
prefix = prefix_codec.encode(data.bytesize)
|
|
96
|
+
prefix + data
|
|
97
|
+
end
|
|
98
|
+
dec = Decoder.new do |bytes, offset|
|
|
99
|
+
len, prefix_size = prefix_codec.decode(bytes, offset: offset)
|
|
100
|
+
value, data_size = codec.decode(bytes, offset: offset + prefix_size)
|
|
101
|
+
[value, prefix_size + data_size]
|
|
102
|
+
end
|
|
103
|
+
Codec.new(enc, dec)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Shift the decode offset by +pre_offset+ before decoding and add
|
|
107
|
+
# +post_offset+ to the consumed byte count afterwards.
|
|
108
|
+
sig do
|
|
109
|
+
params(codec: Codec, pre_offset: Integer, post_offset: Integer).returns(Codec)
|
|
110
|
+
end
|
|
111
|
+
def offset_codec(codec, pre_offset: 0, post_offset: 0)
|
|
112
|
+
enc = Encoder.new(fixed_size: codec.fixed_size) { |v| codec.encode(v) }
|
|
113
|
+
dec = Decoder.new(fixed_size: codec.fixed_size) do |bytes, offset|
|
|
114
|
+
value, consumed = codec.decode(bytes, offset: offset + pre_offset)
|
|
115
|
+
[value, consumed + post_offset]
|
|
116
|
+
end
|
|
117
|
+
Codec.new(enc, dec)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Reverse the byte order of the encoded output (and input).
|
|
121
|
+
sig { params(codec: Codec).returns(Codec) }
|
|
122
|
+
def reverse_codec(codec)
|
|
123
|
+
enc = Encoder.new(fixed_size: codec.fixed_size) do |value|
|
|
124
|
+
codec.encode(value).b.reverse
|
|
125
|
+
end
|
|
126
|
+
dec = Decoder.new(fixed_size: codec.fixed_size) do |bytes, offset|
|
|
127
|
+
size = codec.fixed_size || bytes.bytesize - offset
|
|
128
|
+
slice = bytes.b.byteslice(offset, size) || ''.b
|
|
129
|
+
value, = codec.decode(slice.reverse, offset: 0)
|
|
130
|
+
[value, size]
|
|
131
|
+
end
|
|
132
|
+
Codec.new(enc, dec)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Codecs
|
|
6
|
+
# Data-structure codecs — mirrors @solana/codecs-data-structures.
|
|
7
|
+
module DataStructures
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Encode/decode a fixed ordered list of named fields.
|
|
13
|
+
# +fields+ is an Array of [name, codec] pairs (name can be String or Symbol).
|
|
14
|
+
# Encodes to a Hash on decode; expects a Hash for encode.
|
|
15
|
+
sig { params(fields: T::Array[[T.any(String, Symbol), Codec]]).returns(Codec) }
|
|
16
|
+
def struct_codec(fields)
|
|
17
|
+
fixed = fields.all? { |_, c| c.fixed_size }
|
|
18
|
+
total = fixed ? fields.sum { |_, c| T.must(c.fixed_size) } : nil
|
|
19
|
+
|
|
20
|
+
enc = Encoder.new(fixed_size: total) do |value|
|
|
21
|
+
h = T.cast(value, T::Hash[T.untyped, T.untyped])
|
|
22
|
+
fields.map { |name, codec| codec.encode(h[name] || h[name.to_s]) }.join.b
|
|
23
|
+
end
|
|
24
|
+
dec = Decoder.new(fixed_size: total) do |bytes, offset|
|
|
25
|
+
result = {}
|
|
26
|
+
consumed = 0
|
|
27
|
+
fields.each do |name, codec|
|
|
28
|
+
val, n = codec.decode(bytes, offset: offset + consumed)
|
|
29
|
+
result[name.to_sym] = val
|
|
30
|
+
consumed += n
|
|
31
|
+
end
|
|
32
|
+
[result, consumed]
|
|
33
|
+
end
|
|
34
|
+
Codec.new(enc, dec)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Encode/decode a fixed positional tuple (Array of values, one codec each).
|
|
38
|
+
sig { params(codecs: T::Array[Codec]).returns(Codec) }
|
|
39
|
+
def tuple_codec(codecs)
|
|
40
|
+
fixed = codecs.all?(&:fixed_size)
|
|
41
|
+
total = fixed ? codecs.sum { |c| T.must(c.fixed_size) } : nil
|
|
42
|
+
|
|
43
|
+
enc = Encoder.new(fixed_size: total) do |values|
|
|
44
|
+
arr = T.cast(values, T::Array[T.untyped])
|
|
45
|
+
codecs.each_with_index.map { |c, i| c.encode(arr[i]) }.join.b
|
|
46
|
+
end
|
|
47
|
+
dec = Decoder.new(fixed_size: total) do |bytes, offset|
|
|
48
|
+
result = []
|
|
49
|
+
consumed = 0
|
|
50
|
+
codecs.each do |c|
|
|
51
|
+
val, n = c.decode(bytes, offset: offset + consumed)
|
|
52
|
+
result << val
|
|
53
|
+
consumed += n
|
|
54
|
+
end
|
|
55
|
+
[result, consumed]
|
|
56
|
+
end
|
|
57
|
+
Codec.new(enc, dec)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Encode/decode a variable-length array with a u32LE length prefix.
|
|
61
|
+
# When +size+ is given the array has a fixed element count (no prefix).
|
|
62
|
+
sig { params(element_codec: Codec, size: T.nilable(Integer)).returns(Codec) }
|
|
63
|
+
def array_codec(element_codec, size: nil)
|
|
64
|
+
if size
|
|
65
|
+
fixed = element_codec.fixed_size ? size * T.must(element_codec.fixed_size) : nil
|
|
66
|
+
enc = Encoder.new(fixed_size: fixed) do |values|
|
|
67
|
+
T.cast(values, T::Array[T.untyped]).map { |v| element_codec.encode(v) }.join.b
|
|
68
|
+
end
|
|
69
|
+
dec = Decoder.new(fixed_size: fixed) do |bytes, offset|
|
|
70
|
+
result = []
|
|
71
|
+
consumed = 0
|
|
72
|
+
size.times do
|
|
73
|
+
val, n = element_codec.decode(bytes, offset: offset + consumed)
|
|
74
|
+
result << val
|
|
75
|
+
consumed += n
|
|
76
|
+
end
|
|
77
|
+
[result, consumed]
|
|
78
|
+
end
|
|
79
|
+
Codec.new(enc, dec)
|
|
80
|
+
else
|
|
81
|
+
prefix = Numbers.u32_codec
|
|
82
|
+
enc = Encoder.new do |values|
|
|
83
|
+
arr = T.cast(values, T::Array[T.untyped])
|
|
84
|
+
header = prefix.encode(arr.length)
|
|
85
|
+
body = arr.map { |v| element_codec.encode(v) }.join.b
|
|
86
|
+
header + body
|
|
87
|
+
end
|
|
88
|
+
dec = Decoder.new do |bytes, offset|
|
|
89
|
+
len, prefix_bytes = prefix.decode(bytes, offset: offset)
|
|
90
|
+
result = []
|
|
91
|
+
consumed = prefix_bytes
|
|
92
|
+
len.times do
|
|
93
|
+
val, n = element_codec.decode(bytes, offset: offset + consumed)
|
|
94
|
+
result << val
|
|
95
|
+
consumed += n
|
|
96
|
+
end
|
|
97
|
+
[result, consumed]
|
|
98
|
+
end
|
|
99
|
+
Codec.new(enc, dec)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Encode/decode a Hash.
|
|
104
|
+
# Encoded as: [length prefix] + [key, value, key, value, ...]
|
|
105
|
+
sig { params(key_codec: Codec, value_codec: Codec, size: T.nilable(Integer)).returns(Codec) }
|
|
106
|
+
def map_codec(key_codec, value_codec, size: nil)
|
|
107
|
+
pair_codec = tuple_codec([key_codec, value_codec])
|
|
108
|
+
array_codec(pair_codec, size: size).transform_decoder do |pairs|
|
|
109
|
+
pairs.each_with_object({}) { |(k, v), h| h[k] = v }
|
|
110
|
+
end.transform_encoder do |hash|
|
|
111
|
+
T.cast(hash, T::Hash[T.untyped, T.untyped]).map { |k, v| [k, v] }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Encode/decode a Set (stored as an array of unique elements).
|
|
116
|
+
sig { params(element_codec: Codec, size: T.nilable(Integer)).returns(Codec) }
|
|
117
|
+
def set_codec(element_codec, size: nil)
|
|
118
|
+
array_codec(element_codec, size: size)
|
|
119
|
+
.transform_encoder { |s| T.cast(s, T::Set[T.untyped]).to_a }
|
|
120
|
+
.transform_decoder { |arr| Set.new(arr) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Discriminated-union codec.
|
|
124
|
+
# +variants+ is an Array of [tag, codec] pairs; +discriminator_codec+ encodes
|
|
125
|
+
# the tag (typically a u8 codec).
|
|
126
|
+
# Encode expects +[tag, value]+; decode returns +[tag, value]+.
|
|
127
|
+
sig do
|
|
128
|
+
params(
|
|
129
|
+
variants: T::Array[[T.untyped, Codec]],
|
|
130
|
+
discriminator_codec: Codec
|
|
131
|
+
).returns(Codec)
|
|
132
|
+
end
|
|
133
|
+
def union_codec(variants, discriminator_codec)
|
|
134
|
+
tag_to_codec = variants.to_h
|
|
135
|
+
idx_to_tag = variants.map(&:first)
|
|
136
|
+
|
|
137
|
+
enc = Encoder.new do |tagged_value|
|
|
138
|
+
tag, value = T.cast(tagged_value, [T.untyped, T.untyped])
|
|
139
|
+
inner_codec = tag_to_codec.fetch(tag) { Kernel.raise ArgumentError, "Unknown union tag: #{tag}" }
|
|
140
|
+
discriminator_codec.encode(idx_to_tag.index(tag)) + inner_codec.encode(value)
|
|
141
|
+
end
|
|
142
|
+
dec = Decoder.new do |bytes, offset|
|
|
143
|
+
idx, disc_size = discriminator_codec.decode(bytes, offset: offset)
|
|
144
|
+
tag = idx_to_tag.fetch(idx) { Kernel.raise ArgumentError, "Unknown union discriminant: #{idx}" }
|
|
145
|
+
inner = tag_to_codec.fetch(tag)
|
|
146
|
+
value, n = inner.decode(bytes, offset: offset + disc_size)
|
|
147
|
+
[[tag, value], disc_size + n]
|
|
148
|
+
end
|
|
149
|
+
Codec.new(enc, dec)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Option codec — 1 byte discriminant (0 = None, 1 = Some) + optional value.
|
|
153
|
+
# Encode expects an Solana::Ruby::Kit::Options::Option; decode returns one.
|
|
154
|
+
sig { params(value_codec: Codec).returns(Codec) }
|
|
155
|
+
def option_codec(value_codec)
|
|
156
|
+
disc = Numbers.u8_codec
|
|
157
|
+
enc = Encoder.new do |option|
|
|
158
|
+
if option.is_a?(Solana::Ruby::Kit::Options::Some)
|
|
159
|
+
disc.encode(1) + value_codec.encode(option.value)
|
|
160
|
+
else
|
|
161
|
+
disc.encode(0)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
dec = Decoder.new do |bytes, offset|
|
|
165
|
+
flag, flag_size = disc.decode(bytes, offset: offset)
|
|
166
|
+
if flag == 1
|
|
167
|
+
val, n = value_codec.decode(bytes, offset: offset + flag_size)
|
|
168
|
+
[Solana::Ruby::Kit::Options::Some.new(val), flag_size + n]
|
|
169
|
+
else
|
|
170
|
+
[Solana::Ruby::Kit::Options::None.constants, flag_size]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
Codec.new(enc, dec)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Codecs
|
|
6
|
+
# Mirrors the TypeScript Decoder<T> interface from @solana/codecs-core.
|
|
7
|
+
#
|
|
8
|
+
# A Decoder knows how to turn binary bytes into a Ruby value.
|
|
9
|
+
# Fixed-size decoders advertise their byte length via +fixed_size+;
|
|
10
|
+
# variable-size decoders leave it nil.
|
|
11
|
+
#
|
|
12
|
+
# The inner block receives +(bytes, offset)+ and must return
|
|
13
|
+
# +[value, bytes_consumed]+.
|
|
14
|
+
class Decoder
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { returns(T.nilable(Integer)) }
|
|
18
|
+
attr_reader :fixed_size
|
|
19
|
+
|
|
20
|
+
sig do
|
|
21
|
+
params(
|
|
22
|
+
fixed_size: T.nilable(Integer),
|
|
23
|
+
block: T.proc.params(bytes: String, offset: Integer)
|
|
24
|
+
.returns([T.untyped, Integer])
|
|
25
|
+
).void
|
|
26
|
+
end
|
|
27
|
+
def initialize(fixed_size: nil, &block)
|
|
28
|
+
@fixed_size = fixed_size
|
|
29
|
+
@fn = T.let(
|
|
30
|
+
block,
|
|
31
|
+
T.proc.params(bytes: String, offset: Integer).returns([T.untyped, Integer])
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Decode from +bytes+ starting at +offset+.
|
|
36
|
+
# Returns +[value, bytes_consumed]+.
|
|
37
|
+
sig { params(bytes: String, offset: Integer).returns([T.untyped, Integer]) }
|
|
38
|
+
def decode(bytes, offset: 0)
|
|
39
|
+
@fn.call(bytes.b, offset)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Codecs
|
|
6
|
+
# Mirrors the TypeScript Encoder<T> interface from @solana/codecs-core.
|
|
7
|
+
#
|
|
8
|
+
# An Encoder knows how to turn a Ruby value into a binary String.
|
|
9
|
+
# Fixed-size encoders advertise their byte length via +fixed_size+;
|
|
10
|
+
# variable-size encoders leave it nil and may advertise +max_size+.
|
|
11
|
+
class Encoder
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(T.nilable(Integer)) }
|
|
15
|
+
attr_reader :fixed_size
|
|
16
|
+
|
|
17
|
+
sig { returns(T.nilable(Integer)) }
|
|
18
|
+
attr_reader :max_size
|
|
19
|
+
|
|
20
|
+
sig do
|
|
21
|
+
params(
|
|
22
|
+
fixed_size: T.nilable(Integer),
|
|
23
|
+
max_size: T.nilable(Integer),
|
|
24
|
+
block: T.proc.params(value: T.untyped).returns(String)
|
|
25
|
+
).void
|
|
26
|
+
end
|
|
27
|
+
def initialize(fixed_size: nil, max_size: nil, &block)
|
|
28
|
+
@fixed_size = fixed_size
|
|
29
|
+
@max_size = max_size
|
|
30
|
+
@fn = T.let(block, T.proc.params(value: T.untyped).returns(String))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Encode +value+ and return a binary String.
|
|
34
|
+
sig { params(value: T.untyped).returns(String) }
|
|
35
|
+
def encode(value)
|
|
36
|
+
@fn.call(value).b
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Encode +value+ into +target+ starting at +offset+.
|
|
40
|
+
# Returns the number of bytes written.
|
|
41
|
+
sig { params(value: T.untyped, target: String, offset: Integer).returns(Integer) }
|
|
42
|
+
def encode_into(value, target, offset)
|
|
43
|
+
bytes = encode(value)
|
|
44
|
+
target.b
|
|
45
|
+
# Ensure target is long enough
|
|
46
|
+
target << ("\x00".b * [0, offset + bytes.bytesize - target.bytesize].max)
|
|
47
|
+
target[offset, bytes.bytesize] = bytes
|
|
48
|
+
bytes.bytesize
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Codecs
|
|
6
|
+
# Numeric codecs — mirrors @solana/codecs-numbers.
|
|
7
|
+
# All pack/unpack directives follow Ruby's Array#pack notation.
|
|
8
|
+
# Default endian is :little (Solana is always little-endian on-chain).
|
|
9
|
+
#
|
|
10
|
+
# Fixed sizes (bytes):
|
|
11
|
+
# u8/i8 = 1, u16/i16 = 2, u32/i32 = 4, u64/i64 = 8,
|
|
12
|
+
# u128/i128 = 16, f32 = 4, f64 = 8
|
|
13
|
+
module Numbers
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# ── Unsigned integers ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
sig { returns(Codec) }
|
|
21
|
+
def u8_codec
|
|
22
|
+
enc = Encoder.new(fixed_size: 1) { |v| [Kernel.Integer(v)].pack('C') }
|
|
23
|
+
dec = Decoder.new(fixed_size: 1) do |bytes, offset|
|
|
24
|
+
[bytes.b.byteslice(offset, 1)&.unpack1('C') || 0, 1]
|
|
25
|
+
end
|
|
26
|
+
Codec.new(enc, dec)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
30
|
+
def u16_codec(endian: :little)
|
|
31
|
+
dir = endian == :little ? 'v' : 'n'
|
|
32
|
+
enc = Encoder.new(fixed_size: 2) { |v| [Kernel.Integer(v)].pack(dir) }
|
|
33
|
+
dec = Decoder.new(fixed_size: 2) do |bytes, offset|
|
|
34
|
+
[bytes.b.byteslice(offset, 2)&.unpack1(dir) || 0, 2]
|
|
35
|
+
end
|
|
36
|
+
Codec.new(enc, dec)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
40
|
+
def u32_codec(endian: :little)
|
|
41
|
+
dir = endian == :little ? 'V' : 'N'
|
|
42
|
+
enc = Encoder.new(fixed_size: 4) { |v| [Kernel.Integer(v)].pack(dir) }
|
|
43
|
+
dec = Decoder.new(fixed_size: 4) do |bytes, offset|
|
|
44
|
+
[bytes.b.byteslice(offset, 4)&.unpack1(dir) || 0, 4]
|
|
45
|
+
end
|
|
46
|
+
Codec.new(enc, dec)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
50
|
+
def u64_codec(endian: :little)
|
|
51
|
+
dir = endian == :little ? 'Q<' : 'Q>'
|
|
52
|
+
enc = Encoder.new(fixed_size: 8) { |v| [Kernel.Integer(v)].pack(dir) }
|
|
53
|
+
dec = Decoder.new(fixed_size: 8) do |bytes, offset|
|
|
54
|
+
[bytes.b.byteslice(offset, 8)&.unpack1(dir) || 0, 8]
|
|
55
|
+
end
|
|
56
|
+
Codec.new(enc, dec)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
60
|
+
def u128_codec(endian: :little)
|
|
61
|
+
enc = Encoder.new(fixed_size: 16) do |v|
|
|
62
|
+
n = Kernel.Integer(v)
|
|
63
|
+
if endian == :little
|
|
64
|
+
bytes = []
|
|
65
|
+
16.times { bytes << (n & 0xFF); n >>= 8 }
|
|
66
|
+
bytes.pack('C*')
|
|
67
|
+
else
|
|
68
|
+
bytes = []
|
|
69
|
+
16.times { bytes.unshift(n & 0xFF); n >>= 8 }
|
|
70
|
+
bytes.pack('C*')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
dec = Decoder.new(fixed_size: 16) do |bytes, offset|
|
|
74
|
+
slice = bytes.b.byteslice(offset, 16) || ("\x00" * 16).b
|
|
75
|
+
arr = T.cast(T.unsafe(slice).unpack('C*'), T::Array[Integer])
|
|
76
|
+
n = if endian == :little
|
|
77
|
+
arr.reverse.reduce(0) { |acc, b| (acc << 8) | b }
|
|
78
|
+
else
|
|
79
|
+
arr.reduce(0) { |acc, b| (acc << 8) | b }
|
|
80
|
+
end
|
|
81
|
+
[n, 16]
|
|
82
|
+
end
|
|
83
|
+
Codec.new(enc, dec)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ── Signed integers ──────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
sig { returns(Codec) }
|
|
89
|
+
def i8_codec
|
|
90
|
+
enc = Encoder.new(fixed_size: 1) { |v| [Kernel.Integer(v)].pack('c') }
|
|
91
|
+
dec = Decoder.new(fixed_size: 1) do |bytes, offset|
|
|
92
|
+
[bytes.b.byteslice(offset, 1)&.unpack1('c') || 0, 1]
|
|
93
|
+
end
|
|
94
|
+
Codec.new(enc, dec)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
98
|
+
def i16_codec(endian: :little)
|
|
99
|
+
dir = endian == :little ? 's<' : 's>'
|
|
100
|
+
enc = Encoder.new(fixed_size: 2) { |v| [Kernel.Integer(v)].pack(dir) }
|
|
101
|
+
dec = Decoder.new(fixed_size: 2) do |bytes, offset|
|
|
102
|
+
[bytes.b.byteslice(offset, 2)&.unpack1(dir) || 0, 2]
|
|
103
|
+
end
|
|
104
|
+
Codec.new(enc, dec)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
108
|
+
def i32_codec(endian: :little)
|
|
109
|
+
dir = endian == :little ? 'l<' : 'l>'
|
|
110
|
+
enc = Encoder.new(fixed_size: 4) { |v| [Kernel.Integer(v)].pack(dir) }
|
|
111
|
+
dec = Decoder.new(fixed_size: 4) do |bytes, offset|
|
|
112
|
+
[bytes.b.byteslice(offset, 4)&.unpack1(dir) || 0, 4]
|
|
113
|
+
end
|
|
114
|
+
Codec.new(enc, dec)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
118
|
+
def i64_codec(endian: :little)
|
|
119
|
+
dir = endian == :little ? 'q<' : 'q>'
|
|
120
|
+
enc = Encoder.new(fixed_size: 8) { |v| [Kernel.Integer(v)].pack(dir) }
|
|
121
|
+
dec = Decoder.new(fixed_size: 8) do |bytes, offset|
|
|
122
|
+
[bytes.b.byteslice(offset, 8)&.unpack1(dir) || 0, 8]
|
|
123
|
+
end
|
|
124
|
+
Codec.new(enc, dec)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
128
|
+
def i128_codec(endian: :little)
|
|
129
|
+
enc = Encoder.new(fixed_size: 16) do |v|
|
|
130
|
+
n = Kernel.Integer(v)
|
|
131
|
+
# Two's complement for negative numbers
|
|
132
|
+
n += (1 << 128) if n.negative?
|
|
133
|
+
if endian == :little
|
|
134
|
+
bytes = []
|
|
135
|
+
16.times { bytes << (n & 0xFF); n >>= 8 }
|
|
136
|
+
bytes.pack('C*')
|
|
137
|
+
else
|
|
138
|
+
bytes = []
|
|
139
|
+
16.times { bytes.unshift(n & 0xFF); n >>= 8 }
|
|
140
|
+
bytes.pack('C*')
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
dec = Decoder.new(fixed_size: 16) do |bytes, offset|
|
|
144
|
+
slice = bytes.b.byteslice(offset, 16) || ("\x00" * 16).b
|
|
145
|
+
arr = T.cast(T.unsafe(slice).unpack('C*'), T::Array[Integer])
|
|
146
|
+
n = if endian == :little
|
|
147
|
+
arr.reverse.reduce(0) { |acc, b| (acc << 8) | b }
|
|
148
|
+
else
|
|
149
|
+
arr.reduce(0) { |acc, b| (acc << 8) | b }
|
|
150
|
+
end
|
|
151
|
+
# Convert from unsigned to signed 128-bit
|
|
152
|
+
n -= (1 << 128) if n >= (1 << 127)
|
|
153
|
+
[n, 16]
|
|
154
|
+
end
|
|
155
|
+
Codec.new(enc, dec)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ── Floating point ───────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
161
|
+
def f32_codec(endian: :little)
|
|
162
|
+
dir = endian == :little ? 'e' : 'g'
|
|
163
|
+
enc = Encoder.new(fixed_size: 4) { |v| [Kernel.Float(v)].pack(dir) }
|
|
164
|
+
dec = Decoder.new(fixed_size: 4) do |bytes, offset|
|
|
165
|
+
[bytes.b.byteslice(offset, 4)&.unpack1(dir) || 0.0, 4]
|
|
166
|
+
end
|
|
167
|
+
Codec.new(enc, dec)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
sig { params(endian: Symbol).returns(Codec) }
|
|
171
|
+
def f64_codec(endian: :little)
|
|
172
|
+
dir = endian == :little ? 'E' : 'G'
|
|
173
|
+
enc = Encoder.new(fixed_size: 8) { |v| [Kernel.Float(v)].pack(dir) }
|
|
174
|
+
dec = Decoder.new(fixed_size: 8) do |bytes, offset|
|
|
175
|
+
[bytes.b.byteslice(offset, 8)&.unpack1(dir) || 0.0, 8]
|
|
176
|
+
end
|
|
177
|
+
Codec.new(enc, dec)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# ── Short vector (Solana compact-u16) ────────────────────────────────────
|
|
181
|
+
# Variable-length encoding used in transaction wire format.
|
|
182
|
+
# Each byte uses the 7 low bits for data and bit 7 as a continuation flag.
|
|
183
|
+
|
|
184
|
+
sig { returns(Codec) }
|
|
185
|
+
def compact_u16_codec
|
|
186
|
+
enc = Encoder.new do |v|
|
|
187
|
+
n = Kernel.Integer(v)
|
|
188
|
+
Kernel.raise ArgumentError, "compact_u16 value out of range: #{n}" if n > 0xFFFF || n.negative?
|
|
189
|
+
|
|
190
|
+
bytes = []
|
|
191
|
+
Kernel.loop do
|
|
192
|
+
low7 = n & 0x7F
|
|
193
|
+
n >>= 7
|
|
194
|
+
bytes << (n.positive? ? (low7 | 0x80) : low7)
|
|
195
|
+
break if n.zero?
|
|
196
|
+
end
|
|
197
|
+
bytes.pack('C*')
|
|
198
|
+
end
|
|
199
|
+
dec = Decoder.new do |bytes, offset|
|
|
200
|
+
b = bytes.b
|
|
201
|
+
n = 0
|
|
202
|
+
shift = 0
|
|
203
|
+
consumed = 0
|
|
204
|
+
Kernel.loop do
|
|
205
|
+
byte = b.byteslice(offset + consumed, 1)&.unpack1('C') || 0
|
|
206
|
+
consumed += 1
|
|
207
|
+
n |= (byte & 0x7F) << shift
|
|
208
|
+
shift += 7
|
|
209
|
+
break if (byte & 0x80).zero?
|
|
210
|
+
end
|
|
211
|
+
[n, consumed]
|
|
212
|
+
end
|
|
213
|
+
Codec.new(enc, dec)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|