solana-ruby-kit 7.1.1.1 → 8.2.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -0
  3. data/lib/solana/ruby/kit/errors.rb +8 -0
  4. data/lib/solana/ruby/kit/functional.rb +1 -1
  5. data/lib/solana/ruby/kit/instruction_plans/instruction_plan.rb +13 -1
  6. data/lib/solana/ruby/kit/instruction_plans/transaction_plan.rb +9 -0
  7. data/lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb +117 -2
  8. data/lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb +18 -0
  9. data/lib/solana/ruby/kit/instruction_plans/transaction_planner.rb +24 -2
  10. data/lib/solana/ruby/kit/rpc/api/get_account_info.rb +5 -0
  11. data/lib/solana/ruby/kit/rpc/api/get_ag_genesis_cert.rb +72 -0
  12. data/lib/solana/ruby/kit/rpc/api/get_balance.rb +4 -0
  13. data/lib/solana/ruby/kit/rpc/api/get_block_height.rb +5 -0
  14. data/lib/solana/ruby/kit/rpc/api/get_block_time.rb +5 -0
  15. data/lib/solana/ruby/kit/rpc/api/get_epoch_info.rb +5 -0
  16. data/lib/solana/ruby/kit/rpc/api/get_epoch_schedule.rb +5 -0
  17. data/lib/solana/ruby/kit/rpc/api/get_inflation_reward.rb +6 -1
  18. data/lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb +4 -0
  19. data/lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb +5 -0
  20. data/lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb +5 -0
  21. data/lib/solana/ruby/kit/rpc/api/get_program_accounts.rb +5 -0
  22. data/lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb +4 -0
  23. data/lib/solana/ruby/kit/rpc/api/get_signatures_for_address.rb +5 -0
  24. data/lib/solana/ruby/kit/rpc/api/get_slot.rb +5 -0
  25. data/lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb +5 -0
  26. data/lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb +5 -0
  27. data/lib/solana/ruby/kit/rpc/api/get_token_supply.rb +43 -0
  28. data/lib/solana/ruby/kit/rpc/api/get_transaction.rb +5 -0
  29. data/lib/solana/ruby/kit/rpc/api/get_transactions_for_address.rb +5 -0
  30. data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +5 -0
  31. data/lib/solana/ruby/kit/rpc/api/has_transport.rb +25 -0
  32. data/lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb +4 -0
  33. data/lib/solana/ruby/kit/rpc/api/request_airdrop.rb +4 -0
  34. data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +5 -0
  35. data/lib/solana/ruby/kit/rpc/api/simulate_transaction.rb +5 -0
  36. data/lib/solana/ruby/kit/rpc/client.rb +8 -1
  37. data/lib/solana/ruby/kit/rpc_subscriptions/api/account_notifications.rb +5 -0
  38. data/lib/solana/ruby/kit/rpc_subscriptions/api/has_transport.rb +35 -0
  39. data/lib/solana/ruby/kit/rpc_subscriptions/api/logs_notifications.rb +5 -0
  40. data/lib/solana/ruby/kit/rpc_subscriptions/api/program_notifications.rb +5 -0
  41. data/lib/solana/ruby/kit/rpc_subscriptions/api/root_notifications.rb +5 -0
  42. data/lib/solana/ruby/kit/rpc_subscriptions/api/signature_notifications.rb +5 -0
  43. data/lib/solana/ruby/kit/rpc_subscriptions/api/slot_notifications.rb +5 -0
  44. data/lib/solana/ruby/kit/rpc_subscriptions/client.rb +5 -2
  45. data/lib/solana/ruby/kit/rpc_types/sol.rb +8 -6
  46. data/lib/solana/ruby/kit/subscribable/reactive_action_store.rb +6 -6
  47. data/lib/solana/ruby/kit/subscribable/reactive_stream_store.rb +4 -4
  48. data/lib/solana/ruby/kit/transaction_messages/compute_budget.rb +6 -4
  49. data/lib/solana/ruby/kit/transaction_messages/resource_limit_validation.rb +70 -0
  50. data/lib/solana/ruby/kit/transaction_messages.rb +1 -0
  51. data/lib/solana/ruby/kit/version.rb +1 -1
  52. data/lib/solana/ruby/kit/wallet_standard.rb +4 -4
  53. metadata +6 -3
  54. data/lib/core_extensions/tapioca/name_patch.rb +0 -32
  55. data/lib/core_extensions/tapioca/required_ancestors.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa006377351feaf52ae4b75a9979f21065d0ac039f0c5dd4d18a6dc9a916921b
