solana-ruby-kit 0.1.9 → 7.0.0.1

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: 1d26df33c1471059cde38ac46ec2604e0c26ffc8d64c580de4171396516530fd
4
- data.tar.gz: dba1b05e818b10e8fa7692f6ae4af89c3946bcd19707be52cfdfd1c503e81b63
3
+ metadata.gz: 950ad695ca0d9971314172207d974e747cf35c5dfec1210fb32882f0517306f8
4
+ data.tar.gz: ce0c4c171c78ca202a35bbacdd74630bb24afd3a438c7ebe7669b5cb56ee9a71
5
5
  SHA512:
6
- metadata.gz: 56674f680f3059cd3247591fe2a14f303853b8806e251f4c147de90fb4ff7ebcea498394a2650aab40089d3cb5e52b10686cd3c5797e171c230a21ef05602eb3
7
- data.tar.gz: b57dd91be86b034b1a207c809a04c38ca045700f608e4481ac125536d546f21f68e796f01930259a5054ea5ecf72f020f9260d4b63f6661dd177525c32ab924d
6
+ metadata.gz: 7d356ad614cfe6990a0df08121f6db68dcdf4e72b19979bd01a56233a1fccc699a16ee716c2e12d33b4de5c34792cc627fd0e273bfefce73a7144f70fff37ddf
7
+ data.tar.gz: b866ff957a09ba7dc4ab26708ea1311b2c8a283e992f972351c173187a47d905b6ba62483beefd0f5b2c9d46f9ecdd8574bba1f4491e96771abdc9c75417f7ee
@@ -56,6 +56,10 @@ module Solana::Ruby::Kit
56
56
  TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS
57
57
  TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING
58
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
59
63
  TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE = :SOLANA_ERROR__TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE
60
64
  TRANSACTIONS__BLOCKHASH_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__BLOCKHASH_NOT_FOUND
61
65
  TRANSACTIONS__FAILED_TRANSACTION_PLAN = :SOLANA_ERROR__TRANSACTIONS__FAILED_TRANSACTION_PLAN
@@ -118,6 +122,16 @@ module Solana::Ruby::Kit
118
122
  INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE = :SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE
119
123
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN
120
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
121
135
 
122
136
  # ── Wallet Standard ───────────────────────────────────────────────────────
123
137
  WALLET_STANDARD__INVALID_WIRE_FORMAT = :SOLANA_ERROR__WALLET_STANDARD__INVALID_WIRE_FORMAT
@@ -125,6 +139,7 @@ module Solana::Ruby::Kit
125
139
 
126
140
  # ── Subscribable ──────────────────────────────────────────────────────────
127
141
  SUBSCRIBABLE__RETRY_NOT_SUPPORTED = :SOLANA_ERROR__SUBSCRIBABLE__RETRY_NOT_SUPPORTED
142
+ SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR = :SOLANA_ERROR__SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR
128
143
 
129
144
  # ── Fixed-points ──────────────────────────────────────────────────────────
130
145
  FIXED_POINTS__STRICT_MODE_PRECISION_LOSS = :SOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS
@@ -184,7 +199,9 @@ module Solana::Ruby::Kit
184
199
  TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE => 'Transaction version %{version} is out of range',
185
200
  TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS => 'Failed to decompile address lookup table contents',
186
201
  TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING => 'Failed to decompile transaction: fee payer is missing',
187
- TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found',
202
+ TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found at index %{index}',
203
+ TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE => 'Failed to decompile instruction: account index %{index} is out of range',
204
+ TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED => 'Transaction version %{unsupported_version} is not supported',
188
205
  TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE => 'Transaction simulation failed: %{message}',
189
206
  TRANSACTIONS__BLOCKHASH_NOT_FOUND => 'Blockhash not found',
190
207
  TRANSACTIONS__FAILED_TRANSACTION_PLAN => 'Failed to execute transaction plan',
@@ -245,9 +262,17 @@ module Solana::Ruby::Kit
245
262
  INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE => 'Message packer is already complete; no more instructions to pack',
