solana-ruby-kit 0.1.8 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f96f7c22b28fbf9bec4186e3bf92ec52483d532b7e398da89bf143473b267878
4
- data.tar.gz: 4a2e915aba2620726be4b5c89880c49fd7d54acf950618d4002ce19b54ac7884
3
+ metadata.gz: 7b2766067cfcd434e5c657e1fde1dab4b44b23880e0f514f924d28a94ea0ca4a
4
+ data.tar.gz: 32814612b342df38df079aaba08b8ea17c9128ac9f54279ab3abad622ba02492
5
5
  SHA512:
6
- metadata.gz: 8a7fef4cd3632770e3e7d67f5b4722585f66f2ad45c601687d6664fc38433a85d39de7f4d582062e6396be7df29ea3cf089ca77184a7d8c28c3cf2b3d37015ae
7
- data.tar.gz: '0967fe5ea552384a656b22f5f03d08674e4066c373e0ee83cc31bd45598573aacade37f4295a0c24341eb0e31b7d543720be99cf347aba0ab626429f5c629085'
6
+ metadata.gz: 3bcfbd20e6f610a47053cb385511931004590646ff04e0edf4587e5699df3db8bb148af2b706cf025e8399f26e4736ae9db35ebfd04d68be287df8302bc9f4de
7
+ data.tar.gz: d40b332b7cc427730b17b01a091c53815c596c0ac114bca93f52bed4d35775cd2b57ca5146893e2c94d46cebcbe0f51ccf98d2c6e2cad308cd51c2ff7b022d62
@@ -46,12 +46,20 @@ module Solana::Ruby::Kit
46
46
 
47
47
  # ── Transactions ──────────────────────────────────────────────────────────
48
48
  TRANSACTIONS__TRANSACTION_NOT_SIGNABLE = :SOLANA_ERROR__TRANSACTIONS__TRANSACTION_NOT_SIGNABLE
49
+ TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT = :SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT
50
+ TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT = :SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT
51
+ TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT = :SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT
52
+ TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS = :SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS
49
53
  TRANSACTIONS__EXCEEDS_SIZE_LIMIT = :SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT
50
54
  TRANSACTIONS__MISSING_SIGNER = :SOLANA_ERROR__TRANSACTIONS__MISSING_SIGNER
51
55
  TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE
52
56
  TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS
53
57
  TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING
54
58
  TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND
59
+ # context: { index: }
60
+ TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE
61
+ # context: { unsupported_version: }
62
+ TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED = :SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED
55
63
  TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE = :SOLANA_ERROR__TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE
56
64
  TRANSACTIONS__BLOCKHASH_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__BLOCKHASH_NOT_FOUND
57
65
  TRANSACTIONS__FAILED_TRANSACTION_PLAN = :SOLANA_ERROR__TRANSACTIONS__FAILED_TRANSACTION_PLAN
@@ -88,13 +96,12 @@ module Solana::Ruby::Kit
88
96
  RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
89
97
  RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER
90
98
  RPC__TRANSPORT_HTTP_ERROR = :SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR
91
- # JSON-RPC server errors (-32017..-32019); context keys mirror TypeScript SolanaErrorContext:
92
- # EPOCH_REWARDS_PERIOD_ACTIVE: { slot:, current_block_height:, rewards_complete_block_height: }
93
- # SLOT_NOT_EPOCH_BOUNDARY: { slot: }
94
- # LONG_TERM_STORAGE_UNREACHABLE: (no context)
99
+ # JSON-RPC server errors (see https://github.com/anza-xyz/kit/blob/main/packages/errors/src/codes.ts)
95
100
  JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE
96
101
  JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY
97
102
  JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE
103
+ JSON_RPC__SERVER_ERROR_NO_SLOT_HISTORY = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SLOT_HISTORY
104
+ JSON_RPC__SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND
98
105
  RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST
99
106
  RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID
100
107
  RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED
@@ -115,6 +122,16 @@ module Solana::Ruby::Kit
115
122
  INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE = :SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE
