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 +4 -4
- data/lib/solana/ruby/kit/errors.rb +25 -4
- data/lib/solana/ruby/kit/resource_limit_estimation.rb +156 -0
- data/lib/solana/ruby/kit/rpc/api/get_block_time.rb +20 -0
- data/lib/solana/ruby/kit/rpc/api/get_epoch_schedule.rb +40 -0
- data/lib/solana/ruby/kit/rpc/api/get_inflation_reward.rb +59 -0
- data/lib/solana/ruby/kit/rpc/api/get_signatures_for_address.rb +72 -0
- data/lib/solana/ruby/kit/rpc/client.rb +8 -0
- data/lib/solana/ruby/kit/rpc/transport.rb +3 -4
- data/lib/solana/ruby/kit/rpc_types/sol.rb +102 -0
- data/lib/solana/ruby/kit/rpc_types.rb +1 -0
- data/lib/solana/ruby/kit/subscribable/reactive_action_store.rb +207 -0
- data/lib/solana/ruby/kit/subscribable/reactive_stream_store.rb +255 -0
- data/lib/solana/ruby/kit/subscribable.rb +2 -0
- data/lib/solana/ruby/kit/transaction_messages/compute_budget.rb +145 -0
- data/lib/solana/ruby/kit/transaction_messages.rb +1 -0
- data/lib/solana/ruby/kit/version.rb +1 -1
- data/lib/solana/ruby/kit.rb +1 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d26df33c1471059cde38ac46ec2604e0c26ffc8d64c580de4171396516530fd
|
|
4
|
+
data.tar.gz: dba1b05e818b10e8fa7692f6ae4af89c3946bcd19707be52cfdfd1c503e81b63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 (-
|
|
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']
|
|
121
|
-
req['Accept']
|
|
122
|
-
req['
|
|
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
|
|
@@ -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
|
data/lib/solana/ruby/kit.rb
CHANGED
|
@@ -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.
|
|
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
|