4
- data.tar.gz: 4802bb1454ed72fdca55b226f9fef75f4e766b183be91d69480a5ceddc6d3d66
3
+ metadata.gz: ba7d2659d83b01af6565c7af483364d61e61fcc9761cbca9d63d8d41f6b4d73a
4
+ data.tar.gz: bd8402f845e9d8db5845be0e358676d00e308b2a0777afc1973e8814175e1c1c
5
5
  SHA512:
6
- metadata.gz: 345cfd37b9188dde96eb86dced69885c8de18290e97126ff4e8d328842d44a044d19a5c5e9e9d63124ad792b85b81b892f106b31fccc091dade697f298c91b0e
7
- data.tar.gz: 1b8591236759f984c22122b810ee92796ff9542e6d4c1cba30837a5696e057fb0c065486463223812c0f8c41331389b4604833ebbcef69b53a377229dd5ce153
6
+ metadata.gz: 589ade790ebb62b97f9905e8fe98113efc93deb5a6fad37239d377ab427915838eaa3d461644d213944ee8b80796134b28d5a6da43529a3920bb5037940a20a1
7
+ data.tar.gz: 3c03649c3b4314b93e7321f74b77150cd19bd775535b8431c14b678fdf35d3e7d4703fac43f341c3480ec5031b0c78bd0c0e3b7e385bb01f1822fc57fa5afaec
data/README.md CHANGED
@@ -189,9 +189,17 @@ msg = TxMsg.append_instructions(msg, [instruction])
189
189
  msg = TxMsg.prepend_instructions(msg, [priority_fee_ix])
190
190
 
191
191
  # Compute unit limit (SetComputeUnitLimit instruction from the Compute Budget program)
192
+ # The limit is validated: it must be an integer in [0, MAX_COMPUTE_UNIT_LIMIT] (1,400,000).
193
+ # The runtime silently clamps a larger request rather than failing, so asking for more than
194
+ # the maximum would run on a budget you did not ask for — this raises instead.
192
195
  msg = TxMsg.set_transaction_message_compute_unit_limit(200_000, msg)
193
196
  TxMsg.get_transaction_message_compute_unit_limit(msg) # => 200_000
194
197
 
198
+ # The check is also available on its own, alongside one for heap frame sizes — a whole
199
+ # number of KiB in [MIN_HEAP_SIZE, MAX_HEAP_SIZE] (32 KiB to 256 KiB).
200
+ TxMsg.assert_is_valid_compute_unit_limit(200_000)
201
+ TxMsg.assert_is_valid_heap_size(64 * 1024)
202
+
195
203
  # Loaded accounts data size limit
196
204
  msg = TxMsg.set_transaction_message_loaded_accounts_data_size_limit(64_000, msg)
197
205
  TxMsg.get_transaction_message_loaded_accounts_data_size_limit(msg) # => 64_000
@@ -274,6 +282,7 @@ rpc.get_block_time(slot)
274
282
  rpc.get_inflation_reward([address])
275
283
  rpc.get_signatures_for_address(address)
276
284
  rpc.get_vote_accounts
285
+ rpc.get_ag_genesis_cert # Alpenglow genesis cert, or nil
277
286
  rpc.simulate_transaction(encoded_tx)
278
287
  rpc.send_transaction(encoded_tx)
