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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -12
  3. data/lib/solana/ruby/kit/accounts/maybe_account.rb +1 -1
  4. data/lib/solana/ruby/kit/addresses/address.rb +1 -1
  5. data/lib/solana/ruby/kit/addresses/curve.rb +1 -1
  6. data/lib/solana/ruby/kit/addresses/program_derived_address.rb +1 -1
  7. data/lib/solana/ruby/kit/addresses/public_key.rb +1 -1
  8. data/lib/solana/ruby/kit/codecs/codec.rb +95 -1
  9. data/lib/solana/ruby/kit/codecs/data_structures.rb +51 -8
  10. data/lib/solana/ruby/kit/codecs/numbers.rb +49 -50
  11. data/lib/solana/ruby/kit/codecs/strings.rb +132 -6
  12. data/lib/solana/ruby/kit/encoding/base58.rb +1 -1
  13. data/lib/solana/ruby/kit/errors.rb +12 -0
  14. data/lib/solana/ruby/kit/fast_stable_stringify.rb +1 -1
  15. data/lib/solana/ruby/kit/functional.rb +1 -1
  16. data/lib/solana/ruby/kit/instruction_plans/instruction_plan.rb +1 -1
  17. data/lib/solana/ruby/kit/instruction_plans/max_instructions.rb +1 -1
  18. data/lib/solana/ruby/kit/instruction_plans/transaction_plan.rb +1 -1
  19. data/lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb +107 -2
  20. data/lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb +1 -1
  21. data/lib/solana/ruby/kit/instruction_plans/transaction_planner.rb +2 -2
  22. data/lib/solana/ruby/kit/instructions/accounts.rb +1 -1
  23. data/lib/solana/ruby/kit/instructions/instruction.rb +1 -1
  24. data/lib/solana/ruby/kit/instructions/roles.rb +1 -1
  25. data/lib/solana/ruby/kit/keys/key_pair.rb +1 -1
  26. data/lib/solana/ruby/kit/keys/private_key.rb +1 -1
  27. data/lib/solana/ruby/kit/keys/public_key.rb +1 -1
  28. data/lib/solana/ruby/kit/keys/signatures.rb +1 -1
  29. data/lib/solana/ruby/kit/offchain_messages/codec.rb +1 -1
  30. data/lib/solana/ruby/kit/offchain_messages/message_v1.rb +1 -1
  31. data/lib/solana/ruby/kit/options/option.rb +1 -1
  32. data/lib/solana/ruby/kit/plugin_core.rb +1 -1
  33. data/lib/solana/ruby/kit/programs/associated_token_account.rb +1 -1
  34. data/lib/solana/ruby/kit/programs/stake_program.rb +1 -1
  35. data/lib/solana/ruby/kit/programs/system_program.rb +1 -1
  36. data/lib/solana/ruby/kit/programs.rb +1 -1
  37. data/lib/solana/ruby/kit/promises.rb +1 -1
  38. data/lib/solana/ruby/kit/resource_limit_estimation.rb +1 -1
  39. data/lib/solana/ruby/kit/rpc/api/get_ag_genesis_cert.rb +72 -0
  40. data/lib/solana/ruby/kit/rpc/client.rb +2 -0
  41. data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +1 -1
  42. data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +1 -1
  43. data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +1 -1
  44. data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +1 -1
  45. data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +1 -1
  46. data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +1 -1
  47. data/lib/solana/ruby/kit/rpc_types/commitment.rb +1 -1
  48. data/lib/solana/ruby/kit/rpc_types/lamports.rb +1 -1
  49. data/lib/solana/ruby/kit/rpc_types/sol.rb +1 -1
  50. data/lib/solana/ruby/kit/signers/keypair_signer.rb +1 -1
  51. data/lib/solana/ruby/kit/subscribable/async_iterable.rb +1 -1
  52. data/lib/solana/ruby/kit/subscribable/bridge_store_to_async_iterable.rb +1 -1
  53. data/lib/solana/ruby/kit/subscribable/reactive_action_store.rb +1 -1
  54. data/lib/solana/ruby/kit/subscribable/reactive_stream_store.rb +1 -1
  55. data/lib/solana/ruby/kit/sysvars/clock.rb +1 -1
  56. data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +1 -1
  57. data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +1 -1
  58. data/lib/solana/ruby/kit/sysvars/rent.rb +1 -1
  59. data/lib/solana/ruby/kit/sysvars.rb +1 -1
  60. data/lib/solana/ruby/kit/transaction_confirmation.rb +1 -1
  61. data/lib/solana/ruby/kit/transaction_introspection/compiled_transaction_message.rb +1 -1
  62. data/lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb +1 -1
  63. data/lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb +1 -1
  64. data/lib/solana/ruby/kit/transaction_introspection/get_instructions.rb +1 -1
  65. data/lib/solana/ruby/kit/transaction_introspection/walk_instructions.rb +1 -1
  66. data/lib/solana/ruby/kit/transaction_messages/compute_budget.rb +3 -1
  67. data/lib/solana/ruby/kit/transaction_messages/resource_limit_validation.rb +70 -0
  68. data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +1 -1
  69. data/lib/solana/ruby/kit/transaction_messages.rb +1 -0
  70. data/lib/solana/ruby/kit/transactions/compiler.rb +2 -2
  71. data/lib/solana/ruby/kit/transactions/transaction.rb +30 -10
  72. data/lib/solana/ruby/kit/version.rb +1 -1
  73. data/lib/solana/ruby/kit/wallet_standard.rb +3 -3
  74. metadata +3 -1
