solana-ruby-kit 0.1.7 → 0.1.9

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: 4a6261831e387559dd450f4da24966c5fb21a3d6d079eca64bc1c19577f1517c
4
- data.tar.gz: bb79df5d4217eb20f8fc07b9ae89e6ce070fbcb38f193b4e775d38b1cfe72998
3
+ metadata.gz: 1d26df33c1471059cde38ac46ec2604e0c26ffc8d64c580de4171396516530fd
4
+ data.tar.gz: dba1b05e818b10e8fa7692f6ae4af89c3946bcd19707be52cfdfd1c503e81b63
5
5
  SHA512:
6
- metadata.gz: 6259e78a36520927779d1912d3cd2190690b207fdb538473ed2eabc47e153f9e16380570043087ff94aba46019ef66c5be25383f237f82ad21949cccdb6200c9
7
- data.tar.gz: 9155affe61d202d5e9847b9df5da10958ee85089410cf0bd2f856b77087710d38b3318c3c178057fa9112cfee16d5bb85f51b3d9ae36bfcfce1c01b351f04a00
6
+ metadata.gz: 56674f680f3059cd3247591fe2a14f303853b8806e251f4c147de90fb4ff7ebcea498394a2650aab40089d3cb5e52b10686cd3c5797e171c230a21ef05602eb3
7
+ data.tar.gz: b57dd91be86b034b1a207c809a04c38ca045700f608e4481ac125536d546f21f68e796f01930259a5054ea5ecf72f020f9260d4b63f6661dd177525c32ab924d
@@ -46,6 +46,10 @@ 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
@@ -88,13 +92,12 @@ module Solana::Ruby::Kit
88
92
  RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
89
93
  RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER
90
94
  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)
95
+ # JSON-RPC server errors (see https://github.com/anza-xyz/kit/blob/main/packages/errors/src/codes.ts)
95
96
  JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE
96
97
  JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY
97
98
  JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE
99
+ JSON_RPC__SERVER_ERROR_NO_SLOT_HISTORY = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NO_SLOT_HISTORY
100
+ JSON_RPC__SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND
98
101
  RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST
99
102
  RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID
100
103
  RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED
@@ -120,6 +123,12 @@ module Solana::Ruby::Kit
120
123
  WALLET_STANDARD__INVALID_WIRE_FORMAT = :SOLANA_ERROR__WALLET_STANDARD__INVALID_WIRE_FORMAT
121
124
  WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED = :SOLANA_ERROR__WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED
122
125
 
126
+ # ── Subscribable ──────────────────────────────────────────────────────────
127
+ SUBSCRIBABLE__RETRY_NOT_SUPPORTED = :SOLANA_ERROR__SUBSCRIBABLE__RETRY_NOT_SUPPORTED
128
+
129
+ # ── Fixed-points ──────────────────────────────────────────────────────────
130
+ FIXED_POINTS__STRICT_MODE_PRECISION_LOSS = :SOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS
131
+
123
132
  # ── Invariant violations (internal) ──────────────────────────────────────
124
133
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING
125
134
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
@@ -166,6 +175,10 @@ module Solana::Ruby::Kit
166
175
 
167
176
  # Transactions
168
177
  TRANSACTIONS__TRANSACTION_NOT_SIGNABLE => 'Transaction is not signable (missing fee payer or lifetime constraint)',
178
+ 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.',
179
+ TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT => 'Transaction failed when it was simulated in order to estimate the compute unit consumption.',
180
+ 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.',
181
+ TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_RESOURCE_LIMITS => 'Transaction failed when it was simulated in order to estimate its resource limits.',
169
182
  TRANSACTIONS__EXCEEDS_SIZE_LIMIT => 'Transaction wire size (%{actual_size} bytes) exceeds the limit of %{limit} bytes',
170
183
  TRANSACTIONS__MISSING_SIGNER => 'Transaction is missing a required signer: %{address}',
171
184
  TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE => 'Transaction version %{version} is out of range',
@@ -211,6 +224,8 @@ module Solana::Ruby::Kit
211
224
  JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE => 'Epoch rewards period still active at slot %{slot}',
212
225
  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",
213
226
  JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE => 'Failed to query long-term storage; please try again',
227
+ JSON_RPC__SERVER_ERROR_NO_SLOT_HISTORY => 'No slot history',
228
+ JSON_RPC__SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND => 'Filter transaction not found',
214
229
  RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST => 'Cannot create subscription request',
215
230
  RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID => 'Expected server to return a subscription ID',
216
231
  RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED => 'WebSocket channel closed before message could be buffered',
@@ -231,6 +246,12 @@ module Solana::Ruby::Kit
231
246
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN => 'Instruction plan is empty and produced no transaction messages',
232
247
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
233
248
 
249
+ # Subscribable
250
+ SUBSCRIBABLE__RETRY_NOT_SUPPORTED => 'This reactive store does not support retry(); use create_reactive_store_from_data_publisher_factory for a retryable store',
251
+
252
+ # Fixed-points
253
+ FIXED_POINTS__STRICT_MODE_PRECISION_LOSS => 'Value has more than 9 fractional digits and cannot be represented exactly as a Sol amount',
254
+
234
255
  # Wallet Standard
235
256
  WALLET_STANDARD__INVALID_WIRE_FORMAT => 'Invalid transaction wire format: %{reason}',
236
257
  WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED => 'Signature verification failed for signer %{address}',