279
288
  rpc.request_airdrop(address, lamports) # devnet / testnet only
@@ -640,6 +649,21 @@ result = executor.call(transaction_plan)
640
649
  # A one-argument lambda returning { transaction:, context: } — the shape this method
641
650
  # required before v7.1.0 — is still accepted and adapted onto the flow above.
642
651
 
652
+ # ── 3b. Executing every leaf concurrently ────────────────────────────────────
653
+ # For work that has no execution dependencies — signing or serializing, say —
654
+ # create_transaction_plan_executor_with_concurrent_leaves takes the same callback
655
+ # contract but starts every leaf immediately, each on its own thread. It preserves
656
+ # the plan's nesting, order and divisibility in the result, supports non-divisible
657
+ # sequential plans, and cancels nothing: a leaf that raises does not stop the others,
658
+ # so every leaf runs to completion before the failure is reported. Because the leaves
659
+ # run at the same time, the callback must be safe to call concurrently with itself.
660
+ concurrent = Plans.create_transaction_plan_executor_with_concurrent_leaves(
661
+ execute_transaction_message: ->(_context, message) {
662
+ { transaction: Kit::Transactions.compile_transaction_message(message) }
663
+ }
664
+ )
665
+ result = concurrent.call(transaction_plan)
666
+
643
667
  # ── 4. Plan types ─────────────────────────────────────────────────────────────
644
668
  Plans.single_instruction_plan(ix) # wrap one instruction
645
669
  Plans.sequential_instruction_plan([ix1, ix2]) # ordered, divisible
@@ -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
@@ -147,6 +151,7 @@ module Solana::Ruby::Kit
147
151
 
148
152
  # ── Fixed-points ──────────────────────────────────────────────────────────
149
153
  FIXED_POINTS__STRICT_MODE_PRECISION_LOSS = :SOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS
154
+ LAMPORTS_OUT_OF_RANGE = :SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE
150
155
 
151
156
  # ── Invariant violations (internal) ──────────────────────────────────────
152
157
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING
@@ -206,6 +211,8 @@ module Solana::Ruby::Kit
206
211
  TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found at index %{index}',
207
212
  TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE => 'Failed to decompile instruction: account index %{index} is out of range',
208
213
  TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED => 'Transaction version %{unsupported_version} is not supported',
214
+ 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',
215
+ 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',
209
216
  TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE => 'Transaction simulation failed: %{message}',
210
217
  TRANSACTIONS__BLOCKHASH_NOT_FOUND => 'Blockhash not found',
211
218
  TRANSACTIONS__FAILED_TRANSACTION_PLAN => 'Failed to execute transaction plan',
@@ -286,6 +293,7 @@ module Solana::Ruby::Kit
286
293
 
287
294
  # Fixed-points
288
295
  FIXED_POINTS__STRICT_MODE_PRECISION_LOSS => 'Value has more than 9 fractional digits and cannot be represented exactly as a Sol amount',
296
+ LAMPORTS_OUT_OF_RANGE => 'Lamports value out of range; must fit in an unsigned 64-bit integer',
289
297
 
290
298
  # Wallet Standard
291
299
  WALLET_STANDARD__INVALID_WIRE_FORMAT => 'Invalid transaction wire format: %{reason}',
@@ -21,7 +21,7 @@ module Solana::Ruby::Kit
21
21
  # ->(tx) { Solana::Ruby::Kit::TransactionMessages.set_fee_payer(fee_payer_address, tx) },
22
22
  # ->(tx) { Solana::Ruby::Kit::TransactionMessages.set_blockhash_lifetime(blockhash, tx) },
23
23
  # )
24
- sig { params(value: T.untyped, fns: T::Array[T.proc.params(arg0: T.untyped).returns(T.untyped)]).returns(T.untyped) }
24
+ sig { params(value: T.untyped, fns: T.proc.params(arg0: T.untyped).returns(T.untyped)).returns(T.untyped) }
25
25
  def pipe(value, *fns)