@@ -15,14 +15,134 @@ module Solana::Ruby::Kit
15
15
  # defeated the "directly available as Codecs.x" intent in codecs.rb.
16
16
  extend self
17
17
 
18
+ # Byte order mark, U+FEFF, in its UTF-8 encoding.
19
+ BOM = T.let("\xEF\xBB\xBF".b.freeze, String)
20
+
21
+ # Remove every null character from a decoded string.
22
+ # Mirrors `removeNullCharacters`.
23
+ sig { params(value: String).returns(String) }
24
+ def remove_null_characters(value)
25
+ value.delete("\x00")
26
+ end
27
+
28
+ # Find the first malformed UTF-8 sequence in +bytes+, starting at +offset+.
29
+ # Returns the byte offset at which it starts, or -1 when the bytes are
30
+ # well-formed.
31
+ #
32
+ # This rejects everything the Unicode standard rejects: unexpected
33
+ # continuation bytes, overlong encodings, encoded surrogates, code points
34
+ # above U+10FFFF and truncated sequences. Ruby's own +valid_encoding?+
35
+ # answers the same question, but only yes or no - this reports *where*,
36
+ # which is what the error context needs.
37
+ # Mirrors `findMalformedUtf8SequenceOffset`.
38
+ sig { params(bytes: String, offset: Integer).returns(Integer) }
39
+ def find_malformed_utf8_sequence_offset(bytes, offset = 0)
40
+ arr = T.cast(T.unsafe(bytes.b).unpack('C*'), T::Array[Integer])
41
+ length = arr.length
42
+ index = offset
43
+ while index < length
44
+ lead = T.must(arr[index])
45
+ min = 0x80
46
+ max = 0xBF
47
+ if lead < 0x80
48
+ index += 1
49
+ next
50
+ elsif lead >= 0xC2 && lead <= 0xDF
51
+ count = 1
52
+ elsif lead >= 0xE0 && lead <= 0xEF
53
+ count = 2
54
+ min = 0xA0 if lead == 0xE0 # Overlong.
55
+ max = 0x9F if lead == 0xED # Encoded surrogate.
56
+ elsif lead >= 0xF0 && lead <= 0xF4
57
+ count = 3
58
+ min = 0x90 if lead == 0xF0 # Overlong.
59
+ max = 0x8F if lead == 0xF4 # Above U+10FFFF.
60
+ else
61
+ return index
62
+ end
63
+
64
+ start = index
65
+ index += 1
66
+ count.times do |i|
67
+ return start if index >= length
68
+
69
+ byte = T.must(arr[index])
70
+ in_range = i.zero? ? byte >= min && byte <= max : byte >= 0x80 && byte <= 0xBF
71
+ return start unless in_range
72
+
73
+ index += 1
74
+ end
75
+ end
76
+ -1
77
+ end
78
+
79
+ # Raise unless +bytes+, from +offset+, form well-formed UTF-8.
80
+ # Mirrors `assertIsWellFormedUtf8Bytes`.
81
+ sig { params(bytes: String, offset: Integer).void }
82
+ def assert_is_well_formed_utf8_bytes(bytes, offset = 0)
83
+ malformed = find_malformed_utf8_sequence_offset(bytes, offset)
84
+ return if malformed == -1
85
+
86
+ Kernel.raise SolanaError.new(
87
+ SolanaError::CODECS__INVALID_UTF8_BYTES,
88
+ { bytes: bytes.b, offset: malformed }
89
+ )
90
+ end
91
+
92
+ # Raise unless +value+ can be encoded as UTF-8 without loss.
93
+ # Mirrors `assertIsWellFormedUtf8String`.
94
+ #
95
+ # Upstream guards against lone surrogates, which a JavaScript string can
96
+ # hold because it is a sequence of UTF-16 code units. A Ruby String is a
97
+ # byte sequence with an encoding tag, so the equivalent flaw is a String
98
+ # whose bytes are not valid UTF-8. Upstream's reported +index+ counts
99
+ # UTF-16 code units; the +index+ here is a byte offset.
100
+ sig { params(value: String).void }
101
+ def assert_is_well_formed_utf8_string(value)
102
+ index = find_malformed_utf8_sequence_offset(value.b)
103
+ return if index == -1
104
+
105
+ Kernel.raise SolanaError.new(
106
+ SolanaError::CODECS__INVALID_UTF8_STRING,
107
+ { index: index, value: value }
108
+ )
109
+ end
110
+
18
111
  # UTF-8 string codec.