@@ -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,20 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Rpc
6
+ module Api
7
+ # Returns the estimated production time of a block.
8
+ # Mirrors TypeScript's GetBlockTimeApi.getBlockTime.
9
+ module GetBlockTime
10
+ extend T::Sig
11
+
12
+ sig { params(slot: Integer).returns(T.nilable(Integer)) }
13
+ def get_block_time(slot)
14
+ raw = transport.request('getBlockTime', [slot])
15
+ raw.nil? ? nil : Kernel.Integer(raw)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Rpc
6
+ module Api
7
+ # Struct returned by get_epoch_schedule.
8
+ EpochSchedule = T.let(
9
+ Struct.new(
10
+ :first_normal_epoch, # Integer — first full-length epoch after warmup
11
+ :first_normal_slot, # Integer — first slot after warmup period
12
+ :leader_schedule_slot_offset, # Integer — slots before epoch to compute leader schedule
13
+ :slots_per_epoch, # Integer — max slots per epoch
14
+ :warmup, # Boolean — whether epochs start short and grow
15
+ keyword_init: true
16
+ ),
17
+ T.untyped
18
+ )
19
+
20
+ # Returns the epoch schedule from the cluster's genesis config.
21
+ # Mirrors TypeScript's GetEpochScheduleApi.getEpochSchedule.
22
+ module GetEpochSchedule
23
+ extend T::Sig
24
+
25
+ sig { returns(T.untyped) }
26
+ def get_epoch_schedule
27
+ raw = transport.request('getEpochSchedule', [])
28
+
29
+ EpochSchedule.new(
30
+ first_normal_epoch: Kernel.Integer(raw['firstNormalEpoch']),
31
+ first_normal_slot: Kernel.Integer(raw['firstNormalSlot']),
32
+ leader_schedule_slot_offset: Kernel.Integer(raw['leaderScheduleSlotOffset']),
33
+ slots_per_epoch: Kernel.Integer(raw['slotsPerEpoch']),
34
+ warmup: raw['warmup'] == true
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,59 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Rpc
6
+ module Api
7
+ # Struct for a single entry in the getInflationReward response.
8
+ # nil entries indicate the address was not found or had no reward.
9
+ InflationReward = T.let(
10
+ Struct.new(
11
+ :amount, # Integer (Lamports) — reward credited
12
+ :commission, # Integer — vote account commission at reward time
13
+ :effective_slot, # Integer — slot in which rewards were delivered
14
+ :epoch, # Integer — epoch for which reward occurred
15
+ :post_balance, # Integer (Lamports) — post-reward account balance
16
+ keyword_init: true
17
+ ),
18
+ T.untyped
19
+ )
20
+
21
+ # Returns the inflation/staking reward for a list of addresses for an epoch.
22
+ # Mirrors TypeScript's GetInflationRewardApi.getInflationReward.
23
+ module GetInflationReward
24
+ extend T::Sig
25
+
26
+ sig do
27
+ params(
28
+ addresses: T::Array[T.untyped],
29
+ commitment: T.nilable(Symbol),
30
+ epoch: T.nilable(Integer),
31
+ min_context_slot: T.nilable(Integer)
32
+ ).returns(T::Array[T.nilable(T.untyped)])
33
+ end
34
+ def get_inflation_reward(addresses, commitment: nil, epoch: nil, min_context_slot: nil)
35
+ addr_strings = addresses.map { |a| a.respond_to?(:value) ? a.value : a.to_s }
36
+ config = {}
37
+ config['commitment'] = commitment.to_s if commitment
38
+ config['epoch'] = epoch if epoch
39
+ config['minContextSlot'] = min_context_slot if min_context_slot
40
+
41
+ params = config.empty? ? [addr_strings] : [addr_strings, config]
42
+ raw_list = transport.request('getInflationReward', params)
43
+
44
+ raw_list.map do |entry|
45
+ next nil if entry.nil?
46
+
47
+ InflationReward.new(
48
+ amount: Kernel.Integer(entry['amount']),
49
+ commission: Kernel.Integer(entry['commission']),
50
+ effective_slot: Kernel.Integer(entry['effectiveSlot']),
51
+ epoch: Kernel.Integer(entry['epoch']),
52
+ post_balance: Kernel.Integer(entry['postBalance'])
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ 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
@@ -20,6 +20,10 @@ require_relative 'api/get_token_accounts_by_owner'
20
20
  require_relative 'api/get_epoch_info'
21
21
  require_relative 'api/get_vote_accounts'
22
22
  require_relative 'api/simulate_transaction'
23
+ require_relative 'api/get_block_time'
24
+ require_relative 'api/get_epoch_schedule'
25
+ require_relative 'api/get_inflation_reward'
26
+ require_relative 'api/get_signatures_for_address'
23
27
 
24
28
  module Solana::Ruby::Kit
25
29
  module Rpc
@@ -56,6 +60,10 @@ module Solana::Ruby::Kit
56
60
  include Api::GetEpochInfo
57
61
  include Api::GetVoteAccounts
58
62
  include Api::SimulateTransaction
63
+ include Api::GetBlockTime
64
+ include Api::GetEpochSchedule
65
+ include Api::GetInflationReward
66
+ include Api::GetSignaturesForAddress
59
67
 
60
68
  sig { returns(Transport) }
61
69
  attr_reader :transport
@@ -117,10 +117,9 @@ module Solana::Ruby::Kit
117
117
  http.open_timeout = @open_timeout
118
118
 
119
119
  req = Net::HTTP::Post.new(T.cast(@uri, URI::HTTP).request_uri)
120
- req['Content-Type'] = 'application/json; charset=utf-8'
121
- req['Accept'] = 'application/json'
122
- req['Content-Length'] = body.bytesize.to_s
123
- req['solana-client'] = 'ruby-kit'
120
+ req['Content-Type'] = 'application/json; charset=utf-8'
121
+ req['Accept'] = 'application/json'
122
+ req['solana-client'] = 'ruby-kit'
124
123
  @headers.each { |k, v| req[k] = v }
125
124
  req.body = body
126
125
 
@@ -0,0 +1,102 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'bigdecimal'
5
+ require_relative '../errors'
6
+ require_relative 'lamports'
7
+
8
+ module Solana::Ruby::Kit
9
+ module RpcTypes
10
+ # SOL amounts expressed as a decimal fixed-point value with 9 decimal places.
11
+ # 1 SOL == 1_000_000_000 lamports, so Sol#raw is the exact Lamports count.
12
+ #
13
+ # Mirrors TypeScript's Sol (DecimalFixedPoint<'unsigned', 64, 9>).
14
+ class Sol
15
+ extend T::Sig
16
+
17
+ # The Lamports count as an unsigned integer (u64 range).
18
+ sig { returns(Integer) }
19
+ attr_reader :raw
20
+
21
+ sig { params(raw: Integer).void }
22
+ def initialize(raw)
23
+ @raw = T.let(raw, Integer)
24
+ freeze
25
+ end
26
+
27
+ # Returns the decimal string representation (e.g. "1.5", "0.000000001").
28
+ sig { returns(String) }
29
+ def to_s
30
+ whole = @raw / 10**9
31
+ fraction = @raw % 10**9
32
+ return whole.to_s if fraction.zero?
33
+
34
+ frac_str = fraction.to_s.rjust(9, '0').sub(/0+\z/, '')
35
+ "#{whole}.#{frac_str}"
36
+ end
37
+
38
+ sig { returns(String) }
39
+ def inspect = "#<#{self.class} #{self}>"
40
+
41
+ sig { params(other: T.untyped).returns(T::Boolean) }
42
+ def ==(other)
43
+ other.is_a?(Sol) && @raw == other.raw
44
+ end
45
+
46
+ sig { returns(Integer) }
47
+ def hash = @raw.hash
48
+
49
+ alias eql? ==
50
+ end
51
+
52
+ SOL_DECIMALS = T.let(9, Integer)
53
+ SOL_SCALE = T.let(10**9, Integer)
54
+ SOL_RAW_MAX = T.let(2**64 - 1, Integer)
55
+
56
+ module_function
57
+
58
+ # Parses a decimal string and returns a Sol value.
59
+ # Raises FIXED_POINTS__STRICT_MODE_PRECISION_LOSS if the string has more
60
+ # than 9 fractional digits (mirrors TypeScript's default 'strict' mode).
61
+ #
62
+ # Examples:
63
+ # sol('1.5') # => Sol(raw: 1_500_000_000)
64
+ # sol('0.000000001') # => Sol(raw: 1) — one lamport
65
+ #
66
+ # Mirrors TypeScript's sol(value, rounding?).
67
+ sig { params(value: String).returns(Sol) }
68
+ def sol(value)
69
+ bd = BigDecimal(value)
70
+ Kernel.raise SolanaError.new(SolanaError::FIXED_POINTS__STRICT_MODE_PRECISION_LOSS) if bd < 0
71
+
72
+ scaled = bd * SOL_SCALE
73
+ raw = scaled.to_i
74
+
75
+ unless scaled == BigDecimal(raw.to_s)
76
+ Kernel.raise SolanaError.new(SolanaError::FIXED_POINTS__STRICT_MODE_PRECISION_LOSS)
77
+ end
78
+
79
+ Kernel.raise SolanaError.new(SolanaError::SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE) if raw > SOL_RAW_MAX
80
+
81
+ Sol.new(raw)
82
+ end
83
+
84
+ # Converts a Sol value to its equivalent Lamports integer.
85
+ # The conversion is exact — Sol#raw is the Lamports count.
86
+ #
87
+ # Mirrors TypeScript's solToLamports(value).
88
+ sig { params(value: Sol).returns(Lamports) }
89
+ def sol_to_lamports(value)
90
+ value.raw
91
+ end
92
+
93
+ # Converts a Lamports integer to its equivalent Sol value.
94
+ # The conversion is exact.
95
+ #
96
+ # Mirrors TypeScript's lamportsToSol(value).
97
+ sig { params(value: Lamports).returns(Sol) }
98
+ def lamports_to_sol(value)
99
+ Sol.new(value)
100
+ end
101
+ end
102
+ end
@@ -4,5 +4,6 @@
4
4
  # Core RPC type definitions — mirrors @solana/rpc-types.
5
5
  require_relative 'rpc_types/commitment'
6
6
  require_relative 'rpc_types/lamports'
7
+ require_relative 'rpc_types/sol'
7
8
  require_relative 'rpc_types/cluster_url'
8
9
  require_relative 'rpc_types/account_info'
@@ -0,0 +1,207 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Solana::Ruby::Kit
5
+ module Subscribable
6
+ extend T::Sig
7
+
8
+ # Lifecycle status of a ReactiveActionStore.
9
+ # Mirrors TypeScript's ReactiveActionStatus.
10
+ ReactiveActionStatus = T.type_alias { String } # 'idle' | 'running' | 'success' | 'error'
11
+
12
+ # Lifecycle snapshot of a ReactiveActionStore.
13
+ # `data` persists across running/error states so callers can render stale
14
+ # content while a retry is in flight; only reset() clears it.
15
+ #
16
+ # Mirrors TypeScript's ReactiveActionState<TResult>.
17
+ class ReactiveActionState
18
+ extend T::Sig
19
+
20
+ sig { returns(String) }
21
+ attr_reader :status
22
+
23
+ sig { returns(T.untyped) }
24
+ attr_reader :data
25
+
26
+ sig { returns(T.untyped) }
27
+ attr_reader :error
28
+
29
+ sig { params(status: String, data: T.untyped, error: T.untyped).void }
30
+ def initialize(status:, data: nil, error: nil)
31
+ @status = T.let(status, String)
32
+ @data = T.let(data, T.untyped)
33
+ @error = T.let(error, T.untyped)
34
+ freeze
35
+ end
36
+
37
+ sig { params(other: T.untyped).returns(T::Boolean) }
38
+ def ==(other)
39
+ other.is_a?(ReactiveActionState) &&
40
+ @status == other.status &&
41
+ @data.equal?(other.data) &&
42
+ @error.equal?(other.error)
43
+ end
44
+ end
45
+
46
+ REACTIVE_ACTION_IDLE_STATE = T.let(
47
+ ReactiveActionState.new(status: 'idle').freeze,
48
+ ReactiveActionState
49
+ )
50
+
51
+ # A thread-safe state machine wrapping a callable. Exposes
52
+ # dispatch / dispatch_async / get_state / subscribe / reset
53
+ # so that observers can react to in-flight, succeeded, or failed calls.
54
+ #
55
+ # dispatch — fire-and-forget; runs the callable in a background thread.
56
+ # dispatch_async — blocking; runs the callable in the current thread and
57
+ # returns the result (or raises on failure / supersession).
58
+ #
59
+ # Only the most recent dispatch can mutate state — superseded calls silently
60
+ # drop their result via a generation counter.
61
+ #
62
+ # Mirrors TypeScript's ReactiveActionStore<TArgs, TResult>.
63
+ class ReactiveActionStore
64
+ extend T::Sig
65
+
66
+ sig { params(fn: T.proc.params(args: T.untyped).returns(T.untyped)).void }
67
+ def initialize(fn)
68
+ @fn = T.let(fn, T.proc.params(args: T.untyped).returns(T.untyped))
69
+ @state = T.let(REACTIVE_ACTION_IDLE_STATE, ReactiveActionState)
70
+ @listeners = T.let([], T::Array[T.proc.void])
71
+ @mutex = T.let(Mutex.new, Mutex)
72
+ @current_gen = T.let(0, Integer)
73
+ end
74
+
75
+ # Returns the current lifecycle snapshot.
76
+ sig { returns(ReactiveActionState) }
77
+ def get_state
78
+ @mutex.synchronize { @state }
79
+ end
80
+
81
+ # Fire-and-forget dispatch. Runs fn in a background thread. Only the
82
+ # most recent call's result updates state; superseded threads are ignored.
83
+ sig { params(args: T.untyped).void }
84
+ def dispatch(*args)
85
+ gen = nil
86
+ prev_data = nil
87
+ @mutex.synchronize do
88
+ @current_gen += 1
89
+ gen = @current_gen
90
+ prev_data = @state.data
91
+ @state = ReactiveActionState.new(status: 'running', data: prev_data)
92
+ end
93
+ _notify
94
+
95
+ Thread.new do
96
+ begin
97
+ result = @fn.call(*args)
98
+ active = @mutex.synchronize do
99
+ if @current_gen == gen
100
+ @state = ReactiveActionState.new(status: 'success', data: result)
101
+ true
102
+ else
103
+ false
104
+ end
105
+ end
106
+ _notify if active
107
+ rescue => e
108
+ active = @mutex.synchronize do
109
+ if @current_gen == gen
110
+ @state = ReactiveActionState.new(status: 'error', data: prev_data, error: e)
111
+ true
112
+ else
113
+ false
114
+ end
115
+ end
116
+ _notify if active
117
+ end
118
+ end
119
+
120
+ nil
121
+ end
122
+
123
+ # Blocking dispatch. Runs fn in the current thread. Returns the result on
124
+ # success. Raises the error on failure. Raises RuntimeError if superseded
125
+ # by a concurrent dispatch or reset().
126
+ sig { params(args: T.untyped).returns(T.untyped) }
127
+ def dispatch_async(*args)
128
+ gen = nil
129
+ prev_data = nil
130
+ @mutex.synchronize do
131
+ @current_gen += 1
132
+ gen = @current_gen
133
+ prev_data = @state.data
134
+ @state = ReactiveActionState.new(status: 'running', data: prev_data)
135
+ end
136
+ _notify
137
+
138
+ result = @fn.call(*args)
139
+
140
+ active = @mutex.synchronize do
141
+ if @current_gen == gen
142
+ @state = ReactiveActionState.new(status: 'success', data: result)
143
+ true
144
+ else
145
+ false
146
+ end
147
+ end
148
+ _notify if active
149
+ Kernel.raise 'ReactiveActionStore: call was superseded' unless active
150
+ result
151
+ rescue RuntimeError
152
+ Kernel.raise
153
+ rescue => e
154
+ active = @mutex.synchronize do
155
+ if @current_gen == gen
156
+ @state = ReactiveActionState.new(status: 'error', data: prev_data, error: e)
157
+ true
158
+ else
159
+ false
160
+ end
161
+ end
162
+ _notify if active
163
+ Kernel.raise e
164
+ end
165
+
166
+ # Aborts any in-flight background dispatch (by incrementing generation)
167
+ # and resets state to idle.
168
+ sig { void }
169
+ def reset
170
+ @mutex.synchronize do
171
+ @current_gen += 1
172
+ @state = REACTIVE_ACTION_IDLE_STATE
173
+ end
174
+ _notify
175
+ end
176
+
177
+ # Registers a listener called on every state change.
178
+ # Returns an unsubscribe lambda.
179
+ sig { params(listener: T.proc.void).returns(T.proc.void) }
180
+ def subscribe(&listener)
181
+ @mutex.synchronize { @listeners << listener }
182
+ lambda { @mutex.synchronize { @listeners.delete(listener) } }
183
+ end
184
+
185
+ private
186
+
187
+ sig { void }
188
+ def _notify
189
+ subs = @mutex.synchronize { @listeners.dup }
190
+ subs.each(&:call)
191
+ end
192
+ end
193
+
194
+ module_function
195
+
196
+ # Wraps a callable in a ReactiveActionStore.
197
+ # fn receives the arguments passed to dispatch/dispatch_async.
198
+ #
199
+ # Mirrors TypeScript's createReactiveActionStore.
200
+ sig do
201
+ params(fn: T.proc.params(args: T.untyped).returns(T.untyped)).returns(ReactiveActionStore)
202
+ end
203
+ def create_reactive_action_store(&fn)
204
+ ReactiveActionStore.new(T.let(fn, T.proc.params(args: T.untyped).returns(T.untyped)))
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,255 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../errors'
5
+ require_relative 'data_publisher'
6
+
7
+ module Solana::Ruby::Kit
8
+ module Subscribable
9
+ extend T::Sig
10
+
11
+ # Lifecycle snapshot of a ReactiveStreamStore — a single frozen value
12
+ # capturing status, last-known data, and any error.
13
+ #
14
+ # status values:
15
+ # 'loading' — waiting for first value; data is nil
16
+ # 'loaded' — value has arrived, no active error
17
+ # 'error' — stream failed; data is last known value (may be nil)
18
+ # 'retrying' — retry in progress after error; data preserved
19
+ #
20
+ # Mirrors TypeScript's ReactiveState<T>.
21
+ class ReactiveState
22
+ extend T::Sig
23
+
24
+ sig { returns(String) }
25
+ attr_reader :status
26
+
27
+ sig { returns(T.untyped) }
28
+ attr_reader :data
29
+
30
+ sig { returns(T.untyped) }
31
+ attr_reader :error
32
+
33
+ sig { params(status: String, data: T.untyped, error: T.untyped).void }
34
+ def initialize(status:, data: nil, error: nil)
35
+ @status = T.let(status, String)
36
+ @data = T.let(data, T.untyped)
37
+ @error = T.let(error, T.untyped)
38
+ freeze
39
+ end
40
+
41
+ sig { params(other: T.untyped).returns(T::Boolean) }
42
+ def ==(other)
43
+ other.is_a?(ReactiveState) &&
44
+ @status == other.status &&
45
+ @data.equal?(other.data) &&
46
+ @error.equal?(other.error)
47
+ end
48
+ end
49
+
50
+ REACTIVE_LOADING_STATE = T.let(
51
+ ReactiveState.new(status: 'loading').freeze,
52
+ ReactiveState
53
+ )
54
+
55
+ # Thread-safe store that tracks the latest value published to a data channel
56
+ # and notifies subscribers on every state change.
57
+ #
58
+ # Compatible with any observer pattern that expects a
59
+ # { subscribe, get_unified_state } contract.
60
+ #
61
+ # Mirrors TypeScript's ReactiveStreamStore<T>.
62
+ class ReactiveStreamStore
63
+ extend T::Sig
64
+
65
+ sig { void }
66
+ def initialize
67
+ @state = T.let(REACTIVE_LOADING_STATE, ReactiveState)
68
+ @subscribers = T.let([], T::Array[T.proc.void])
69
+ @mutex = T.let(Mutex.new, Mutex)
70
+ end
71
+
72
+ # Returns the current lifecycle snapshot.
73
+ sig { returns(ReactiveState) }
74
+ def get_unified_state
75
+ @mutex.synchronize { @state }
76
+ end
77
+
78
+ # Returns the most recent data value, or nil if no value has arrived yet.
79
+ # @deprecated Use get_unified_state instead.
80
+ sig { returns(T.untyped) }
81
+ def get_state
82
+ @mutex.synchronize { @state.data }
83
+ end
84
+
85
+ # Returns the current error, or nil if no error has occurred.
86
+ # @deprecated Use get_unified_state instead.
87
+ sig { returns(T.untyped) }
88
+ def get_error
89
+ @mutex.synchronize { @state.error }
90
+ end
91
+
92
+ # Re-opens the stream after an error. Raises RETRY_NOT_SUPPORTED on
93
+ # DataPublisher-backed stores — use create_reactive_store_from_data_publisher_factory
94
+ # for a retryable store.
95
+ sig { void }
96
+ def retry
97
+ Kernel.raise SolanaError.new(SolanaError::SUBSCRIBABLE__RETRY_NOT_SUPPORTED)
98
+ end
99
+
100
+ # Registers a callback invoked on every state change.
101
+ # Returns an unsubscribe lambda.
102
+ sig { params(callback: T.proc.void).returns(T.proc.void) }
103
+ def subscribe(&callback)
104
+ @mutex.synchronize { @subscribers << callback }
105
+ lambda { @mutex.synchronize { @subscribers.delete(callback) } }
106
+ end
107
+
108
+ # Internal: transition state and notify all subscribers.
109
+ sig { params(new_state: ReactiveState).void }
110
+ def _set_state(new_state)
111
+ subs = nil
112
+ @mutex.synchronize do
113
+ return if @state.equal?(new_state)
114
+ @state = new_state
115
+ subs = @subscribers.dup
116
+ end
117
+ subs&.each(&:call)
118
+ end
119
+ end
120
+
121
+ # Deprecated alias — use ReactiveStreamStore.
122
+ ReactiveStore = ReactiveStreamStore
123
+
124
+ # A ReactiveStreamStore that supports retry() by re-invoking a factory proc
125
+ # to get a fresh DataPublisher on each connection attempt.
126
+ # Mirrors TypeScript's createReactiveStoreFromDataPublisherFactory result.
127
+ class RetryableReactiveStreamStore < ReactiveStreamStore
128
+ extend T::Sig
129
+
130
+ sig do
131
+ params(
132
+ data_channel: T.untyped,
133
+ error_channel: T.untyped,
134
+ create_publisher: T.proc.returns(DataPublisher),
135
+ signal: T.nilable(T.proc.void)
136
+ ).void
137
+ end
138
+ def initialize(data_channel:, error_channel:, create_publisher:, signal: nil)
139
+ super()
140
+ @data_channel = T.let(data_channel, T.untyped)
141
+ @error_channel = T.let(error_channel, T.untyped)
142
+ @outer_signal = T.let(signal, T.nilable(T.proc.void))
143
+ @create_publisher = T.let(create_publisher, T.proc.returns(DataPublisher))
144
+ @stopped = T.let(false, T::Boolean)
145
+ # Per-connection active flag shared with subscriber lambdas via closure.
146
+ @conn_active = T.let([false], T::Array[T::Boolean])
147
+ _connect
148
+ end
149
+
150
+ sig { override.void }
151
+ def retry
152
+ stopped = @mutex.synchronize { @stopped }
153
+ return if stopped
154
+ return unless get_unified_state.status == 'error'
155
+
156
+ stale_data = get_unified_state.data
157
+ # Deactivate old connection's subscribers
158
+ @mutex.synchronize { @conn_active[0] = false }
159
+ _set_state(ReactiveState.new(status: 'retrying', data: stale_data))
160
+ _connect
161
+ end
162
+
163
+ private
164
+
165
+ sig { void }
166
+ def _connect
167
+ return if @mutex.synchronize { @stopped }
168
+
169
+ # Fresh active flag for this connection window
170
+ active = [true]
171
+ @mutex.synchronize { @conn_active = active }
172
+
173
+ # inner_signal fires (raises) once this connection is superseded or errors out,
174
+ # which causes DataPublisher to prune those subscribers automatically.
175
+ inner_signal = lambda do
176
+ @outer_signal&.call
177
+ Kernel.raise 'connection_inactive' unless active[0]
178
+ end
179
+
180
+ begin
181
+ publisher = @create_publisher.call
182
+ rescue => e
183
+ active[0] = false
184
+ stale_data = get_unified_state.data
185
+ _set_state(ReactiveState.new(status: 'error', data: stale_data, error: e))
186
+ return
187
+ end
188
+
189
+ publisher.on(@data_channel, signal: inner_signal) do |data|
190
+ _set_state(ReactiveState.new(status: 'loaded', data: data))
191
+ end
192
+
193
+ publisher.on(@error_channel, signal: inner_signal) do |err|
194
+ next unless active[0] && get_unified_state.status != 'error'
195
+ active[0] = false
196
+ stale_data = get_unified_state.data
197
+ _set_state(ReactiveState.new(status: 'error', data: stale_data, error: err))
198
+ end
199
+ end
200
+ end
201
+
202
+ module_function
203
+
204
+ # Creates a ReactiveStreamStore backed by a ready-made DataPublisher.
205
+ # retry() is not supported — see create_reactive_store_from_data_publisher_factory.
206
+ #
207
+ # Mirrors TypeScript's createReactiveStoreFromDataPublisher (deprecated in TS).
208
+ sig do
209
+ params(
210
+ publisher: DataPublisher,
211
+ data_channel: T.untyped,
212
+ error_channel: T.untyped,
213
+ signal: T.nilable(T.proc.void)
214
+ ).returns(ReactiveStreamStore)
215
+ end
216
+ def create_reactive_store_from_data_publisher(publisher, data_channel:, error_channel:, signal: nil)
217
+ store = ReactiveStreamStore.new
218
+
219
+ publisher.on(data_channel, signal: signal) do |data|
220
+ store._set_state(ReactiveState.new(status: 'loaded', data: data))
221
+ end
222
+
223
+ publisher.on(error_channel, signal: signal) do |err|
224
+ unified = store.get_unified_state
225
+ next if unified.status == 'error'
226
+ store._set_state(ReactiveState.new(status: 'error', data: unified.data, error: err))
227
+ end
228
+
229
+ store
230
+ end
231
+
232
+ # Creates a retryable ReactiveStreamStore from a DataPublisher factory proc.
233
+ # The factory is called once immediately and again on each retry().
234
+ #
235
+ # Mirrors TypeScript's createReactiveStoreFromDataPublisherFactory.
236
+ sig do
237
+ params(
238
+ data_channel: T.untyped,
239
+ error_channel: T.untyped,
240
+ signal: T.nilable(T.proc.void),
241
+ blk: T.proc.returns(DataPublisher)
242
+ ).returns(ReactiveStreamStore)
243
+ end
244
+ def create_reactive_store_from_data_publisher_factory(
245
+ data_channel:, error_channel:, signal: nil, &blk
246
+ )
247
+ RetryableReactiveStreamStore.new(
248
+ data_channel: data_channel,
249
+ error_channel: error_channel,
250
+ create_publisher: T.let(blk, T.proc.returns(DataPublisher)),
251
+ signal: signal
252
+ )
253
+ end
254
+ end
255
+ end
@@ -6,6 +6,8 @@ require 'timeout'
6
6
  # Mirrors @solana/subscribable.
7
7
  require_relative 'subscribable/data_publisher'
8
8
  require_relative 'subscribable/async_iterable'
9
+ require_relative 'subscribable/reactive_stream_store'
10
+ require_relative 'subscribable/reactive_action_store'
9
11
 
10
12
  module Solana::Ruby::Kit
11
13
  module Subscribable
@@ -0,0 +1,145 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../addresses/address'
5
+ require_relative '../instructions/instruction'
6
+ require_relative 'transaction_message'
7
+
8
+ module Solana::Ruby::Kit
9
+ module TransactionMessages
10
+ # Address of the Compute Budget program.
11
+ COMPUTE_BUDGET_PROGRAM_ADDRESS = T.let(
12
+ Addresses::Address.new('ComputeBudget111111111111111111111111111111'),
13
+ Addresses::Address
14
+ )
15
+
16
+ # Maximum compute unit limit per transaction (from Agave execution_budget.rs).
17
+ MAX_COMPUTE_UNIT_LIMIT = T.let(1_400_000, Integer)
18
+
19
+ # Maximum loaded accounts data size limit per transaction (64 MiB, from Agave).
20
+ MAX_LOADED_ACCOUNTS_DATA_SIZE_LIMIT = T.let(64 * 1024 * 1024, Integer)
21
+
22
+ SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR = T.let(2, Integer)
23
+ SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT_DISCRIMINATOR = T.let(4, Integer)
24
+
25
+ module_function
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # SetComputeUnitLimit instruction helpers
29
+ # ---------------------------------------------------------------------------
30
+
31
+ sig { params(units: Integer).returns(Instructions::Instruction) }
32
+ def get_set_compute_unit_limit_instruction(units)
33
+ data = ([SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR].pack('C') + [units].pack('V')).b
34
+ Instructions::Instruction.new(
35
+ program_address: COMPUTE_BUDGET_PROGRAM_ADDRESS,
36
+ accounts: nil,
37
+ data: data
38
+ )
39
+ end
40
+
41
+ sig { params(instruction: Instructions::Instruction).returns(T::Boolean) }
42
+ def set_compute_unit_limit_instruction?(instruction)
43
+ instruction.program_address == COMPUTE_BUDGET_PROGRAM_ADDRESS &&
44
+ !instruction.data.nil? &&
45
+ T.must(instruction.data).bytesize == 5 &&
46
+ T.must(instruction.data).getbyte(0) == SET_COMPUTE_UNIT_LIMIT_DISCRIMINATOR
47
+ end
48
+
49
+ sig { params(data: String).returns(Integer) }
50
+ def compute_unit_limit_from_instruction_data(data)
51
+ T.must(data[1, 4]).unpack1('V')
52
+ end
53
+
54
+ # Returns the compute unit limit set on a transaction message, or nil if none is set.
55
+ # Mirrors `getTransactionMessageComputeUnitLimit` from @solana/transaction-messages.
56
+ sig { params(message: TransactionMessage).returns(T.nilable(Integer)) }
57
+ def get_transaction_message_compute_unit_limit(message)
58
+ ix = message.instructions.find { |i| set_compute_unit_limit_instruction?(i) }
59
+ ix ? compute_unit_limit_from_instruction_data(T.must(ix.data)) : nil
60
+ end
61
+
62
+ # Sets the compute unit limit on a transaction message, appending or replacing the
63
+ # SetComputeUnitLimit instruction. Mirrors `setTransactionMessageComputeUnitLimit`.
64
+ sig { params(limit: Integer, message: TransactionMessage).returns(TransactionMessage) }
65
+ def set_transaction_message_compute_unit_limit(limit, message)
66
+ existing_idx = message.instructions.index { |i| set_compute_unit_limit_instruction?(i) }
67
+ new_ix = get_set_compute_unit_limit_instruction(limit)
68
+
69
+ if existing_idx.nil?
70
+ append_instructions(message, [new_ix])
71
+ elsif compute_unit_limit_from_instruction_data(T.must(message.instructions[existing_idx].data)) == limit
72
+ message
73
+ else
74
+ new_instructions = message.instructions.dup
75
+ new_instructions[T.must(existing_idx)] = new_ix
76
+ TransactionMessage.new(
77
+ version: message.version,
78
+ instructions: new_instructions,
79
+ fee_payer: message.fee_payer,
80
+ lifetime_constraint: message.lifetime_constraint,
81
+ address_table_lookups: message.address_table_lookups
82
+ )
83
+ end
84
+ end
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # SetLoadedAccountsDataSizeLimit instruction helpers
88
+ # ---------------------------------------------------------------------------
89
+
90
+ sig { params(limit: Integer).returns(Instructions::Instruction) }
91
+ def get_set_loaded_accounts_data_size_limit_instruction(limit)
92
+ data = ([SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT_DISCRIMINATOR].pack('C') + [limit].pack('V')).b
93
+ Instructions::Instruction.new(
94
+ program_address: COMPUTE_BUDGET_PROGRAM_ADDRESS,
95
+ accounts: nil,
96
+ data: data
97
+ )
98
+ end
99
+
100
+ sig { params(instruction: Instructions::Instruction).returns(T::Boolean) }
101
+ def set_loaded_accounts_data_size_limit_instruction?(instruction)
102
+ instruction.program_address == COMPUTE_BUDGET_PROGRAM_ADDRESS &&
103
+ !instruction.data.nil? &&
104
+ T.must(instruction.data).bytesize == 5 &&
105
+ T.must(instruction.data).getbyte(0) == SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT_DISCRIMINATOR
106
+ end
107
+
108
+ sig { params(data: String).returns(Integer) }
109
+ def loaded_accounts_data_size_limit_from_instruction_data(data)
110
+ T.must(data[1, 4]).unpack1('V')
111
+ end
112
+
113
+ # Returns the loaded accounts data size limit set on a transaction message, or nil.
114
+ # Mirrors `getTransactionMessageLoadedAccountsDataSizeLimit`.
115
+ sig { params(message: TransactionMessage).returns(T.nilable(Integer)) }
116
+ def get_transaction_message_loaded_accounts_data_size_limit(message)
117
+ ix = message.instructions.find { |i| set_loaded_accounts_data_size_limit_instruction?(i) }
118
+ ix ? loaded_accounts_data_size_limit_from_instruction_data(T.must(ix.data)) : nil
119
+ end
120
+
121
+ # Sets the loaded accounts data size limit on a transaction message.
122
+ # Mirrors `setTransactionMessageLoadedAccountsDataSizeLimit`.
123
+ sig { params(limit: Integer, message: TransactionMessage).returns(TransactionMessage) }
124
+ def set_transaction_message_loaded_accounts_data_size_limit(limit, message)
125
+ existing_idx = message.instructions.index { |i| set_loaded_accounts_data_size_limit_instruction?(i) }
126
+ new_ix = get_set_loaded_accounts_data_size_limit_instruction(limit)
127
+
128
+ if existing_idx.nil?
129
+ append_instructions(message, [new_ix])
130
+ elsif loaded_accounts_data_size_limit_from_instruction_data(T.must(message.instructions[existing_idx].data)) == limit
131
+ message
132
+ else
133
+ new_instructions = message.instructions.dup
134
+ new_instructions[T.must(existing_idx)] = new_ix
135
+ TransactionMessage.new(
136
+ version: message.version,
137
+ instructions: new_instructions,
138
+ fee_payer: message.fee_payer,
139
+ lifetime_constraint: message.lifetime_constraint,
140
+ address_table_lookups: message.address_table_lookups
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
@@ -3,3 +3,4 @@
3
3
 
4
4
  # Transaction message construction utilities — mirrors @solana/transaction-messages.
5
5
  require_relative 'transaction_messages/transaction_message'
6
+ require_relative 'transaction_messages/compute_budget'
@@ -4,7 +4,7 @@
4
4
  module Solana
5
5
  module Ruby
6
6
  module Kit
7
- VERSION = '0.1.7'
7
+ VERSION = '0.1.9'
8
8
  end
9
9
  end
10
10
  end
@@ -50,6 +50,7 @@ require_relative 'kit/programs'
50
50
  require_relative 'kit/sysvars'
51
51
  require_relative 'kit/transaction_confirmation'
52
52
  require_relative 'kit/instruction_plans'
53
+ require_relative 'kit/resource_limit_estimation'
53
54
 
54
55
  # Solana::Ruby::Kit is a Ruby translation of @anza-xyz/kit — the JavaScript SDK for
55
56
  # building Solana apps — into idiomatic Ruby with Sorbet static types.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solana-ruby-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Zupan, Idhra Inc.
@@ -235,16 +235,21 @@ files:
235
235
  - lib/solana/ruby/kit/programs/system_program.rb
236
236
  - lib/solana/ruby/kit/promises.rb
237
237
  - lib/solana/ruby/kit/railtie.rb
238
+ - lib/solana/ruby/kit/resource_limit_estimation.rb
238
239
  - lib/solana/ruby/kit/rpc.rb
239
240
  - lib/solana/ruby/kit/rpc/api/get_account_info.rb
240
241
  - lib/solana/ruby/kit/rpc/api/get_balance.rb
241
242
  - lib/solana/ruby/kit/rpc/api/get_block_height.rb
243
+ - lib/solana/ruby/kit/rpc/api/get_block_time.rb
242
244
  - lib/solana/ruby/kit/rpc/api/get_epoch_info.rb
245
+ - lib/solana/ruby/kit/rpc/api/get_epoch_schedule.rb
246
+ - lib/solana/ruby/kit/rpc/api/get_inflation_reward.rb
243
247
  - lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb
244
248
  - lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb
245
249
  - lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb
246
250
  - lib/solana/ruby/kit/rpc/api/get_program_accounts.rb
247
251
  - lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb
252
+ - lib/solana/ruby/kit/rpc/api/get_signatures_for_address.rb
248
253
  - lib/solana/ruby/kit/rpc/api/get_slot.rb
249
254
  - lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb
250
255
  - lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb
@@ -278,11 +283,14 @@ files:
278
283
  - lib/solana/ruby/kit/rpc_types/cluster_url.rb
279
284
  - lib/solana/ruby/kit/rpc_types/commitment.rb
280
285
  - lib/solana/ruby/kit/rpc_types/lamports.rb
286
+ - lib/solana/ruby/kit/rpc_types/sol.rb
281
287
  - lib/solana/ruby/kit/signers.rb
282
288
  - lib/solana/ruby/kit/signers/keypair_signer.rb
283
289
  - lib/solana/ruby/kit/subscribable.rb
284
290
  - lib/solana/ruby/kit/subscribable/async_iterable.rb
285
291
  - lib/solana/ruby/kit/subscribable/data_publisher.rb
292
+ - lib/solana/ruby/kit/subscribable/reactive_action_store.rb
293
+ - lib/solana/ruby/kit/subscribable/reactive_stream_store.rb
286
294
  - lib/solana/ruby/kit/sysvars.rb
287
295
  - lib/solana/ruby/kit/sysvars/addresses.rb
288
296
  - lib/solana/ruby/kit/sysvars/clock.rb
@@ -291,6 +299,7 @@ files:
291
299
  - lib/solana/ruby/kit/sysvars/rent.rb
292
300
  - lib/solana/ruby/kit/transaction_confirmation.rb
293
301
  - lib/solana/ruby/kit/transaction_messages.rb
302
+ - lib/solana/ruby/kit/transaction_messages/compute_budget.rb
294
303
  - lib/solana/ruby/kit/transaction_messages/transaction_message.rb
295
304
  - lib/solana/ruby/kit/transactions.rb
296
305
  - lib/solana/ruby/kit/transactions/compiler.rb