solana-ruby-kit 0.1.6 → 0.1.8

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: dddb792ac729b3981104e68f18131191f6775d864e785d25cd47ad5d169e94cc
4
- data.tar.gz: 4fd955c2fb214186034906194a530ad62461ee7d3169b7e0718233de4331d4a8
3
+ metadata.gz: f96f7c22b28fbf9bec4186e3bf92ec52483d532b7e398da89bf143473b267878
4
+ data.tar.gz: 4a2e915aba2620726be4b5c89880c49fd7d54acf950618d4002ce19b54ac7884
5
5
  SHA512:
6
- metadata.gz: 9341acdd7f36e55078fa7a21989ec8e29aa00a351e52e3caa05a2f2b14ccee3fbe443f6b35d9914ccb4e53192e215e12659c4c7f83090bc2c027c9ff4fa52f87
7
- data.tar.gz: 0dc330cd1fc6f3177d283eb6b251e0ad187c92f482c328aae1c72a9881bf338206ff2a9fadf7ab3c0a9f4744ce798244bac7368cb8517beee3cb89252abcea75
6
+ metadata.gz: 8a7fef4cd3632770e3e7d67f5b4722585f66f2ad45c601687d6664fc38433a85d39de7f4d582062e6396be7df29ea3cf089ca77184a7d8c28c3cf2b3d37015ae
7
+ data.tar.gz: '0967fe5ea552384a656b22f5f03d08674e4066c373e0ee83cc31bd45598573aacade37f4295a0c24341eb0e31b7d543720be99cf347aba0ab626429f5c629085'
@@ -116,6 +116,16 @@ module Solana::Ruby::Kit
116
116
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN
117
117
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN
118
118
 
119
+ # ── Wallet Standard ───────────────────────────────────────────────────────
120
+ WALLET_STANDARD__INVALID_WIRE_FORMAT = :SOLANA_ERROR__WALLET_STANDARD__INVALID_WIRE_FORMAT
121
+ WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED = :SOLANA_ERROR__WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED
122
+
123
+ # ── Subscribable ──────────────────────────────────────────────────────────
124
+ SUBSCRIBABLE__RETRY_NOT_SUPPORTED = :SOLANA_ERROR__SUBSCRIBABLE__RETRY_NOT_SUPPORTED
125
+
126
+ # ── Fixed-points ──────────────────────────────────────────────────────────
127
+ FIXED_POINTS__STRICT_MODE_PRECISION_LOSS = :SOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS
128
+
119
129
  # ── Invariant violations (internal) ──────────────────────────────────────
120
130
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING
121
131
  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
@@ -227,6 +237,16 @@ module Solana::Ruby::Kit
227
237
  INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN => 'Instruction plan is empty and produced no transaction messages',
228
238
  INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
229
239
 
240
+ # Subscribable
241
+ SUBSCRIBABLE__RETRY_NOT_SUPPORTED => 'This reactive store does not support retry(); use create_reactive_store_from_data_publisher_factory for a retryable store',
242
+
243
+ # Fixed-points
244
+ FIXED_POINTS__STRICT_MODE_PRECISION_LOSS => 'Value has more than 9 fractional digits and cannot be represented exactly as a Sol amount',
245
+
246
+ # Wallet Standard
247
+ WALLET_STANDARD__INVALID_WIRE_FORMAT => 'Invalid transaction wire format: %{reason}',
248
+ WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED => 'Signature verification failed for signer %{address}',
249
+
230
250
  # Invariant violations
231
251
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING => 'Subscription iterator state is missing (internal error)',
232
252
  INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE => 'Subscription iterator must not poll before resolving existing message (internal error)',
@@ -23,7 +23,7 @@ module Solana::Ruby::Kit
23
23
 
24
24
  # The Stake Program address.
25
25
  PROGRAM_ID = T.let(
26
- Addresses::Address.new('Stake11111111111111111111111111111111111111111'),
26
+ Addresses::Address.new('Stake11111111111111111111111111111111111111'),
27
27
  Addresses::Address
28
28
  )
29
29
 
@@ -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
@@ -20,6 +20,9 @@ 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'
23
26
 