19
112
  # When +size+ is given the encoded bytes are fixed to that length
20
113
  # (zero-padded or truncated); otherwise the codec is variable-length
21
114
  # and must be used inside a size-prefixed container.
22
- sig { params(size: T.nilable(Integer)).returns(Codec) }
23
- def utf8_codec(size: nil)
115
+ #
116
+ # +fatal+ rejects invalid UTF-8 instead of passing it through: on encode a
117
+ # String whose bytes are not valid UTF-8, on decode a malformed byte
118
+ # sequence.
119
+ #
120
+ # +ignore_bom+ follows TextDecoder's confusing spelling: the default,
121
+ # +false+, *strips* a leading byte order mark; +true+ keeps it.
122
+ #
123
+ # +remove_null_characters+ strips every null character from the decoded
124
+ # string, which is what makes fixed-size padded strings read back cleanly.
125
+ # It is on by default, matching upstream, and makes the codec lossy for
126
+ # strings that legitimately contain nulls - pass +false+ for a lossless
127
+ # round trip.
128
+ sig do
129
+ params(
130
+ size: T.nilable(Integer),
131
+ fatal: T::Boolean,
132
+ ignore_bom: T::Boolean,
133
+ remove_null_characters: T::Boolean
134
+ ).returns(Codec)
135
+ end
136
+ def utf8_codec(size: nil, fatal: false, ignore_bom: false, remove_null_characters: true)
137
+ strip_nulls = remove_null_characters
24
138
  enc = Encoder.new(fixed_size: size) do |v|
25
- raw = v.to_s.encode('UTF-8').b
139
+ str = v.to_s
140
+ # Transcode anything that is not already UTF-8 (or raw bytes); a
141
+ # UTF-8 to UTF-8 `encode` is a no-op and would not validate, which is
142
+ # what `fatal` is for.
143
+ str = str.encode(::Encoding::UTF_8) unless [::Encoding::UTF_8, ::Encoding::BINARY].include?(str.encoding)
144
+ raw = str.b
145
+ assert_is_well_formed_utf8_string(raw) if fatal
26
146
  if size
27
147
  raw.bytesize <= size ? raw.ljust(size, "\x00") : raw.byteslice(0, size) || ''.b
28
148
  else
