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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/lib/core_extensions/tapioca/name_patch.rb +32 -0
  3. data/lib/core_extensions/tapioca/required_ancestors.rb +13 -0
  4. data/lib/generators/solana/ruby/kit/install/install_generator.rb +17 -0
  5. data/lib/generators/solana/ruby/kit/install/templates/solana_ruby_kit.rb.tt +8 -0
  6. data/lib/solana/ruby/kit/accounts/account.rb +47 -0
  7. data/lib/solana/ruby/kit/accounts/maybe_account.rb +86 -0
  8. data/lib/solana/ruby/kit/accounts.rb +6 -0
  9. data/lib/solana/ruby/kit/addresses/address.rb +133 -0
  10. data/lib/solana/ruby/kit/addresses/curve.rb +112 -0
  11. data/lib/solana/ruby/kit/addresses/program_derived_address.rb +155 -0
  12. data/lib/solana/ruby/kit/addresses/public_key.rb +39 -0
  13. data/lib/solana/ruby/kit/addresses.rb +11 -0
  14. data/lib/solana/ruby/kit/codecs/bytes.rb +58 -0
  15. data/lib/solana/ruby/kit/codecs/codec.rb +135 -0
  16. data/lib/solana/ruby/kit/codecs/data_structures.rb +177 -0
  17. data/lib/solana/ruby/kit/codecs/decoder.rb +43 -0
  18. data/lib/solana/ruby/kit/codecs/encoder.rb +52 -0
  19. data/lib/solana/ruby/kit/codecs/numbers.rb +217 -0
  20. data/lib/solana/ruby/kit/codecs/strings.rb +116 -0
  21. data/lib/solana/ruby/kit/codecs.rb +25 -0
  22. data/lib/solana/ruby/kit/configuration.rb +48 -0
  23. data/lib/solana/ruby/kit/encoding/base58.rb +62 -0
  24. data/lib/solana/ruby/kit/errors.rb +226 -0
  25. data/lib/solana/ruby/kit/fast_stable_stringify.rb +62 -0
  26. data/lib/solana/ruby/kit/functional.rb +29 -0
  27. data/lib/solana/ruby/kit/instruction_plans/plans.rb +27 -0
  28. data/lib/solana/ruby/kit/instruction_plans.rb +47 -0
  29. data/lib/solana/ruby/kit/instructions/accounts.rb +80 -0
  30. data/lib/solana/ruby/kit/instructions/instruction.rb +71 -0
  31. data/lib/solana/ruby/kit/instructions/roles.rb +84 -0
  32. data/lib/solana/ruby/kit/instructions.rb +7 -0
  33. data/lib/solana/ruby/kit/keys/key_pair.rb +84 -0
  34. data/lib/solana/ruby/kit/keys/private_key.rb +39 -0
  35. data/lib/solana/ruby/kit/keys/public_key.rb +31 -0
  36. data/lib/solana/ruby/kit/keys/signatures.rb +171 -0
  37. data/lib/solana/ruby/kit/keys.rb +11 -0
  38. data/lib/solana/ruby/kit/offchain_messages/codec.rb +107 -0
  39. data/lib/solana/ruby/kit/offchain_messages/message.rb +22 -0
  40. data/lib/solana/ruby/kit/offchain_messages.rb +16 -0
  41. data/lib/solana/ruby/kit/options/option.rb +132 -0
  42. data/lib/solana/ruby/kit/options.rb +5 -0
  43. data/lib/solana/ruby/kit/plugin_core.rb +58 -0
  44. data/lib/solana/ruby/kit/programs.rb +42 -0
  45. data/lib/solana/ruby/kit/promises.rb +85 -0
  46. data/lib/solana/ruby/kit/railtie.rb +18 -0
  47. data/lib/solana/ruby/kit/rpc/api/get_account_info.rb +76 -0
  48. data/lib/solana/ruby/kit/rpc/api/get_balance.rb +41 -0
  49. data/lib/solana/ruby/kit/rpc/api/get_block_height.rb +29 -0
  50. data/lib/solana/ruby/kit/rpc/api/get_epoch_info.rb +47 -0
  51. data/lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb +52 -0
  52. data/lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb +29 -0
  53. data/lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb +56 -0
  54. data/lib/solana/ruby/kit/rpc/api/get_program_accounts.rb +60 -0
  55. data/lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb +56 -0
  56. data/lib/solana/ruby/kit/rpc/api/get_slot.rb +30 -0
  57. data/lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb +38 -0
  58. data/lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb +48 -0
  59. data/lib/solana/ruby/kit/rpc/api/get_transaction.rb +36 -0
  60. data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +62 -0
  61. data/lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb +41 -0
  62. data/lib/solana/ruby/kit/rpc/api/request_airdrop.rb +35 -0
  63. data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +61 -0
  64. data/lib/solana/ruby/kit/rpc/api/simulate_transaction.rb +47 -0
  65. data/lib/solana/ruby/kit/rpc/client.rb +83 -0
  66. data/lib/solana/ruby/kit/rpc/transport.rb +137 -0
  67. data/lib/solana/ruby/kit/rpc.rb +13 -0
  68. data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +33 -0
  69. data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +33 -0
  70. data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +51 -0
  71. data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +52 -0
  72. data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +38 -0
  73. data/lib/solana/ruby/kit/rpc_parsed_types.rb +16 -0
  74. data/lib/solana/ruby/kit/rpc_subscriptions/api/account_notifications.rb +29 -0
  75. data/lib/solana/ruby/kit/rpc_subscriptions/api/logs_notifications.rb +28 -0
  76. data/lib/solana/ruby/kit/rpc_subscriptions/api/program_notifications.rb +30 -0
  77. data/lib/solana/ruby/kit/rpc_subscriptions/api/root_notifications.rb +19 -0
  78. data/lib/solana/ruby/kit/rpc_subscriptions/api/signature_notifications.rb +28 -0
  79. data/lib/solana/ruby/kit/rpc_subscriptions/api/slot_notifications.rb +19 -0
  80. data/lib/solana/ruby/kit/rpc_subscriptions/autopinger.rb +42 -0
  81. data/lib/solana/ruby/kit/rpc_subscriptions/client.rb +80 -0
  82. data/lib/solana/ruby/kit/rpc_subscriptions/subscription.rb +58 -0
  83. data/lib/solana/ruby/kit/rpc_subscriptions/transport.rb +163 -0
  84. data/lib/solana/ruby/kit/rpc_subscriptions.rb +12 -0
  85. data/lib/solana/ruby/kit/rpc_types/account_info.rb +53 -0
  86. data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +56 -0
  87. data/lib/solana/ruby/kit/rpc_types/commitment.rb +52 -0
  88. data/lib/solana/ruby/kit/rpc_types/lamports.rb +43 -0
  89. data/lib/solana/ruby/kit/rpc_types.rb +8 -0
  90. data/lib/solana/ruby/kit/signers/keypair_signer.rb +126 -0
  91. data/lib/solana/ruby/kit/signers.rb +5 -0
  92. data/lib/solana/ruby/kit/subscribable/async_iterable.rb +80 -0
  93. data/lib/solana/ruby/kit/subscribable/data_publisher.rb +90 -0
  94. data/lib/solana/ruby/kit/subscribable.rb +13 -0
  95. data/lib/solana/ruby/kit/sysvars/addresses.rb +19 -0
  96. data/lib/solana/ruby/kit/sysvars/clock.rb +37 -0
  97. data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +34 -0
  98. data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +22 -0
  99. data/lib/solana/ruby/kit/sysvars/rent.rb +29 -0
  100. data/lib/solana/ruby/kit/sysvars.rb +33 -0
  101. data/lib/solana/ruby/kit/transaction_confirmation.rb +159 -0
  102. data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +168 -0
  103. data/lib/solana/ruby/kit/transaction_messages.rb +5 -0
  104. data/lib/solana/ruby/kit/transactions/transaction.rb +135 -0
  105. data/lib/solana/ruby/kit/transactions.rb +5 -0
  106. data/lib/solana/ruby/kit/version.rb +10 -0
  107. data/lib/solana/ruby/kit.rb +100 -0
  108. data/solana-ruby-kit.gemspec +29 -0
  109. 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