116
123
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN
117
124
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN
125
+ # context: { max_instructions:, transaction_instruction_limit: }
126
+ INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION = :SOLANA_ERROR__INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION
127
+ # context: { max_instructions:, num_instructions: }
128
+ INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED = :SOLANA_ERROR__INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED
129
+
130
+ # ── Transaction introspection ────────────────────────────────────────────
131
+ TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION = :SOLANA_ERROR__TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION
132
+ TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE = :SOLANA_ERROR__TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE
133
+ # context: { needed_bytes: }
134
+ TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE = :SOLANA_ERROR__TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE
118
135
 
119
136
  # ── Wallet Standard ───────────────────────────────────────────────────────
120
137
  WALLET_STANDARD__INVALID_WIRE_FORMAT = :SOLANA_ERROR__WALLET_STANDARD__INVALID_WIRE_FORMAT
@@ -172,12 +189,18 @@ module Solana::Ruby::Kit
172
189
 
173
190
  # Transactions
174
191
  TRANSACTIONS__TRANSACTION_NOT_SIGNABLE => 'Transaction is not signable (missing fee payer or lifetime constraint)',
192
+ TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT => 'Failed to estimate the compute unit consumption for this transaction message. This is likely because simulating the transaction failed.',
193
+ TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT => 'Transaction failed when it was simulated in order to estimate the compute unit consumption.',
194
+ TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT => 'Failed to estimate the loaded accounts data size for this transaction message. The RPC did not return a loadedAccountsDataSize value from simulation. This value is required for version 1 transactions.',
195
+ TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS => 'Transaction failed when it was simulated in order to estimate its resource limits.',
175
196
  TRANSACTIONS__EXCEEDS_SIZE_LIMIT => 'Transaction wire size (%{actual_size} bytes) exceeds the limit of %{limit} bytes',
176
197
  TRANSACTIONS__MISSING_SIGNER => 'Transaction is missing a required signer: %{address}',
177
198
  TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE => 'Transaction version %{version} is out of range',
178
199
  TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS => 'Failed to decompile address lookup table contents',
179
200
  TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING => 'Failed to decompile transaction: fee payer is missing',
180
- TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found',
201
+ TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found at index %{index}',
202
+ TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE => 'Failed to decompile instruction: account index %{index} is out of range',
203
+ TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED => 'Transaction version %{unsupported_version} is not supported',
181
204
  TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE => 'Transaction simulation failed: %{message}',
182
205
  TRANSACTIONS__BLOCKHASH_NOT_FOUND => 'Blockhash not found',
183
206
  TRANSACTIONS__FAILED_TRANSACTION_PLAN => 'Failed to execute transaction plan',
@@ -217,6 +240,8 @@ module Solana::Ruby::Kit
217
240
  JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE => 'Epoch rewards period still active at slot %{slot}',
218
241
  JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY => "Rewards cannot be found because slot %{slot} is not the epoch boundary. This may be due to gap in the queried node's local ledger or long-term storage",
219
242
  JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE => 'Failed to query long-term storage; please try again',
243
+ JSON_RPC__SERVER_ERROR_NO_SLOT_HISTORY => 'No slot history',
244
+ JSON_RPC__SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND => 'Filter transaction not found',
220
245
  RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST => 'Cannot create subscription request',
221
246
  RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID => 'Expected server to return a subscription ID',
222
247
  RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED => 'WebSocket channel closed before message could be buffered',
@@ -236,6 +261,13 @@ module Solana::Ruby::Kit
236
261
  INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE => 'Message packer is already complete; no more instructions to pack',
237
262
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN => 'Instruction plan is empty and produced no transaction messages',
238
263
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
264
+ INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION => 'The configured maximum of %{max_instructions} instructions per transaction is invalid. It must be a positive integer no greater than the transaction format limit of %{transaction_instruction_limit} instructions per transaction.',
265
+ INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED => 'Planning this transaction message would require %{num_instructions} instructions, which exceeds the configured maximum of %{max_instructions} instructions per transaction.',
266
+
267
+ # Transaction introspection
268
+ TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION => "Cannot decode a 'jsonParsed'-encoded getTransaction response; its instructions are pre-parsed and lack raw bytes. Fetch with encoding: 'json' or 'base64' instead.",
269
+ TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE => 'Unrecognized getTransaction response shape; expected a base64, base58, or json-encoded transaction.',
270
+ TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE => 'Malformed compiled transaction message: expected at least %{needed_bytes} byte(s)',
239
271
 