@@ -32,9 +152,15 @@ module Solana::Ruby::Kit
32
152
  dec = Decoder.new(fixed_size: size) do |bytes, offset|
33
153
  len = size || (bytes.bytesize - offset)
34
154
  slice = bytes.b.byteslice(offset, len) || ''.b
35
- # Strip null padding for fixed-size strings
36
- str = size ? slice.delete_suffix("\x00" * slice.bytesize.times.take_while { |i| slice.b[-1 - i] == "\x00" }.length) : slice
37
- [str.force_encoding('UTF-8'), len]
155
+ assert_is_well_formed_utf8_bytes(slice) if fatal
156
+ slice = T.must(slice.byteslice(BOM.bytesize..)) if !ignore_bom && slice.start_with?(BOM)
157
+ str = slice.force_encoding(::Encoding::UTF_8)
158
+ # Non-fatal decoding substitutes U+FFFD for malformed sequences, as
159
+ # TextDecoder does. `scrub` is Ruby's version of exactly that, and it
160
+ # also keeps the null-stripping below from raising on invalid bytes.
161
+ str = str.scrub unless fatal
162
+ str = remove_null_characters(str) if strip_nulls
163
+ [str, len]
38
164
  end
39
165
  Codec.new(enc, dec)
40
166
  end
@@ -13,7 +13,7 @@ module Solana::Ruby::Kit
13
13
  String
14
14
  )
15
15
 
16
- module_function
16
+ extend self
17
17
 
18
18
  # Encodes a binary String to a base58 string.
19
19
  sig { params(bytes: String).returns(String) }
@@ -60,6 +60,10 @@ module Solana::Ruby::Kit
60
60
  TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE
61
61
  # context: { unsupported_version: }
62
62
  TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED = :SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED
63
+ # context: { compute_unit_limit:, max_compute_unit_limit: }
64
+ TRANSACTION__COMPUTE_UNIT_LIMIT_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTION__COMPUTE_UNIT_LIMIT_OUT_OF_RANGE
65
+ # context: { heap_size:, max_heap_size:, min_heap_size:, multiple_of: }
66
+ TRANSACTION__INVALID_HEAP_SIZE = :SOLANA_ERROR__TRANSACTION__INVALID_HEAP_SIZE
63
67
  TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE = :SOLANA_ERROR__TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE
64
68
  TRANSACTIONS__BLOCKHASH_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__BLOCKHASH_NOT_FOUND
65
69
  TRANSACTIONS__FAILED_TRANSACTION_PLAN = :SOLANA_ERROR__TRANSACTIONS__FAILED_TRANSACTION_PLAN
@@ -91,6 +95,10 @@ module Solana::Ruby::Kit
91
95
  CODECS__INVALID_BYTE_LENGTH = :SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH
92
96
  CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR = :SOLANA_ERROR__CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR
93
97
  CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC = :SOLANA_ERROR__CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC
98
+ # context: { bytes:, offset: }
99
+ CODECS__INVALID_UTF8_BYTES = :SOLANA_ERROR__CODECS__INVALID_UTF8_BYTES
100
+ # context: { index:, value: }
101
+ CODECS__INVALID_UTF8_STRING = :SOLANA_ERROR__CODECS__INVALID_UTF8_STRING
94
102
 
95
103
  # ── RPC / JSON-RPC ────────────────────────────────────────────────────────
96
104
  RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
@@ -207,6 +215,8 @@ module Solana::Ruby::Kit
207
215
  TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found at index %{index}',
208
216
  TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE => 'Failed to decompile instruction: account index %{index} is out of range',
209
217
  TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED => 'Transaction version %{unsupported_version} is not supported',
218
+ TRANSACTION__COMPUTE_UNIT_LIMIT_OUT_OF_RANGE => 'Transaction compute unit limit must be an integer in the range [0, %{max_compute_unit_limit}]. `%{compute_unit_limit}` given',
219
+ TRANSACTION__INVALID_HEAP_SIZE => 'Transaction heap size must be an integer multiple of %{multiple_of} bytes in the range [%{min_heap_size}, %{max_heap_size}]. `%{heap_size}` given',
210
220
  TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE => 'Transaction simulation failed: %{message}',