24
27
  module Solana::Ruby::Kit
25
28
  module Rpc
@@ -56,6 +59,9 @@ module Solana::Ruby::Kit
56
59
  include Api::GetEpochInfo
57
60
  include Api::GetVoteAccounts
58
61
  include Api::SimulateTransaction
62
+ include Api::GetBlockTime
63
+ include Api::GetEpochSchedule
64
+ include Api::GetInflationReward
59
65
 
60
66
  sig { returns(Transport) }
61
67
  attr_reader :transport
@@ -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
@@ -4,7 +4,7 @@
4
4
  module Solana
5
5
  module Ruby
6
6
  module Kit
7
- VERSION = '0.1.6'
7
+ VERSION = '0.1.8'
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,254 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'base64'
5
+ require 'rbnacl'
6
+ require_relative 'addresses/address'
7
+ require_relative 'keys/signatures'
8
+ require_relative 'transactions/transaction'
9
+ require_relative 'errors'
10
+
11
+ module Solana::Ruby::Kit
12
+ # Server-side Ruby port of the @solana/wallet-standard signTransaction interface.
13
+ #
14
+ # When a browser wallet (Phantom, Backpack, Solflare, …) calls
15
+ # wallet.features['solana:signTransaction'].signTransaction(transaction)
16
+ # the signed transaction is returned as serialised wire bytes. Rails receives
17
+ # those bytes (typically base64-encoded in a JSON request body) and uses this
18
+ # module to:
19
+ #
20
+ # 1. Decode the wire bytes back into a +Transactions::Transaction+ struct.
21
+ # 2. Verify every present Ed25519 signature against the message bytes.
22
+ # 3. Confirm that a specific wallet address (or all required addresses) signed.
23
+ # 4. Optionally broadcast the verified transaction via +Rpc+.
24
+ #
25
+ # Solana addresses ARE Ed25519 public keys (32-byte base58), so signature
26
+ # verification requires no external key lookup — the verify key is derived
27
+ # directly from the signer's address string.
28
+ #
29
+ # Typical Rails controller usage:
30
+ #
31
+ # wire_bytes = Base64.strict_decode64(params[:signed_transaction])
32
+ # tx = Solana::Ruby::Kit::WalletStandard.verify_signed_transaction!(wire_bytes)
33
+ # Solana::Ruby::Kit::Transactions.assert_fully_signed_transaction!(tx)
34
+ # # tx is now safe to inspect or forward to the RPC node
35
+ #
36
+ # Mirrors @solana/wallet-standard (anza-xyz/kit) and @wallet-standard/features.
37
+ module WalletStandard
38
+ extend T::Sig
39
+
40
+ # ── Wallet Standard feature identifiers ──────────────────────────────────
41
+ # String constants that identify which signing capabilities a browser wallet
42
+ # supports. Client-side JavaScript reads wallet.features to detect these;
43
+ # the constants are documented here so Rails code can reference them by name
44
+ # when building feature-negotiation metadata for the frontend.
45
+
46
+ # The wallet can sign transactions without broadcasting them.
47
+ SIGN_TRANSACTION = T.let('solana:signTransaction', String)
48
+ # The wallet can sign and broadcast in a single round-trip.
49
+ SIGN_AND_SEND_TRANSACTION = T.let('solana:signAndSendTransaction', String)
50
+ # The wallet can sign arbitrary off-chain messages.
51
+ SIGN_MESSAGE = T.let('solana:signMessage', String)
52
+ # Base Wallet Standard lifecycle features.
53
+ CONNECT = T.let('standard:connect', String)
54
+ DISCONNECT = T.let('standard:disconnect', String)
55
+ EVENTS = T.let('standard:events', String)
56
+
57
+ module_function
58
+
59
+ # ── Wire-format decoder ───────────────────────────────────────────────────
60
+
61
+ # Decodes a Solana wire-encoded transaction into a +Transactions::Transaction+.
62
+ #
63
+ # Accepts the binary output of a wallet's +signTransaction+ call. Binary
64
+ # (ASCII_8BIT) strings are used as-is; any other encoding is treated as
65
+ # standard base64 and decoded automatically.
66
+ #
67
+ # Solana wire format (legacy; v0 versioned transactions add a one-byte
68
+ # version prefix to the message section):
69
+ #
70
+ # [compact-u16] num_signatures
71
+ # [64 bytes × n] signature slots (all-zero slot means unfilled)
72
+ # [message bytes …]
73
+ #
74
+ # The returned Transaction carries:
75
+ # +message_bytes+ — raw bytes that were (or will be) signed
76
+ # +signatures+ — ordered Hash of signer_address → raw_sig_bytes or nil
77
+ #
78
+ # Raises +SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT+ for malformed input.
79
+ #
80
+ # Mirrors +getTransactionDecoder()+ from @solana/transactions.
81
+ sig { params(wire_bytes: String).returns(Transactions::Transaction) }
82
+ def decode_wire_transaction(wire_bytes)
83
+ bytes = coerce_to_binary(wire_bytes)
84
+ offset = 0
85
+
86
+ # 1. Signature count (compact-u16)
87
+ sig_count, offset = decode_compact_u16(bytes, offset)
88
+ if sig_count > 19
89
+ Kernel.raise SolanaError.new(
90
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
91
+ reason: "signature count #{sig_count} exceeds the maximum of 19"
92
+ )
93
+ end
94
+
95
+ # 2. Signature slots — 64 bytes each; all-zero means the slot is unfilled.
96
+ zero64 = ("\x00" * 64).b
97
+ raw_sigs = T.let([], T::Array[T.nilable(String)])
98
+ sig_count.times do
99
+ slot = bytes[offset, 64]
100
+ if slot.nil? || slot.bytesize != 64
101
+ Kernel.raise SolanaError.new(
102
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
103
+ reason: 'truncated signature slot'
104
+ )
105
+ end
106
+ raw_sigs << (slot.b == zero64 ? nil : slot.b)
107
+ offset += 64
108
+ end
109
+
110
+ # 3. Everything after the signatures section is the message.
111
+ msg_slice = bytes[offset..]
112
+ if msg_slice.nil? || msg_slice.empty?
113
+ Kernel.raise SolanaError.new(
114
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
115
+ reason: 'message section is missing'
116
+ )
117
+ end
118
+ message_bytes = T.must(msg_slice).b
119
+
120
+ # 4. Determine version and locate the message header.
121
+ # Legacy transactions: first byte = num_required_signatures (0..127)
122
+ # Versioned (v0) trans.: first byte = 0x80 | version (≥ 128); skip it.
123
+ msg_pos = 0
124
+ first_byte = message_bytes.getbyte(msg_pos).to_i
125
+ msg_pos += 1 if (first_byte & 0x80) != 0
126
+
127
+ num_required_sigs = message_bytes.getbyte(msg_pos).to_i
128
+ msg_pos += 1
129
+ msg_pos += 2 # skip num_readonly_signed + num_readonly_unsigned
130
+
131
+ # 5. Parse the account list to recover signer addresses.
132
+ num_accounts, msg_pos = decode_compact_u16(message_bytes, msg_pos)
133
+ if msg_pos + (num_accounts * 32) > message_bytes.bytesize
134
+ Kernel.raise SolanaError.new(
135
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
136
+ reason: 'account table truncated'
137
+ )
138
+ end
139
+
140
+ account_addrs = T.let([], T::Array[String])
141
+ num_accounts.times do
142
+ account_addrs << Addresses.encode_address(message_bytes[msg_pos, 32].b)
143
+ msg_pos += 32
144
+ end
145
+
146
+ # 6. Build the signatures map.
147
+ # The first +num_required_sigs+ accounts in the account table are the
148
+ # signers; their raw_sigs slots are positionally aligned.
149
+ signer_addrs = account_addrs[0, num_required_sigs]
150
+ signatures = T.let({}, T::Hash[String, T.nilable(String)])
151
+ signer_addrs.each_with_index { |addr, i| signatures[addr] = raw_sigs[i] }
152
+
153
+ Transactions::Transaction.new(message_bytes: message_bytes, signatures: signatures)
154
+ end
155
+
156
+ # ── Signature verification ────────────────────────────────────────────────
157
+
158
+ # Verifies every filled (non-nil) signature in +transaction.signatures+.
159
+ #
160
+ # For each signed slot the signer's Ed25519 public key is reconstructed
161
+ # directly from their address (Solana addresses are base58-encoded 32-byte
162
+ # Ed25519 public keys), so no external key lookup is required.
163
+ #
164
+ # Raises +SolanaError::WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED+ if
165
+ # any signature does not verify. Nil (unfilled) slots are skipped silently;
166
+ # use +Transactions.assert_fully_signed_transaction!+ to enforce that all
167
+ # required signers have signed.
168
+ sig { params(transaction: Transactions::Transaction).void }
169
+ def verify_transaction_signatures!(transaction)
170
+ transaction.signatures.each do |addr_str, sig_raw|
171
+ next if sig_raw.nil?
172
+
173
+ verify_key = RbNaCl::VerifyKey.new(Addresses.decode_address(Addresses::Address.new(addr_str)))
174
+ sig_bytes = Keys::SignatureBytes.new(sig_raw)
175
+
176
+ unless Keys.verify_signature(verify_key, sig_bytes, transaction.message_bytes)
177
+ Kernel.raise SolanaError.new(
178
+ SolanaError::WALLET_STANDARD__SIGNATURE_VERIFICATION_FAILED,
179
+ address: addr_str
180
+ )
181
+ end
182
+ end
183
+ end
184
+
185
+ # Convenience method for Rails controllers: decode wire bytes received from a
186
+ # browser wallet and verify all present signatures in one call.
187
+ #
188
+ # Returns the decoded +Transaction+ on success.
189
+ # Raises +SolanaError+ for malformed wire bytes or any invalid signature.
190
+ # Does NOT require the transaction to be fully signed; follow up with
191
+ # +Transactions.assert_fully_signed_transaction!+ when broadcasting.
192
+ #
193
+ # Accepts binary or base64-encoded input (see +decode_wire_transaction+).
194
+ sig { params(wire_bytes: String).returns(Transactions::Transaction) }
195
+ def verify_signed_transaction!(wire_bytes)
196
+ tx = decode_wire_transaction(wire_bytes)
197
+ verify_transaction_signatures!(tx)
198
+ tx
199
+ end
200
+
201
+ # Returns +true+ if +address+ has provided a cryptographically valid
202
+ # signature for +transaction+; +false+ if the slot is absent, nil, or the
203
+ # signature does not verify against +transaction.message_bytes+.
204
+ sig { params(transaction: Transactions::Transaction, address: Addresses::Address).returns(T::Boolean) }
205
+ def signed_by?(transaction, address)
206
+ sig_raw = transaction.signatures[address.value]
207
+ return false if sig_raw.nil?
208
+
209
+ verify_key = RbNaCl::VerifyKey.new(Addresses.decode_address(address))
210
+ sig_bytes = Keys::SignatureBytes.new(sig_raw)
211
+ Keys.verify_signature(verify_key, sig_bytes, transaction.message_bytes)
212
+ end
213
+
214
+ # ── Private helpers ───────────────────────────────────────────────────────
215
+
216
+ # Forces +input+ to a binary (ASCII_8BIT) String.
217
+ # ASCII_8BIT input is returned as-is; anything else is treated as base64.
218
+ sig { params(input: String).returns(String) }
219
+ def coerce_to_binary(input)
220
+ return input.b if input.encoding == ::Encoding::ASCII_8BIT
221
+
222
+ stripped = input.strip
223
+ begin
224
+ Base64.strict_decode64(stripped).b
225
+ rescue ArgumentError
226
+ Base64.decode64(stripped).b
227
+ end
228
+ end
229
+ private_class_method :coerce_to_binary
230
+
231
+ # Reads a Solana compact-u16 from +bytes+ starting at +offset+.
232
+ # Returns +[decoded_integer, next_offset]+.
233
+ sig { params(bytes: String, offset: Integer).returns([Integer, Integer]) }
234
+ def decode_compact_u16(bytes, offset)
235
+ value = 0
236
+ shift = 0
237
+ loop do
238
+ byte = bytes.getbyte(offset)
239
+ if byte.nil?
240
+ Kernel.raise SolanaError.new(
241
+ SolanaError::WALLET_STANDARD__INVALID_WIRE_FORMAT,
242
+ reason: 'compact-u16 read past end of buffer'
243
+ )
244
+ end
245
+ offset += 1
246
+ value |= (byte & 0x7f) << shift
247
+ shift += 7
248
+ break unless (byte & 0x80) != 0
249
+ end
250
+ [value, offset]
251
+ end
252
+ private_class_method :decode_compact_u16
253
+ end
254
+ end
@@ -41,6 +41,9 @@ require_relative 'kit/rpc_subscriptions'
41
41
  # ── Plugin system ─────────────────────────────────────────────────────────────