240
272
  # Subscribable
241
273
  SUBSCRIBABLE__RETRY_NOT_SUPPORTED => 'This reactive store does not support retry(); use create_reactive_store_from_data_publisher_factory for a retryable store',
@@ -5,6 +5,7 @@ require_relative '../errors'
5
5
  require_relative '../instructions/instruction'
6
6
  require_relative '../transaction_messages/transaction_message'
7
7
  require_relative '../transactions/compiler'
8
+ require_relative 'max_instructions'
8
9
 
9
10
  module Solana::Ruby::Kit
10
11
  module InstructionPlans
@@ -68,9 +69,17 @@ module Solana::Ruby::Kit
68
69
  # Packs as many instructions as possible into +message+ and returns the
69
70
  # updated message. Raises SolanaError if the message is too small or the
70
71
  # packer is already done.
71
- sig { params(message: TransactionMessages::TransactionMessage).returns(TransactionMessages::TransactionMessage) }
72
- def pack_message_to_capacity(message)
73
- @pack_proc.call(message)
72
+ #
73
+ # +max_instructions+ caps the number of top-level instructions allowed in the
74
+ # returned message (defaults to 16; must be a positive integer no greater than 64).
75
+ sig do
76
+ params(
77
+ message: TransactionMessages::TransactionMessage,
78
+ max_instructions: T.nilable(Integer)
79
+ ).returns(TransactionMessages::TransactionMessage)
80
+ end
81
+ def pack_message_to_capacity(message, max_instructions: nil)
82
+ @pack_proc.call(message, max_instructions)
74
83
  end
75
84
  end
76
85
 
@@ -122,11 +131,15 @@ module Solana::Ruby::Kit
122
131
  offset = T.let(0, Integer)