211
221
  TRANSACTIONS__BLOCKHASH_NOT_FOUND => 'Blockhash not found',
212
222
  TRANSACTIONS__FAILED_TRANSACTION_PLAN => 'Failed to execute transaction plan',
@@ -238,6 +248,8 @@ module Solana::Ruby::Kit
238
248
  CODECS__INVALID_BYTE_LENGTH => 'Expected %{expected} bytes but got %{actual}',
239
249
  CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR => 'Expected zero value to match fixed discriminator',
240
250
  CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC => 'A fixed-size nullable codec cannot wrap a variable-size codec',
251
+ CODECS__INVALID_UTF8_BYTES => 'Invalid UTF-8 byte sequence at offset %{offset}',
252
+ CODECS__INVALID_UTF8_STRING => 'Invalid UTF-8 string at index %{index}',
241
253
 
242
254
  # RPC
243
255
  RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER => 'Integer overflow while serializing large integer %{value}',
@@ -8,7 +8,7 @@ module Solana::Ruby::Kit
8
8
  module FastStableStringify
9
9
  extend T::Sig
10
10
 
11
- module_function
11
+ extend self
12
12
 
13
13
  # Serialize +value+ to a deterministic JSON string.
14
14
  # Hash keys are sorted lexicographically at every level.
@@ -6,7 +6,7 @@ module Solana::Ruby::Kit
6
6
  # Mirrors the TypeScript package @solana/functional.
7
7
  module Functional
8
8
  extend T::Sig
9
- module_function
9
+ extend self
10
10
 
11
11
  # Passes an initial value through a series of single-argument callables,
12
12
  # returning the final result.
@@ -97,7 +97,7 @@ module Solana::Ruby::Kit
97
97
 
98
98
  # ── Factory helpers ────────────────────────────────────────────────────────
99
99
 
100
- module_function
100
+ extend self
101
101
 
102
102
  # Wraps a single instruction in a plan.
103
103
  # Mirrors `singleInstructionPlan(instruction)`.
@@ -16,7 +16,7 @@ module Solana::Ruby::Kit
16
16
  # The hard maximum number of top-level instructions the transaction format can encode.
17
17
  TRANSACTION_INSTRUCTION_LIMIT = T.let(64, Integer)
18
18
 
19
- module_function
19
+ extend self
20
20
 
21
21
  # Resolves the effective maximum number of instructions allowed in a transaction message,
22
22
  # falling back to the default when no value is provided.
@@ -41,7 +41,7 @@ module Solana::Ruby::Kit
41
41
  def kind = :parallel
42
42
  end
43
43
 
44
- module_function
44
+ extend self
45
45
 
46
46
  # Creates a single-message plan.
47
47
  # Mirrors `singleTransactionPlan(transactionMessage)`.
@@ -9,7 +9,7 @@ module Solana::Ruby::Kit
9
9
  module InstructionPlans
10
10
  extend T::Sig
11
11
 
12
- module_function
12
+ extend self
13
13
 
14
14
  # Creates a TransactionPlanExecutor — a callable that traverses a TransactionPlan,
15
15
  # executing each single transaction message and collecting results.
@@ -58,9 +58,60 @@ module Solana::Ruby::Kit
58
58
  }
59
59
  end
60
60
 