26
26
  fns.reduce(value) { |acc, fn| T.unsafe(fn).call(acc) }
27
27
  end
@@ -23,7 +23,10 @@ module Solana::Ruby::Kit
23
23
 
24
24
  # A plan that wraps a single instruction.
25
25
  class SingleInstructionPlan < T::Struct
26
+ extend T::Sig
27
+
26
28
  const :instruction, Instructions::Instruction
29
+ sig { returns(Symbol) }
27
30
  def kind = :single
28
31
  end
29
32
 
@@ -31,21 +34,30 @@ module Solana::Ruby::Kit
31
34
  # +divisible: true+ allows the planner to split children across transactions.
32
35
  # +divisible: false+ means all children should be atomic (one tx or a bundle).
33
36
  class SequentialInstructionPlan < T::Struct
37
+ extend T::Sig
38
+
34
39
  const :plans, T::Array[T.untyped] # Array[InstructionPlan]
35
40
  const :divisible, T::Boolean
41
+ sig { returns(Symbol) }
36
42
  def kind = :sequential
37
43
  end
38
44
 
39
45
  # A plan whose children may execute concurrently or be packed in any order.
40
46
  class ParallelInstructionPlan < T::Struct
47
+ extend T::Sig
48
+
41
49
  const :plans, T::Array[T.untyped] # Array[InstructionPlan]
50
+ sig { returns(Symbol) }
42
51
  def kind = :parallel
43
52
  end
44
53
 
45
54
  # Provides a MessagePacker that dynamically packs instructions into messages.
46
55
  # The +get_message_packer+ proc returns a fresh MessagePacker each call.
47
56
  class MessagePackerInstructionPlan < T::Struct
57
+ extend T::Sig
58
+
48
59
  const :get_message_packer, T.untyped # Proc -> MessagePacker
60
+ sig { returns(Symbol) }
49
61
  def kind = :message_packer
50
62
  end
51
63
 
@@ -199,7 +211,7 @@ module Solana::Ruby::Kit
199
211
  return packed
200
212
  end
201
213
 
202
- next_packed = TransactionMessages.append_instructions(packed, [instructions[i]])
214
+ next_packed = TransactionMessages.append_instructions(packed, [T.must(instructions[i])])
203
215
  size = Transactions.get_transaction_message_size(next_packed)
204
216
 
205
217
  if size > Transactions::TRANSACTION_SIZE_LIMIT
@@ -17,18 +17,27 @@ module Solana::Ruby::Kit
17
17
  # | ParallelTransactionPlan
18
18
 
19
19
  class SingleTransactionPlan < T::Struct
20
+ extend T::Sig
21
+
20
22
  const :message, TransactionMessages::TransactionMessage
23
+ sig { returns(Symbol) }
21
24
  def kind = :single
22
25
  end
23
26
 
24
27
  class SequentialTransactionPlan < T::Struct
28
+ extend T::Sig
29
+
25
30
  const :plans, T::Array[T.untyped] # Array[TransactionPlan]
26
31
  const :divisible, T::Boolean
32
+ sig { returns(Symbol) }
27
33
  def kind = :sequential
28
34
  end
29
35
 
30
36
  class ParallelTransactionPlan < T::Struct
37
+ extend T::Sig
38
+
31
39
  const :plans, T::Array[T.untyped] # Array[TransactionPlan]
40
+ sig { returns(Symbol) }
32
41
  def kind = :parallel
33
42
  end
34
43
 
@@ -9,6 +9,8 @@ module Solana::Ruby::Kit
9
9
  module InstructionPlans
10
10
  extend T::Sig
11
11
 
12
+ module_function
13
+
12
14
  # Creates a TransactionPlanExecutor — a callable that traverses a TransactionPlan,
13
15
  # executing each single transaction message and collecting results.
14
16
  #