42
42
  require_relative 'kit/plugin_core'
43
43
 
44
+ # ── Wallet Standard (server-side signature verification) ──────────────────────
45
+ require_relative 'kit/wallet_standard'
46
+
44
47
  # ── Higher-level helpers ──────────────────────────────────────────────────────
45
48
  require_relative 'kit/offchain_messages'
46
49
  require_relative 'kit/programs'
@@ -75,6 +78,7 @@ require_relative 'kit/instruction_plans'
75
78
  # Solana::Ruby::Kit::Sysvars — sysvar fetch/decode (@solana/sysvars)
76
79
  # Solana::Ruby::Kit::TransactionConfirmation — confirmation polling (@solana/transaction-confirmation)
77
80
  # Solana::Ruby::Kit::InstructionPlans — multi-tx planning (@solana/instruction-plans)
81
+ # Solana::Ruby::Kit::WalletStandard — server-side sig verify (@solana/wallet-standard)
78
82
 
79
83
  module Solana::Ruby::Kit
80
84
  extend T::Sig
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.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Zupan, Idhra Inc.
@@ -239,7 +239,10 @@ files:
239
239
  - lib/solana/ruby/kit/rpc/api/get_account_info.rb
240
240
  - lib/solana/ruby/kit/rpc/api/get_balance.rb