61
+ # Creates a TransactionPlanExecutor that processes every leaf concurrently.
62
+ #
63
+ # It takes the same configuration as +create_transaction_plan_executor+ and its
64
+ # +execute_transaction_message+ callback follows the same contract: it receives a fresh
65
+ # mutable +context+ Hash for every leaf and returns the Hash a successful result should
66
+ # carry. On success the two are merged with the returned value winning; when the callback
67
+ # raises, the context accumulated so far is preserved on the failed result. The legacy
68
+ # one-argument callable shape is accepted here too.
69
+ #
70
+ # The difference is the traversal. This executor preserves the input plan's nesting, order
71
+ # and divisibility in the returned result, but it does not enforce the execution
72
+ # dependencies expressed by sequential plans: every leaf is started immediately, each on
73
+ # its own thread, with no concurrency limit. That makes it suitable for work that can be
74
+ # performed independently, such as signing or serializing transactions. For the same
75
+ # reason a callback that raises does not cancel the other leaves — each one runs to
76
+ # completion — and non-divisible sequential plans are supported rather than rejected.
77
+ #
78
+ # Once every leaf has settled, a result tree containing any failure raises
79
+ # INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN carrying the complete tree on its
80
+ # +transaction_plan_result+ reader.
81
+ #
82
+ # Because each leaf runs on its own thread, the callback must be safe to run concurrently
83
+ # with itself. Each leaf gets its own context Hash, so the contexts do not need guarding,
84
+ # but anything the callback shares between invocations does.
85
+ #
86
+ # Mirrors `createTransactionPlanExecutorWithConcurrentLeaves(config)` from
87
+ # @solana/instruction-plans.
88
+ # NOTE: Ruby translation has no abort signal; leaves are never canceled, only failed.
89
+ sig { params(execute_transaction_message: T.untyped).returns(T.untyped) }
90
+ def create_transaction_plan_executor_with_concurrent_leaves(execute_transaction_message:)
91
+ ->(transaction_plan) {
92
+ # Traversal spawns every leaf's thread on the way down and returns a tree of thunks;
93
+ # resolving them afterwards is what joins the threads, so all leaves are already in
94
+ # flight before the first one is waited on.
95
+ result = concurrent_executor_traverse(transaction_plan, execute_transaction_message).call
96
+
97
+ cause = executor_find_error(result)
98
+ if cause
99
+ err = SolanaError.new(
100
+ SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
101
+ { cause: cause }
102
+ )
103
+ err.instance_variable_set(:@transaction_plan_result, result)
104
+ err.define_singleton_method(:transaction_plan_result) { result }
105
+ Kernel.raise err
106
+ end
107
+
108
+ result
109
+ }
110
+ end
111
+
61
112
  # ── Private helpers ────────────────────────────────────────────────────────
62
113
 
63
- module_function
114
+ extend self
64
115
 
65
116
  sig { params(plan: T.untyped, execute_fn: T.untyped, state: T.untyped).returns(T.untyped) }
66
117
 
@@ -145,6 +196,58 @@ module Solana::Ruby::Kit
145
196
  returned
146
197
  end
147
198
 
199
+ # Returns a thunk that resolves to this plan's result. Leaf threads are started as the
200
+ # tree is walked, so the whole plan is in flight by the time the outermost thunk is called.
201
+ sig { params(plan: T.untyped, execute_fn: T.untyped).returns(T.untyped) }
202
+
203
+ def concurrent_executor_traverse(plan, execute_fn)
204
+ case plan.kind
205
+ when :single
206
+ thread = Thread.new { concurrent_executor_execute_single(plan, execute_fn) }
207
+ -> { thread.value }
208
+ when :sequential, :parallel
209
+ thunks = plan.plans.map { |sub| concurrent_executor_traverse(sub, execute_fn) }
210
+ Kernel.lambda do
211
+ results = thunks.map(&:call)
212
+ if plan.kind == :parallel
213
+ parallel_transaction_plan_result(results)
214
+ elsif plan.divisible
215
+ sequential_transaction_plan_result(results)
216
+ else
217
+ # Unlike the sequential executor, non-divisible plans are supported: nothing
218
+ # about running every leaf independently depends on the plan being divisible.
219
+ non_divisible_sequential_transaction_plan_result(results)
220
+ end
221
+ end
222
+ else
223
+ Kernel.raise SolanaError.new(
224
+ SolanaError::INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
225
+ { kind: plan.kind }
226
+ )
227
+ end
228
+ end
229
+
230
+ # Runs one leaf. Identical to +executor_traverse_single+ except that a failure is recorded
231
+ # and nothing else — there is no shared cancellation state for it to trip.
232
+ sig { params(plan: T.untyped, execute_fn: T.untyped).returns(T.untyped) }
233
+
234
+ def concurrent_executor_execute_single(plan, execute_fn)
235
+ context = {}
236
+
237
+ begin
238
+ returned = executor_invoke(execute_fn, context, plan.message)
239
+ successful_single_transaction_plan_result(plan.message, nil, context.merge(returned))
240
+ rescue SolanaError => e
241
+ failed_single_transaction_plan_result(plan.message, e, context)
242
+ rescue => e
243
+ wrapped = SolanaError.new(
244
+ SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
245
+ { cause: e }
246
+ )
247
+ failed_single_transaction_plan_result(plan.message, wrapped, context)
248
+ end
249
+ end
250
+
148
251
  sig { params(result: T.untyped).returns(T.untyped) }