@@ -48,7 +50,7 @@ module Solana::Ruby::Kit
48
50
  )
49
51
  # Store the full result tree as a non-enumerable attribute for recovery.
50
52
  err.instance_variable_set(:@transaction_plan_result, result)
51
- err.define_singleton_method(:transaction_plan_result) { @transaction_plan_result }
53
+ err.define_singleton_method(:transaction_plan_result) { result }
52
54
  Kernel.raise err
53
55
  end
54
56
 
@@ -56,12 +58,63 @@ module Solana::Ruby::Kit
56
58
  }
57
59
  end
58
60
 
59
- module_function :create_transaction_plan_executor
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
60
111
 
61
112
  # ── Private helpers ────────────────────────────────────────────────────────
62
113
 
63
114
  module_function
64
115
 
116
+ sig { params(plan: T.untyped, execute_fn: T.untyped, state: T.untyped).returns(T.untyped) }
117
+
65
118
  def executor_traverse(plan, execute_fn, state)
66
119
  case plan.kind
67
120
  when :sequential then executor_traverse_sequential(plan, execute_fn, state)
@@ -75,6 +128,8 @@ module Solana::Ruby::Kit
75
128
  end
76
129
  end
77
130
 
131
+ sig { params(plan: T.untyped, execute_fn: T.untyped, state: T.untyped).returns(T.untyped) }
132
+
78
133
  def executor_traverse_sequential(plan, execute_fn, state)
79
134
  results = plan.plans.map { |sub| executor_traverse(sub, execute_fn, state) }
80
135
  plan.divisible \
@@ -82,11 +137,15 @@ module Solana::Ruby::Kit
82
137
  : non_divisible_sequential_transaction_plan_result(results)
83
138
  end
84
139
 
140
+ sig { params(plan: T.untyped, execute_fn: T.untyped, state: T.untyped).returns(T.untyped) }
141
+
85
142
  def executor_traverse_parallel(plan, execute_fn, state)
86
143
  results = plan.plans.map { |sub| executor_traverse(sub, execute_fn, state) }
87
144
  parallel_transaction_plan_result(results)
88
145
  end
89
146
 
147
+ sig { params(plan: T.untyped, execute_fn: T.untyped, state: T.untyped).returns(T.untyped) }
148
+
90
149
  def executor_traverse_single(plan, execute_fn, state)
91
150
  # A fresh context per single transaction plan. Nothing is populated yet — filling
92
151
  # it in is the callback's job, which is what lets a partial context survive an error.
@@ -137,6 +196,60 @@ module Solana::Ruby::Kit
137
196
  returned
138
197
  end
139
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
+
251
+ sig { params(result: T.untyped).returns(T.untyped) }
252
+
140
253
  def executor_find_error(result)
141
254
  return result.status.error if result.kind == :single && result.status.kind == :failed
142
255
  return nil if result.kind == :single
@@ -154,5 +267,7 @@ module Solana::Ruby::Kit
154
267
  private_class_method :executor_traverse_single
155
268
  private_class_method :executor_invoke
156
269
  private_class_method :executor_find_error
270
+ private_class_method :concurrent_executor_traverse
271
+ private_class_method :concurrent_executor_execute_single
157
272
  end
158
273
  end
@@ -21,38 +21,56 @@ module Solana::Ruby::Kit
21
21
  # the result. Mirrors TypeScript's TransactionPlanResultStatus union.
22
22
 
23
23
  class SuccessfulStatus < T::Struct
24
+ extend T::Sig
25
+
24
26
  const :transaction, T.nilable(Transactions::Transaction)
25
27
  const :context, T::Hash[T.untyped, T.untyped]
28
+ sig { returns(Symbol) }
26
29
  def kind = :successful
27
30
  end
28
31
 
29
32
  class FailedStatus < T::Struct
33
+ extend T::Sig
34
+
30
35
  const :error, SolanaError
31
36
  const :context, T::Hash[T.untyped, T.untyped], default: {}.freeze