246
263
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN => 'Instruction plan is empty and produced no transaction messages',
247
264
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
265
+ 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.',
266
+ 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.',
267
+
268
+ # Transaction introspection
269
+ 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.",
270
+ TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE => 'Unrecognized getTransaction response shape; expected a base64, base58, or json-encoded transaction.',
271
+ TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE => 'Malformed compiled transaction message: expected at least %{needed_bytes} byte(s)',
248
272
 
249
273
  # Subscribable
250
274
  SUBSCRIBABLE__RETRY_NOT_SUPPORTED => 'This reactive store does not support retry(); use create_reactive_store_from_data_publisher_factory for a retryable store',
275
+ SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR => 'The stream store closed in an error state but did not report an error.',
251
276
 
252
277
  # Fixed-points
253
278
  FIXED_POINTS__STRICT_MODE_PRECISION_LOSS => 'Value has more than 9 fractional digits and cannot be represented exactly as a Sol amount',
@@ -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'
@@ -9,7 +9,9 @@ module Solana::Ruby::Kit
9
9
  InflationReward = T.let(
10
10
  Struct.new(
11
11
  :amount, # Integer (Lamports) — reward credited
12
- :commission, # Integer — vote account commission at reward time
12
+ :commission, # Integer, nilable — vote account commission at reward time;
13
+ # null once the vote account reports commission through
14
+ # getVoteAccounts' inflationRewardsCommissionBps instead
13
15
  :effective_slot, # Integer — slot in which rewards were delivered
14
16
  :epoch, # Integer — epoch for which reward occurred
15
17
  :post_balance, # Integer (Lamports) — post-reward account balance
@@ -46,7 +48,7 @@ module Solana::Ruby::Kit
46
48
 
47
49
  InflationReward.new(
48
50
  amount: Kernel.Integer(entry['amount']),
49
- commission: Kernel.Integer(entry['commission']),
51
+ commission: entry['commission'] ? Kernel.Integer(entry['commission']) : nil,
50
52
  effective_slot: Kernel.Integer(entry['effectiveSlot']),
51
53
  epoch: Kernel.Integer(entry['epoch']),
52
54
  post_balance: Kernel.Integer(entry['postBalance'])
@@ -0,0 +1,208 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Rpc
6
+ module Api
7
+ # One signature-level entry returned by getTransactionsForAddress when
8
+ # `transaction_details` is `:signatures`.
9
+ # Mirrors TypeScript's GetTransactionsForAddressSignature.
10
+ TransactionsForAddressSignature = T.let(
11
+ Struct.new(
12
+ :block_time, # Integer | nil — Unix timestamp of block production, nil if unavailable
13
+ :confirmation_status, # Symbol | nil — :processed, :confirmed, or :finalized
14
+ :err, # Hash | nil — transaction error, nil if succeeded
15
+ :memo, # String | nil — memo associated with the transaction
16
+ :signature, # String — base-58 encoded transaction signature
17
+ :slot, # Integer — slot containing the transaction
18
+ :transaction_index, # Integer — 0-based index within the block
19
+ keyword_init: true
20
+ ),
21
+ T.untyped
22
+ )
23
+
24
+ # The result envelope shared by every mode of getTransactionsForAddress.
25
+ #
26
+ # `data` holds the matching transactions in the requested sort order. Its
27
+ # element type depends on `transaction_details`: TransactionsForAddressSignature
28
+ # structs for `:signatures`, and raw (String-keyed) response hashes for
29
+ # `:full` — the same shape `Rpc::Api::GetTransaction#get_transaction`
30
+ # returns, so each element can be handed straight to
31
+ # `TransactionIntrospection.decode_transaction_from_rpc_response`.
32
+ #
33
+ # `pagination_token` is the cursor to pass back as `pagination_token` on a
34
+ # subsequent call, or nil when there are no more results.
35
+ #
36
+ # Mirrors TypeScript's GetTransactionsForAddressApiResponse.
37
+ TransactionsForAddressPage = T.let(
38
+ Struct.new(
39
+ :data, # Array<TransactionsForAddressSignature> | Array<Hash>
40
+ :pagination_token, # String | nil — cursor for the next page
41
+ keyword_init: true
42
+ ),
43
+ T.untyped
44
+ )
45
+
46
+ # Returns confirmed transactions that load the given address, with
47
+ # server-side filtering, sorting, and cursor-based pagination.
48
+ #
49
+ # This combines the discovery step of getSignaturesForAddress with
50
+ # filtering and pagination the server applies itself. Note: check your RPC
51
+ # provider for support for this method.
52
+ #
53
+ # Mirrors TypeScript's GetTransactionsForAddressApi.getTransactionsForAddress.
54
+ # See https://solana.com/docs/rpc/http/gettransactionsforaddress
55
+ module GetTransactionsForAddress
56
+ extend T::Sig
57
+
58
+ # Maps the snake_case keys of a Ruby +filters+ hash onto the wire names.
59
+ # Comparison operators (:eq, :gt, :gte, :lt, :lte) pass through as-is.
60
+ FILTER_KEYS = T.let(
61
+ {
62
+ block_time: 'blockTime',
63
+ signature: 'signature',
64
+ slot: 'slot',
65
+ status: 'status',
66
+ token_accounts: 'tokenAccounts'
67
+ }.freeze,
68
+ T::Hash[Symbol, String]
69
+ )
70
+
71
+ COMPARISON_KEYS = T.let(%i[eq gt gte lt lte].freeze, T::Array[Symbol])
72
+
73
+ # @param address [String] base-58 address to search for.
74
+ # @param transaction_details [Symbol] :signatures (default) or :full.
75
+ # @param encoding [String, nil] 'base58', 'base64', 'json' (server default),
76
+ # or 'jsonParsed'. Only valid with `transaction_details: :full`.
77
+ # @param max_supported_transaction_version [Integer, nil] newest transaction
78
+ # version to accept. Omit to receive only legacy transactions (in which
79
+ # case no `version` is returned). Only valid with `transaction_details: :full`.
80
+ # @param commitment [Symbol, nil] :confirmed or :finalized. This method does
81
+ # not support :processed.
82
+ # @param filters [Hash, nil] server-side filters, combined with AND. Supports
83
+ # `block_time:` ({eq/gt/gte/lt/lte}), `signature:` ({gt/gte/lt/lte}),
84
+ # `slot:` ({gt/gte/lt/lte}), `status:` (:any, :succeeded, :failed), and
85
+ # `token_accounts:` (:none, :balance_changed, :all).
86
+ # @param limit [Integer, nil] max results. The server caps this at 1000 for
87
+ # `:signatures` and 100 for `:full`.
88
+ # @param min_context_slot [Integer, nil] reject data older than this slot.
89
+ # @param pagination_token [String, nil] cursor from a previous response's
90
+ # `pagination_token`, formatted "<slot>:<position>".
91
+ # @param sort_order [Symbol, nil] :desc (newest first, server default) or :asc.
92
+ # @return [TransactionsForAddressPage]
93
+ sig do
94
+ params(
95
+ address: String,
96
+ transaction_details: Symbol,
97
+ encoding: T.nilable(String),
98
+ max_supported_transaction_version: T.nilable(Integer),
99
+ commitment: T.nilable(Symbol),
100
+ filters: T.nilable(T::Hash[Symbol, T.untyped]),
101
+ limit: T.nilable(Integer),
102
+ min_context_slot: T.nilable(Integer),
103
+ pagination_token: T.nilable(String),
104
+ sort_order: T.nilable(Symbol)
105
+ ).returns(T.untyped)
106
+ end
107
+ def get_transactions_for_address(
108
+ address,
109
+ transaction_details: :signatures,
110
+ encoding: nil,
111
+ max_supported_transaction_version: nil,
112
+ commitment: nil,
113
+ filters: nil,
114
+ limit: nil,
115
+ min_context_slot: nil,
116
+ pagination_token: nil,
117
+ sort_order: nil
118
+ )
119
+ unless %i[signatures full].include?(transaction_details)
120
+ Kernel.raise ArgumentError, "transaction_details must be :signatures or :full, got #{transaction_details.inspect}"
121
+ end
122
+
123
+ # The RPC rejects `processed` for this method; TypeScript encodes that
124
+ # as `Exclude<Commitment, 'processed'>`, so fail fast here too.
125
+ if commitment == :processed
126
+ Kernel.raise ArgumentError, 'getTransactionsForAddress does not support the :processed commitment'
127
+ end
128
+
129
+ if transaction_details == :signatures
130
+ if encoding
131
+ Kernel.raise ArgumentError, 'encoding is only valid with transaction_details: :full'
132
+ end
133
+ if max_supported_transaction_version
134
+ Kernel.raise ArgumentError, 'max_supported_transaction_version is only valid with transaction_details: :full'
135
+ end
136
+ end
137
+
138
+ config = { 'transactionDetails' => transaction_details.to_s }
139
+ config['encoding'] = encoding if encoding
140
+ config['maxSupportedTransactionVersion'] = max_supported_transaction_version if max_supported_transaction_version
141
+ config['commitment'] = commitment.to_s if commitment
142
+ config['limit'] = limit if limit
143
+ config['minContextSlot'] = min_context_slot if min_context_slot
144
+ config['paginationToken'] = pagination_token if pagination_token
145
+ config['sortOrder'] = sort_order.to_s if sort_order
146
+ config['filters'] = build_filters(filters) if filters && !filters.empty?
147
+
148
+ raw = transport.request('getTransactionsForAddress', [address, config])
149
+ rows = raw['data'] || []
150
+
151
+ data = if transaction_details == :full
152
+ # Returned verbatim so each element can be fed to
153
+ # TransactionIntrospection.decode_transaction_from_rpc_response.
154
+ rows
155
+ else
156
+ rows.map do |tx|
157
+ TransactionsForAddressSignature.new(
158
+ block_time: tx['blockTime'],
159
+ confirmation_status: tx['confirmationStatus']&.to_sym,
160
+ err: tx['err'],
161
+ memo: tx['memo'],
162
+ signature: tx['signature'],
163
+ slot: Kernel.Integer(tx['slot']),
164
+ transaction_index: tx['transactionIndex'] && Kernel.Integer(tx['transactionIndex'])
165
+ )
166
+ end
167
+ end
168
+
169
+ TransactionsForAddressPage.new(data: data, pagination_token: raw['paginationToken'])
170
+ end
171
+
172
+ # ── Private helpers ────────────────────────────────────────────────────
173
+
174
+ # Converts the snake_case +filters+ hash into its wire form. Symbol values
175
+ # (`status`, `token_accounts`) are lowerCamelCased so `:balance_changed`
176
+ # reaches the server as `"balanceChanged"`.
177
+ sig { params(filters: T::Hash[Symbol, T.untyped]).returns(T::Hash[String, T.untyped]) }
178
+ def build_filters(filters)
179
+ filters.each_with_object({}) do |(key, value), out|
180
+ wire_key = FILTER_KEYS[key]
181
+ Kernel.raise ArgumentError, "unknown filter #{key.inspect}" if wire_key.nil?
182
+
183
+ out[wire_key] = if value.is_a?(Hash)
184
+ value.each_with_object({}) do |(op, operand), cmp|
185
+ unless COMPARISON_KEYS.include?(op)
186
+ Kernel.raise ArgumentError, "unknown comparison #{op.inspect} in filter #{key.inspect}"
187
+ end
188
+ cmp[op.to_s] = operand
189
+ end
190
+ elsif value.is_a?(Symbol)
191
+ camelize(value)
192
+ else
193
+ value
194
+ end
195
+ end
196
+ end
197
+ private :build_filters
198
+
199
+ sig { params(value: Symbol).returns(String) }
200
+ def camelize(value)
201
+ head, *rest = value.to_s.split('_')
202
+ ([head] + rest.map(&:capitalize)).join
203
+ end
204
+ private :camelize
205
+ end
206
+ end
207
+ end
208
+ 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
@@ -24,6 +24,7 @@ require_relative 'api/get_block_time'
24
24
  require_relative 'api/get_epoch_schedule'
25
25
  require_relative 'api/get_inflation_reward'
26
26
  require_relative 'api/get_signatures_for_address'
27
+ require_relative 'api/get_transactions_for_address'
27
28
 
28
29
  module Solana::Ruby::Kit
29
30
  module Rpc
@@ -64,6 +65,7 @@ module Solana::Ruby::Kit
64
65
  include Api::GetEpochSchedule
65
66
  include Api::GetInflationReward
66
67
  include Api::GetSignaturesForAddress
68
+ include Api::GetTransactionsForAddress
67
69
 
68
70
  sig { returns(Transport) }
69
71
  attr_reader :transport