149
252
 
150
253
  def executor_find_error(result)
@@ -164,5 +267,7 @@ module Solana::Ruby::Kit
164
267
  private_class_method :executor_traverse_single
165
268
  private_class_method :executor_invoke
166
269
  private_class_method :executor_find_error
270
+ private_class_method :concurrent_executor_traverse
271
+ private_class_method :concurrent_executor_execute_single
167
272
  end
168
273
  end
@@ -74,7 +74,7 @@ module Solana::Ruby::Kit
74
74
  def kind = :parallel
75
75
  end
76
76
 
77
- module_function
77
+ extend self
78
78
 
79
79
  # ── Result factory helpers ────────────────────────────────────────────────
80
80
 
@@ -11,7 +11,7 @@ module Solana::Ruby::Kit
11
11
  module InstructionPlans
12
12
  extend T::Sig
13
13
 
14
- module_function
14
+ extend self
15
15
 
16
16
  # Creates a TransactionPlanner — a callable that converts an InstructionPlan
17
17
  # into a TransactionPlan by packing instructions into transaction messages.
@@ -71,7 +71,7 @@ module Solana::Ruby::Kit
71
71
 
72
72
  # ── Private helpers ────────────────────────────────────────────────────────
73
73
 
74
- module_function
74
+ extend self
75
75
 
76
76
  sig { params(plan: T.untyped, ctx: T.untyped).returns(T.untyped) }
77
77
 
@@ -19,7 +19,7 @@ module Solana::Ruby::Kit
19
19
 
20
20
  # AccountMeta subtypes — mirrors the four TypeScript convenience types.
21
21
 
22
- module_function
22
+ extend self
23
23
 
24
24
  # Creates a read-only account reference.
25
25
  sig { params(address: Addresses::Address).returns(AccountMeta) }
@@ -18,7 +18,7 @@ module Solana::Ruby::Kit
18
18
  const :data, T.nilable(String) # binary String (Uint8Array equivalent)
19
19
  end
20
20
 
21
- module_function
21
+ extend self
22
22
 
23
23
  # Returns true if the instruction targets the given program address.
24
24
  # Mirrors `isInstructionForProgram()`.
@@ -19,7 +19,7 @@ module Solana::Ruby::Kit
19
19
 
20
20
  ALL = T.let([READONLY, WRITABLE, READONLY_SIGNER, WRITABLE_SIGNER].freeze, T::Array[Integer])
21
21
 
22
- module_function
22
+ extend self
23
23
 
24
24
  # Returns true if the role requires the account to sign the transaction.
25
25
  sig { params(role: Integer).returns(T::Boolean) }
@@ -24,7 +24,7 @@ module Solana::Ruby::Kit
24
24
  const :verify_key, T.untyped # RbNaCl::VerifyKey
25
25
  end
26
26
 
27
- module_function
27
+ extend self
28
28
 
29
29
  # Generates a fresh Ed25519 key pair using a cryptographically secure RNG.
30
30
  # Mirrors `generateKeyPair()` in TypeScript.
@@ -7,7 +7,7 @@ require_relative '../errors'
7
7
  module Solana::Ruby::Kit
8
8
  module Keys
9
9
  extend T::Sig
10
- module_function
10
+ extend self
11
11
 
12
12
  # Creates an Ed25519 signing key from a raw 32-byte private key seed.
13
13
  #
@@ -7,7 +7,7 @@ require_relative '../errors'
7
7
  module Solana::Ruby::Kit
8
8
  module Keys
9
9
  extend T::Sig
10
- module_function
10
+ extend self
11
11
 