37
+ sig { returns(Symbol) }
32
38
  def kind = :failed
33
39
  end
34
40
 
35
41
  class CanceledStatus < T::Struct
42
+ extend T::Sig
43
+
36
44
  const :context, T::Hash[T.untyped, T.untyped], default: {}.freeze
45
+ sig { returns(Symbol) }
37
46
  def kind = :canceled
38
47
  end
39
48
 
40
49
  # ── Result plan types ──────────────────────────────────────────────────────
41
50
 
42
51
  class SingleTransactionPlanResult < T::Struct
52
+ extend T::Sig
53
+
43
54
  const :message, TransactionMessages::TransactionMessage
44
55
  const :status, T.untyped # SuccessfulStatus | FailedStatus | CanceledStatus
56
+ sig { returns(Symbol) }
45
57
  def kind = :single
46
58
  end
47
59
 
48
60
  class SequentialTransactionPlanResult < T::Struct
61
+ extend T::Sig
62
+
49
63
  const :plans, T::Array[T.untyped] # Array[TransactionPlanResult]
50
64
  const :divisible, T::Boolean
65
+ sig { returns(Symbol) }
51
66
  def kind = :sequential
52
67
  end
53
68
 
54
69
  class ParallelTransactionPlanResult < T::Struct
70
+ extend T::Sig
71
+
55
72
  const :plans, T::Array[T.untyped] # Array[TransactionPlanResult]
73
+ sig { returns(Symbol) }
56
74
  def kind = :parallel
57
75
  end
58
76
 
@@ -11,6 +11,8 @@ module Solana::Ruby::Kit
11
11
  module InstructionPlans
12
12
  extend T::Sig
13
13
 
14
+ module_function
15
+
14
16
  # Creates a TransactionPlanner — a callable that converts an InstructionPlan
15
17
  # into a TransactionPlan by packing instructions into transaction messages.
16
18
  #
@@ -67,12 +69,12 @@ module Solana::Ruby::Kit
67
69
  }
68
70
  end
69
71
 
70
- module_function :create_transaction_planner
71
-
72
72
  # ── Private helpers ────────────────────────────────────────────────────────
73
73
 
74
74
  module_function
75
75
 
76
+ sig { params(plan: T.untyped, ctx: T.untyped).returns(T.untyped) }
77
+
76
78
  def planner_traverse(plan, ctx)
77
79
  case plan.kind
78
80
  when :sequential then planner_traverse_sequential(plan, ctx)
@@ -87,6 +89,8 @@ module Solana::Ruby::Kit
87
89
  end
88
90
  end
89
91
 
92
+ sig { params(plan: T.untyped, ctx: T.untyped).returns(T.untyped) }
93
+
90
94
  def planner_traverse_sequential(plan, ctx)
91
95
  must_fit_in_parent = ctx[:parent] && (ctx[:parent].kind == :parallel || !plan.divisible)
92
96
 
@@ -118,6 +122,8 @@ module Solana::Ruby::Kit
118
122
  { kind: :sequential, divisible: plan.divisible, plans: tx_plans }
119
123
  end
120
124
 
125
+ sig { params(plan: T.untyped, ctx: T.untyped).returns(T.untyped) }
126
+
121
127
  def planner_traverse_parallel(plan, ctx)
122
128
  candidates = ctx[:parent_candidates].dup
123
129
  tx_plans = []
@@ -139,6 +145,8 @@ module Solana::Ruby::Kit
139
145
  { kind: :parallel, plans: tx_plans }
140
146
  end
141
147
 
148
+ sig { params(plan: T.untyped, ctx: T.untyped).returns(T.untyped) }
149
+
142
150
  def planner_traverse_single(plan, ctx)
143
151
  predicate = ->(msg) { TransactionMessages.append_instructions(msg, [plan.instruction]) }
144
152
  candidate = planner_select_and_mutate_candidate(ctx, ctx[:parent_candidates], &predicate)