241
241
  - lib/solana/ruby/kit/rpc/api/get_block_height.rb
242
+ - lib/solana/ruby/kit/rpc/api/get_block_time.rb
242
243
  - lib/solana/ruby/kit/rpc/api/get_epoch_info.rb
244
+ - lib/solana/ruby/kit/rpc/api/get_epoch_schedule.rb
245
+ - lib/solana/ruby/kit/rpc/api/get_inflation_reward.rb
243
246
  - lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb
244
247
  - lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb
245
248
  - lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb
@@ -278,11 +281,14 @@ files:
278
281
  - lib/solana/ruby/kit/rpc_types/cluster_url.rb
279
282
  - lib/solana/ruby/kit/rpc_types/commitment.rb
280
283
  - lib/solana/ruby/kit/rpc_types/lamports.rb
284
+ - lib/solana/ruby/kit/rpc_types/sol.rb
281
285
  - lib/solana/ruby/kit/signers.rb
282
286
  - lib/solana/ruby/kit/signers/keypair_signer.rb
283
287
  - lib/solana/ruby/kit/subscribable.rb
284
288
  - lib/solana/ruby/kit/subscribable/async_iterable.rb
285
289
  - lib/solana/ruby/kit/subscribable/data_publisher.rb
290
+ - lib/solana/ruby/kit/subscribable/reactive_action_store.rb
291
+ - lib/solana/ruby/kit/subscribable/reactive_stream_store.rb
286
292
  - lib/solana/ruby/kit/sysvars.rb
287
293
  - lib/solana/ruby/kit/sysvars/addresses.rb
288
294
  - lib/solana/ruby/kit/sysvars/clock.rb
@@ -296,6 +302,7 @@ files:
296
302
  - lib/solana/ruby/kit/transactions/compiler.rb
297
303
  - lib/solana/ruby/kit/transactions/transaction.rb
298
304
  - lib/solana/ruby/kit/version.rb
305
+ - lib/solana/ruby/kit/wallet_standard.rb
299
306
  - solana-ruby-kit.gemspec
300
307
  licenses:
301
308
  - MIT