12
12
  # Derives the Ed25519 verification (public) key from a signing (private) key.
13
13
  #
@@ -60,7 +60,7 @@ module Solana::Ruby::Kit
60
60
  SIGNATURE_MIN_STR_LEN = 64
61
61
  SIGNATURE_MAX_STR_LEN = 88
62
62
 
63
- module_function
63
+ extend self
64
64
 
65
65
  # Returns true if the string is a valid base58-encoded Ed25519 signature.
66
66
  # Mirrors `isSignature()` in TypeScript.
@@ -15,7 +15,7 @@ module Solana::Ruby::Kit
15
15
 
16
16
  MAGIC = T.let("\xFF\xFF".b.freeze, String)
17
17
 
18
- module_function
18
+ extend self
19
19
 
20
20
  # Serialize a Message to its canonical binary form.
21
21
  sig { params(msg: Message).returns(String) }
@@ -36,7 +36,7 @@ module Solana::Ruby::Kit
36
36
  def version = 1
37
37
  end
38
38
 
39
- module_function
39
+ extend self
40
40
 
41
41
  # Asserts that a version 1 offchain message received from an untrusted source is the
42
42
  # message it was expected to be.
@@ -57,7 +57,7 @@ module Solana::Ruby::Kit
57
57
  # accepts Option<T>, T, or nil — useful for codec input.
58
58
  OptionOrNullable = T.type_alias { T.untyped }
59
59
 
60
- module_function
60
+ extend self
61
61
 
62
62
  # Wraps a value in Some. Mirrors `some<T>(value)`.
63
63
  sig { params(value: T.untyped).returns(Solana::Ruby::Kit::Options::Some[T.untyped]) }
@@ -48,7 +48,7 @@ module Solana::Ruby::Kit
48
48
  module PluginCore
49
49
  extend T::Sig
50
50
 
51
- module_function
51
+ extend self
52
52
 
53
53
  sig { returns(PluginClient) }
54
54
  def create_client
@@ -47,7 +47,7 @@ module Solana::Ruby::Kit
47
47
  Addresses::Address
48
48
  )
49
49
 
50
- module_function
50
+ extend self
51
51
 
52
52
  # Derives the canonical Associated Token Account address (a PDA) for a
53
53
  # given wallet and mint.
@@ -40,7 +40,7 @@ module Solana::Ruby::Kit
40
40
  DISCRIMINATOR_INITIALIZE = T.let(0, Integer)
41
41
  DISCRIMINATOR_DELEGATE = T.let(2, Integer)
42
42
 
43
- module_function
43
+ extend self
44
44
 
45
45
  # Builds the two instructions needed to create and initialise a new stake
46
46
  # account funded from +from+.
@@ -27,7 +27,7 @@ module Solana::Ruby::Kit
27
27
  # Instruction discriminators (u32 little-endian).
28
28
  DISCRIMINATOR_TRANSFER = T.let(2, Integer)
29
29
 
30
- module_function
30
+ extend self
31
31
 
32
32
  # Builds a System Program Transfer instruction that moves +lamports+ from
33
33
  # +sender+ to +recipient+.
@@ -13,7 +13,7 @@ module Solana::Ruby::Kit
13
13
  module Programs
14
14
  extend T::Sig
15
15
 
16
- module_function
16
+ extend self
17
17
 
18
18
  # Returns true when +err+ is a custom program error, optionally matching
19
19
  # a specific error code.
@@ -10,7 +10,7 @@ module Solana::Ruby::Kit
10
10
  module Promises
11
11
  extend T::Sig
12
12
 
13
- module_function
13
+ extend self
14
14
 
15
15
  # Run each callable in a dedicated thread and return the first result.
16
16
  # All losing threads are killed to avoid memory leaks — analogous to the
@@ -19,7 +19,7 @@ module Solana::Ruby::Kit
19
19
 
20
20
  PROVISORY_LIMIT = T.let(0, Integer)
21
21
 
22
- module_function
22
+ extend self
23
23
 
24
24
  # Returns a callable that estimates the resource limits required by a transaction message
25
25
  # by simulating it with maximum limits set, then reading back the actual consumption.