@@ -148,6 +156,8 @@ module Solana::Ruby::Kit
148
156
  { kind: :single, message: msg }
149
157
  end
150
158
 
159
+ sig { params(plan: T.untyped, ctx: T.untyped).returns(T.untyped) }
160
+
151
161
  def planner_traverse_message_packer(plan, ctx)
152
162
  packer = plan.get_message_packer.call
153
163
  tx_plans = []
@@ -175,6 +185,8 @@ module Solana::Ruby::Kit
175
185
  end
176
186
  end
177
187
 
188
+ sig { params(ctx: T.untyped, candidates: T.untyped, predicate: T.proc.params(arg0: T.untyped).returns(T.untyped)).returns(T.untyped) }
189
+
178
190
  def planner_select_and_mutate_candidate(ctx, candidates, &predicate)
179
191
  candidates.each do |candidate|
180
192
  begin
@@ -198,6 +210,8 @@ module Solana::Ruby::Kit
198
210
  nil
199
211
  end
200
212
 
213
+ sig { params(ctx: T.untyped, predicate: T.proc.params(arg0: T.untyped).returns(T.untyped)).returns(T.untyped) }
214
+
201
215
  def planner_create_new_message(ctx, &predicate)
202
216
  new_msg = ctx[:create_transaction_message].call
203
217
  updated_msg = ctx[:on_transaction_message_updated].call(predicate.call(new_msg))
@@ -220,6 +234,8 @@ module Solana::Ruby::Kit
220
234
  updated_msg
221
235
  end
222
236
 
237
+ sig { params(plan: T.untyped, message: T.untyped, ctx: T.untyped).returns(T.untyped) }
238
+
223
239
  def planner_fit_entire_plan(plan, message, ctx)
224
240
  case plan.kind
225
241
  when :sequential, :parallel
@@ -259,17 +275,23 @@ module Solana::Ruby::Kit
259
275
  end
260
276
  end
261
277
 
278
+ sig { params(plan: T.untyped).returns(T.untyped) }
279
+
262
280
  def planner_get_sequential_candidate(plan)
263
281
  return plan if plan[:kind] == :single
264
282
  return nil unless plan[:kind] == :sequential && plan[:plans]&.any?
265
283
  planner_get_sequential_candidate(plan[:plans].last)
266
284
  end
267
285
 
286
+ sig { params(plan: T.untyped).returns(T.untyped) }
287
+
268
288
  def planner_get_parallel_candidates(plan)
269
289
  return [plan] if plan[:kind] == :single
270
290
  (plan[:plans] || []).flat_map { |p| planner_get_parallel_candidates(p) }
271
291
  end
272
292
 
293
+ sig { params(plan: T.untyped).returns(T.untyped) }
294
+
273
295
  def planner_freeze(plan)
274
296
  case plan[:kind]
275
297
  when :single
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative 'has_transport'
5
+
4
6
  require 'base64'
5
7
  require_relative '../../rpc_types/account_info'
6
8
 
@@ -16,6 +18,9 @@ module Solana::Ruby::Kit
16
18
  # (nil when the account does not exist)
17
19
  module GetAccountInfo
18
20
  extend T::Sig
21
+ extend T::Helpers
22
+
23
+ requires_ancestor { HasTransport }
19
24
 
20
25
  SUPPORTED_ENCODINGS = T.let(%w[base64 jsonParsed base64+zstd].freeze, T::Array[String])
21
26
 