123
132
  MessagePacker.new(
124
133
  done_proc: -> { offset >= total_length },
125
- pack_proc: ->(message) {
134
+ pack_proc: ->(message, max_instructions) {
126
135
  if offset >= total_length
127
136
  Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
128
137
  end
129
138
 
139
+ InstructionPlans.assert_valid_max_instructions_per_transaction(max_instructions)
140
+ resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
141
+ InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)
142
+
130
143
  base_ix = get_instruction.call(offset, 0)
131
144
  with_base = TransactionMessages.append_instructions(message, [base_ix])
132
145
  base_size = Transactions.get_transaction_message_size(with_base)
@@ -164,18 +177,30 @@ module Solana::Ruby::Kit
164
177
  idx = T.let(0, Integer)
165
178
  MessagePacker.new(
166
179
  done_proc: -> { idx >= instructions.length },
167
- pack_proc: ->(message) {
180
+ pack_proc: ->(message, max_instructions) {
168
181
  if idx >= instructions.length
169
182
  Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
170
183
  end
171
184
 
185
+ InstructionPlans.assert_valid_max_instructions_per_transaction(max_instructions)
186
+ resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
187
+ InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)
188
+
172
189
  original_size = Transactions.get_transaction_message_size(message)
173
190
  packed = T.let(message, TransactionMessages::TransactionMessage)
174
191
  start_idx = idx
175
192
 
176
193
  (idx...instructions.length).each do |i|
177
- packed = TransactionMessages.append_instructions(packed, [instructions[i]])
178
- size = Transactions.get_transaction_message_size(packed)
194
+ # Stop once the message is full, before compiling a message that would exceed
195
+ # the instruction limit (which would raise). The assertion above guarantees at
196
+ # least the first instruction fits, so reaching the limit here is a graceful stop.
197
+ if packed.instructions.length >= resolved_max
198
+ idx = i
199
+ return packed
200
+ end
201
+
202
+ next_packed = TransactionMessages.append_instructions(packed, [instructions[i]])
203
+ size = Transactions.get_transaction_message_size(next_packed)
179
204
 
180
205
  if size > Transactions::TRANSACTION_SIZE_LIMIT
181
206
  if i == start_idx
@@ -190,6 +215,8 @@ module Solana::Ruby::Kit
190
215
  idx = i
191
216
  return packed
192
217
  end
218
+
219
+ packed = next_packed
193
220
  end
194
221
 
195
222
  idx = instructions.length
@@ -0,0 +1,59 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../errors'
5
+
6
+ module Solana::Ruby::Kit
7
+ module InstructionPlans
8
+ extend T::Sig
9
+
10
+ # The default maximum number of top-level instructions per planned transaction message.
11
+ #
12
+ # Intentionally lower than the transaction format's instruction limit to leave headroom
13
+ # for inner instructions (CPIs), which are not visible at planning time.
14
+ DEFAULT_MAX_INSTRUCTIONS_PER_TRANSACTION = T.let(16, Integer)
15
+
16
+ # The hard maximum number of top-level instructions the transaction format can encode.
17
+ TRANSACTION_INSTRUCTION_LIMIT = T.let(64, Integer)
18
+
19
+ module_function
20
+
21
+ # Resolves the effective maximum number of instructions allowed in a transaction message,
22
+ # falling back to the default when no value is provided.
23
+ # Mirrors `resolveMaxInstructions(maxInstructions)`.
24
+ sig { params(max_instructions: T.nilable(Integer)).returns(Integer) }
25
+ def resolve_max_instructions(max_instructions)
26
+ max_instructions || DEFAULT_MAX_INSTRUCTIONS_PER_TRANSACTION
27
+ end
28
+
29
+ # Asserts that a configured maximum number of instructions per transaction is valid.
30
+ # +nil+ is allowed and falls back to the default.
31
+ # Mirrors `assertValidMaxInstructionsPerTransaction(maxInstructions)`.
32
+ sig { params(max_instructions: T.nilable(Integer)).void }
33
+ def assert_valid_max_instructions_per_transaction(max_instructions)
34
+ return if max_instructions.nil?
35
+
36
+ if max_instructions <= 0 || max_instructions > TRANSACTION_INSTRUCTION_LIMIT
37
+ Kernel.raise SolanaError.new(
38
+ SolanaError::INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION,
39
+ {
40
+ max_instructions: max_instructions,
41
+ transaction_instruction_limit: TRANSACTION_INSTRUCTION_LIMIT
42
+ }
43
+ )
44
+ end
45
+ end
46
+
47
+ # Raises if +num_instructions+ exceeds +max_instructions+.
48
+ # Mirrors `assertMaxInstructionsPerTransaction(numInstructions, maxInstructions)`.
49
+ sig { params(num_instructions: Integer, max_instructions: Integer).void }
50
+ def assert_max_instructions_per_transaction(num_instructions, max_instructions)
51
+ return if num_instructions <= max_instructions
52
+
53
+ Kernel.raise SolanaError.new(
54
+ SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED,
55
+ { max_instructions: max_instructions, num_instructions: num_instructions }
56
+ )
57
+ end
58
+ end
59
+ end
@@ -23,27 +23,45 @@ module Solana::Ruby::Kit
23
23
  # Called after instructions are appended to a message. Must return the
24
24
  # (possibly further updated) message. Defaults to identity.
25
25
  #
26
- # The returned planner is a lambda: planner.call(instruction_plan) -> TransactionPlan
26
+ # max_instructions_per_transaction: Integer (optional)
27
+ # Maximum number of top-level instructions allowed in each planned transaction.
28
+ # Must be a positive integer no greater than 64. Defaults to 16. Can be overridden
29
+ # per-call via the returned planner's `max_instructions_per_transaction:` kwarg.
30
+ #
31
+ # The returned planner is a lambda:
32
+ # planner.call(instruction_plan, max_instructions_per_transaction: nil) -> TransactionPlan
27
33
  #
28
34
  # Mirrors `createTransactionPlanner(config)` from @solana/instruction-plans.
29
35
  # NOTE: Ruby translation is synchronous; abort-signal support is omitted.
30
36
  sig do
31
37
  params(
32
- create_transaction_message: T.untyped,
33
- on_transaction_message_updated: T.untyped
38
+ create_transaction_message: T.untyped,
39
+ on_transaction_message_updated: T.untyped,
40
+ max_instructions_per_transaction: T.nilable(Integer)
34
41
  ).returns(T.untyped)
35
42
  end
36
43
  def create_transaction_planner(
37
44
  create_transaction_message:,
38
- on_transaction_message_updated: ->(msg) { msg }
45
+ on_transaction_message_updated: ->(msg) { msg },
46
+ max_instructions_per_transaction: nil
39
47
  )
40
48
  ctx = {
41
49
  create_transaction_message: create_transaction_message,
42
50
  on_transaction_message_updated: on_transaction_message_updated
43
51
  }
52
+ config_max_instructions_per_transaction = max_instructions_per_transaction
53
+
54
+ ->(instruction_plan, max_instructions_per_transaction: nil) {
55
+ resolved_max = max_instructions_per_transaction || config_max_instructions_per_transaction
56
+
57
+ # Reject up front any configured maximum the transaction format could never satisfy,
58
+ # rather than discovering it mid-plan when a message fails to compile.
59
+ InstructionPlans.assert_valid_max_instructions_per_transaction(resolved_max)
44
60
 
45
- ->(instruction_plan) {
46
- mutable = planner_traverse(instruction_plan, ctx.merge(parent: nil, parent_candidates: []))
61
+ mutable = planner_traverse(
62
+ instruction_plan,
63
+ ctx.merge(parent: nil, parent_candidates: [], max_instructions_per_transaction: resolved_max)
64
+ )
47
65
  Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN) unless mutable
48
66
  planner_freeze(mutable)
49
67
  }
@@ -136,7 +154,7 @@ module Solana::Ruby::Kit
136
154
  candidates = ctx[:parent_candidates].dup
137
155
 
138
156
  until packer.done?
139
- predicate = ->(msg) { packer.pack_message_to_capacity(msg) }
157
+ predicate = ->(msg) { packer.pack_message_to_capacity(msg, max_instructions: ctx[:max_instructions_per_transaction]) }
140
158
  candidate = planner_select_and_mutate_candidate(ctx, candidates, &predicate)
141
159
  unless candidate
142
160
  msg = planner_create_new_message(ctx, &predicate)
@@ -162,11 +180,18 @@ module Solana::Ruby::Kit
162
180
  begin
163
181
  updated = ctx[:on_transaction_message_updated].call(predicate.call(candidate[:message]))
164
182
  if Transactions.get_transaction_message_size(updated) <= Transactions::TRANSACTION_SIZE_LIMIT
183
+ InstructionPlans.assert_max_instructions_per_transaction(
184
+ updated.instructions.length,
185
+ InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
186
+ )
165
187
  candidate[:message] = updated
166
188
  return candidate
167
189
  end
168
190
  rescue SolanaError => e
169
- next if e.code == SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN
191
+ next if [
192
+ SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
193
+ SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED
194
+ ].include?(e.code)
170
195
  Kernel.raise
171
196
  end
172
197
  end
@@ -187,6 +212,11 @@ module Solana::Ruby::Kit
187
212
  )
188
213
  end
189
214
 
215
+ InstructionPlans.assert_max_instructions_per_transaction(
216
+ updated_msg.instructions.length,
217
+ InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
218
+ )
219
+
190
220
  updated_msg
191
221
  end
192
222
 
@@ -205,11 +235,21 @@ module Solana::Ruby::Kit
205
235
  num_free_bytes: Transactions::TRANSACTION_SIZE_LIMIT - base_size }
206
236
  )
207
237
  end
238
+ InstructionPlans.assert_max_instructions_per_transaction(
239
+ updated.instructions.length,
240
+ InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
241
+ )
208
242
  updated
209
243
  when :message_packer
210
244
  packer = plan.get_message_packer.call
211
245
  msg = message
212
- msg = packer.pack_message_to_capacity(msg) until packer.done?
246
+ until packer.done?
247
+ msg = packer.pack_message_to_capacity(msg, max_instructions: ctx[:max_instructions_per_transaction])
248
+ InstructionPlans.assert_max_instructions_per_transaction(
249
+ msg.instructions.length,
250
+ InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
251
+ )
252
+ end
213
253
  msg
214
254
  else
215
255
  Kernel.raise SolanaError.new(
@@ -3,6 +3,7 @@
3
3
 
4
4
  # Mirrors @solana/instruction-plans.
5
5
  # An InstructionPlan describes operations that may span multiple transactions.
6
+ require_relative 'instruction_plans/max_instructions'
6
7
  require_relative 'instruction_plans/instruction_plan'
7
8
  require_relative 'instruction_plans/transaction_plan'
8
9
  require_relative 'instruction_plans/transaction_plan_result'
@@ -0,0 +1,156 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'base64'
5
+ require_relative 'errors'
6
+ require_relative 'functional'
7
+ require_relative 'transaction_messages'
8
+ require_relative 'transaction_messages/compute_budget'
9
+ require_relative 'transactions'
10
+
11
+ module Solana::Ruby::Kit
12
+ # Utilities for estimating and setting transaction resource limits (compute units and
13
+ # loaded accounts data size) via simulation.
14
+ #
15
+ # Mirrors `estimateResourceLimitsFactory`, `estimateAndSetResourceLimitsFactory`,
16
+ # and `fillTransactionMessageProvisoryResourceLimits` from @solana/kit.
17
+ module ResourceLimitEstimation
18
+ extend T::Sig
19
+
20
+ PROVISORY_LIMIT = T.let(0, Integer)
21
+
22
+ module_function
23
+
24
+ # Returns a callable that estimates the resource limits required by a transaction message
25
+ # by simulating it with maximum limits set, then reading back the actual consumption.
26
+ #
27
+ # The returned callable accepts a TransactionMessage and optional keyword args:
28
+ # commitment: Symbol (nil uses RPC default)
29
+ # min_context_slot: Integer
30
+ #
31
+ # Returns a Hash with:
32
+ # :compute_unit_limit Integer (always present)
33
+ # :loaded_accounts_data_size_limit Integer (present when the RPC returns it)
34
+ #
35
+ # Raises SolanaError with TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT
36
+ # if the message is version 1 but the RPC does not return loadedAccountsDataSize.
37
+ # Raises SolanaError with TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS
38
+ # if the simulated transaction itself fails.
39
+ # Raises SolanaError with TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT for any other error.
40
+ #
41
+ # Mirrors `estimateResourceLimitsFactory({ rpc })` from @solana/kit.
42
+ sig { params(rpc: T.untyped).returns(T.untyped) }
43
+ def estimate_resource_limits_factory(rpc:)
44
+ ->(transaction_message, commitment: nil, min_context_slot: nil) do
45
+ replace_recent_blockhash = !TransactionMessages.durable_nonce_lifetime?(transaction_message)
46
+ is_v1 = transaction_message.version == 1
47
+
48
+ tx_for_sim = Functional.pipe(
49
+ transaction_message,
50
+ ->(m) { TransactionMessages.set_transaction_message_compute_unit_limit(TransactionMessages::MAX_COMPUTE_UNIT_LIMIT, m) },
51
+ ->(m) { is_v1 ? TransactionMessages.set_transaction_message_loaded_accounts_data_size_limit(TransactionMessages::MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT, m) : m }
52
+ )
53
+
54
+ transaction = Transactions.compile_transaction_message(tx_for_sim)
55
+ wire_bytes = Transactions.wire_encode_transaction(transaction)
56
+ encoded = Base64.strict_encode64(wire_bytes)
57
+
58
+ sim_opts = { replace_recent_blockhash: replace_recent_blockhash }
59
+ sim_opts[:commitment] = commitment if commitment
60
+ sim_opts[:min_context_slot] = min_context_slot if min_context_slot
61
+
62
+ begin
63
+ sim_value = rpc.simulate_transaction(encoded, **sim_opts).value
64
+
65
+ transaction_error = sim_value['err']
66
+ units_consumed = sim_value['unitsConsumed']
67
+ loaded_accounts_data_size = sim_value['loadedAccountsDataSize']
68
+
69
+ if is_v1 && loaded_accounts_data_size.nil?
70
+ Kernel.raise SolanaError.new(SolanaError::TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT)
71
+ end
72
+
73
+ if transaction_error
74
+ Kernel.raise SolanaError.new(
75
+ SolanaError::TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS
76
+ )
77
+ end
78
+
79
+ compute_unit_limit = [units_consumed.to_i, 4_294_967_295].min
80
+
81
+ estimate = { compute_unit_limit: compute_unit_limit }
82
+ estimate[:loaded_accounts_data_size_limit] = loaded_accounts_data_size.to_i unless loaded_accounts_data_size.nil?
83
+ estimate
84
+ rescue SolanaError => e
85
+ known = [
86
+ SolanaError::TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS,
87
+ SolanaError::TRANSACTION__FAILED_TO_ESTIMATE_LOADED_ACCOUNTS_DATA_SIZE_LIMIT
88
+ ]
89
+ Kernel.raise if known.include?(e.code)
90
+ Kernel.raise SolanaError.new(SolanaError::TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT)
91
+ rescue StandardError
92
+ Kernel.raise SolanaError.new(SolanaError::TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT)
93
+ end
94
+ end
95
+ end
96
+
97
+ # Returns a callable that estimates resource limits for a transaction message and
98
+ # sets them on the message, unless they are already explicitly configured.
99
+ #
100
+ # A limit of 0 (the provisory value) or the maximum (1,400,000 for CU) is treated
101
+ # as non-explicit and will be replaced with the estimate.
102
+ #
103
+ # Mirrors `estimateAndSetResourceLimitsFactory(estimator)` from @solana/kit.
104
+ sig { params(estimate_resource_limits: T.untyped).returns(T.untyped) }
105
+ def estimate_and_set_resource_limits_factory(estimate_resource_limits)
106
+ ->(transaction_message, **opts) do
107
+ existing_cu = TransactionMessages.get_transaction_message_compute_unit_limit(transaction_message)
108
+ cu_explicit = !existing_cu.nil? &&
109
+ existing_cu != PROVISORY_LIMIT &&
110
+ existing_cu != TransactionMessages::MAX_COMPUTE_UNIT_LIMIT
111
+
112
+ is_v1 = transaction_message.version == 1
113
+ loaded_explicit = true
114
+ if is_v1
115
+ existing_loaded = TransactionMessages.get_transaction_message_loaded_accounts_data_size_limit(transaction_message)
116
+ loaded_explicit = !existing_loaded.nil? && existing_loaded != PROVISORY_LIMIT
117
+ end
118
+
119
+ return transaction_message if cu_explicit && loaded_explicit
120
+
121
+ estimate = estimate_resource_limits.call(transaction_message, **opts)
122
+
123
+ result = transaction_message
124
+ unless cu_explicit
125
+ result = TransactionMessages.set_transaction_message_compute_unit_limit(estimate[:compute_unit_limit], result)
126
+ end
127
+ if is_v1 && !loaded_explicit && estimate.key?(:loaded_accounts_data_size_limit)
128
+ result = TransactionMessages.set_transaction_message_loaded_accounts_data_size_limit(estimate[:loaded_accounts_data_size_limit], result)
129
+ end
130
+ result
131
+ end
132
+ end
133
+
134
+ # Sets compute unit limit (and, for V1, loaded accounts data size limit) to the
135
+ # provisory value of 0 on the message, unless a limit is already present.
136
+ #
137
+ # Use this during message construction to reserve space for limits that will later
138
+ # be replaced with actual estimates via `estimate_and_set_resource_limits_factory`.
139
+ #
140
+ # Mirrors `fillTransactionMessageProvisoryResourceLimits` from @solana/kit.
141
+ sig do
142
+ params(message: TransactionMessages::TransactionMessage)
143
+ .returns(TransactionMessages::TransactionMessage)
144
+ end
145
+ def fill_transaction_message_provisory_resource_limits(message)
146
+ result = message
147
+ if TransactionMessages.get_transaction_message_compute_unit_limit(result).nil?
148
+ result = TransactionMessages.set_transaction_message_compute_unit_limit(PROVISORY_LIMIT, result)
149
+ end
150
+ if result.version == 1 && TransactionMessages.get_transaction_message_loaded_accounts_data_size_limit(result).nil?
151
+ result = TransactionMessages.set_transaction_message_loaded_accounts_data_size_limit(PROVISORY_LIMIT, result)
152
+ end
153
+ result
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,72 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Rpc
6
+ module Api
7
+ # Struct representing one transaction signature entry returned by getSignaturesForAddress.
8
+ # Mirrors TypeScript's GetSignaturesForAddressTransaction.
9
+ SignatureInfo = T.let(
10
+ Struct.new(
11
+ :block_time, # Integer | nil — Unix timestamp of block production, nil if unavailable
12
+ :confirmation_status, # Symbol | nil — :processed, :confirmed, or :finalized
13
+ :err, # Hash | nil — transaction error, nil if succeeded
14
+ :memo, # String | nil — memo associated with the transaction
15
+ :signature, # String — base-58 encoded transaction signature
16
+ :slot, # Integer — slot containing the transaction
17
+ :transaction_index, # Integer | nil — 0-based index within block (Agave 4.0+)
18
+ keyword_init: true
19
+ ),
20
+ T.untyped
21
+ )
22
+
23
+ # Returns confirmed transaction signatures that reference the given address.
24
+ # Mirrors TypeScript's GetSignaturesForAddressApi.getSignaturesForAddress.
25
+ # See https://solana.com/docs/rpc/http/getsignaturesforaddress
26
+ module GetSignaturesForAddress
27
+ extend T::Sig
28
+
29
+ sig do
30
+ params(
31
+ address: String,
32
+ before: T.nilable(String),
33
+ until_sig: T.nilable(String),
34
+ limit: T.nilable(Integer),
35
+ commitment: T.nilable(Symbol),
36
+ min_context_slot: T.nilable(Integer)
37
+ ).returns(T::Array[T.untyped])
38
+ end
39
+ def get_signatures_for_address(
40
+ address,
41
+ before: nil,
42
+ until_sig: nil,
43
+ limit: nil,
44
+ commitment: nil,
45
+ min_context_slot: nil
46
+ )
47
+ config = {}
48
+ config['before'] = before if before
49
+ config['until'] = until_sig if until_sig
50
+ config['limit'] = limit if limit
51
+ config['commitment'] = commitment.to_s if commitment
52
+ config['minContextSlot'] = min_context_slot if min_context_slot
53
+
54
+ params = config.empty? ? [address] : [address, config]
55
+ result = transport.request('getSignaturesForAddress', params)
56
+
57
+ result.map do |tx|
58
+ SignatureInfo.new(
59
+ block_time: tx['blockTime'],
60
+ confirmation_status: tx['confirmationStatus']&.to_sym,
61
+ err: tx['err'],
62
+ memo: tx['memo'],
63
+ signature: tx['signature'],
64
+ slot: Kernel.Integer(tx['slot']),
65
+ transaction_index: tx['transactionIndex']
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -12,6 +12,7 @@ module Solana::Ruby::Kit
12
12
  :activated_stake,
13
13
  :epoch_vote_account,
14
14
  :commission,
15
+ :inflation_rewards_commission_bps,
15
16
  :last_vote,
16
17
  :epoch_credits,
17
18
  :root_slot,
@@ -45,6 +46,7 @@ module Solana::Ruby::Kit
45
46
  activated_stake: Kernel.Integer(v['activatedStake']),
46
47
  epoch_vote_account: v['epochVoteAccount'],
47
48
  commission: Kernel.Integer(v['commission']),
49
+ inflation_rewards_commission_bps: v['inflationRewardsCommissionBps'] ? Kernel.Integer(v['inflationRewardsCommissionBps']) : nil,
48
50
  last_vote: Kernel.Integer(v['lastVote']),
49
51
  epoch_credits: v['epochCredits'],
50
52
  root_slot: v['rootSlot'] ? Kernel.Integer(v['rootSlot']) : nil
@@ -23,6 +23,7 @@ require_relative 'api/simulate_transaction'
23
23
  require_relative 'api/get_block_time'
24
24
  require_relative 'api/get_epoch_schedule'
25
25
  require_relative 'api/get_inflation_reward'
26
+ require_relative 'api/get_signatures_for_address'
26
27
 
27
28
  module Solana::Ruby::Kit
28
29
  module Rpc
@@ -62,6 +63,7 @@ module Solana::Ruby::Kit
62
63
  include Api::GetBlockTime
63
64
  include Api::GetEpochSchedule
64
65
  include Api::GetInflationReward
66
+ include Api::GetSignaturesForAddress
65
67
 
66
68
  sig { returns(Transport) }
67
69
  attr_reader :transport