@@ -0,0 +1,72 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'has_transport'
5
+
6
+ module Solana::Ruby::Kit
7
+ module Rpc
8
+ module Api
9
+ # The block an Alpenglow genesis certificate certifies.
10
+ AgGenesisCertBlock = T.let(
11
+ Struct.new(
12
+ :block_id, # Array<Integer> — block id of the certified block, as a 32-byte array
13
+ :slot, # Integer — slot of the certified block
14
+ keyword_init: true
15
+ ),
16
+ T.untyped
17
+ )
18
+
19
+ # The aggregate signature over an Alpenglow genesis certificate.
20
+ AgGenesisCertSignature = T.let(
21
+ Struct.new(
22
+ :bitmap, # Array<Integer> — ranks of the validators in the aggregate signature
23
+ :signature, # Array<Integer> — the 192-byte aggregate BLS signature
24
+ keyword_init: true
25
+ ),
26
+ T.untyped
27
+ )
28
+
29
+ # The Alpenglow genesis certificate, as gossiped between validators.
30
+ # Mirrors the `WireBlockCertMessage` type in the Agave validator.
31
+ AgGenesisCert = T.let(
32
+ Struct.new(
33
+ :block, # AgGenesisCertBlock
34
+ :signature, # AgGenesisCertSignature
35
+ keyword_init: true
36
+ ),
37
+ T.untyped
38
+ )
39
+
40
+ # Returns the Alpenglow genesis certificate — the certificate over the block at which the
41
+ # Alpenglow consensus protocol was activated — or nil if the node does not have one.
42
+ # Mirrors TypeScript's GetAgGenesisCertApi.getAgGenesisCert.
43
+ # @see https://solana.com/docs/rpc/http/getaggenesiscert
44
+ module GetAgGenesisCert
45
+ extend T::Sig
46
+ extend T::Helpers
47
+
48
+ requires_ancestor { HasTransport }
49
+
50
+ sig { returns(T.untyped) }
51
+ def get_ag_genesis_cert
52
+ raw = transport.request('getAgGenesisCert', [])
53
+ return nil if raw.nil?
54
+
55
+ block = raw['block'] || {}
56
+ signature = raw['signature'] || {}
57
+
58
+ AgGenesisCert.new(
59
+ block: AgGenesisCertBlock.new(
60
+ block_id: Kernel.Array(block['blockId']).map { |b| Kernel.Integer(b) },
61
+ slot: Kernel.Integer(block['slot'])
62
+ ),
63
+ signature: AgGenesisCertSignature.new(
64
+ bitmap: Kernel.Array(signature['bitmap']).map { |b| Kernel.Integer(b) },
65
+ signature: Kernel.Array(signature['signature']).map { |b| Kernel.Integer(b) }
66
+ )
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,6 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative 'has_transport'
4
5
  require_relative '../../rpc_types/account_info'
5
6
 
6
7
  module Solana::Ruby::Kit
@@ -14,6 +15,9 @@ module Solana::Ruby::Kit
14
15
  # .value — Integer (lamports)
15
16
  module GetBalance
16
17
  extend T::Sig
18
+ extend T::Helpers
19
+
20
+ requires_ancestor { HasTransport }
17
21
 
18
22
  sig do
19
23
  params(
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative 'has_transport'
5
+
4
6
  module Solana::Ruby::Kit
5
7
  module Rpc
6
8
  module Api
@@ -8,6 +10,9 @@ module Solana::Ruby::Kit
8
10
  # Mirrors TypeScript's `GetBlockHeightApi.getBlockHeight(config?)`.
9
11
  module GetBlockHeight
10
12
  extend T::Sig
13
+ extend T::Helpers
14
+
15
+ requires_ancestor { HasTransport }
11
16
 
12
17
  sig do
13
18
  params(
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative 'has_transport'
5
+
4
6
  module Solana::Ruby::Kit
5
7
  module Rpc
6
8
  module Api
@@ -8,6 +10,9 @@ module Solana::Ruby::Kit
8
10
  # Mirrors TypeScript's GetBlockTimeApi.getBlockTime.
9
11
  module GetBlockTime
10
12
  extend T::Sig
13
+ extend T::Helpers
14
+
15
+ requires_ancestor { HasTransport }
11
16
 
12
17
  sig { params(slot: Integer).returns(T.nilable(Integer)) }
13
18
  def get_block_time(slot)