solana-ruby-kit 7.0.0 → 7.0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/solana/ruby/kit/errors.rb +2 -0
- data/lib/solana/ruby/kit/rpc/api/get_inflation_reward.rb +4 -2
- data/lib/solana/ruby/kit/rpc/api/get_transactions_for_address.rb +208 -0
- data/lib/solana/ruby/kit/rpc/client.rb +2 -0
- data/lib/solana/ruby/kit/subscribable/bridge_store_to_async_iterable.rb +172 -0
- data/lib/solana/ruby/kit/subscribable.rb +1 -0
- data/lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb +22 -5
- data/lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb +5 -3
- data/lib/solana/ruby/kit/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 950ad695ca0d9971314172207d974e747cf35c5dfec1210fb32882f0517306f8
|
|
4
|
+
data.tar.gz: ce0c4c171c78ca202a35bbacdd74630bb24afd3a438c7ebe7669b5cb56ee9a71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d356ad614cfe6990a0df08121f6db68dcdf4e72b19979bd01a56233a1fccc699a16ee716c2e12d33b4de5c34792cc627fd0e273bfefce73a7144f70fff37ddf
|
|
7
|
+
data.tar.gz: b866ff957a09ba7dc4ab26708ea1311b2c8a283e992f972351c173187a47d905b6ba62483beefd0f5b2c9d46f9ecdd8574bba1f4491e96771abdc9c75417f7ee
|
|
@@ -139,6 +139,7 @@ module Solana::Ruby::Kit
|
|
|
139
139
|
|
|
140
140
|
# ── Subscribable ──────────────────────────────────────────────────────────
|
|
141
141
|
SUBSCRIBABLE__RETRY_NOT_SUPPORTED = :SOLANA_ERROR__SUBSCRIBABLE__RETRY_NOT_SUPPORTED
|
|
142
|
+
SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR = :SOLANA_ERROR__SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR
|
|
142
143
|
|
|
143
144
|
# ── Fixed-points ──────────────────────────────────────────────────────────
|
|
144
145
|
FIXED_POINTS__STRICT_MODE_PRECISION_LOSS = :SOLANA_ERROR__FIXED_POINTS__STRICT_MODE_PRECISION_LOSS
|
|
@@ -271,6 +272,7 @@ module Solana::Ruby::Kit
|
|
|
271
272
|
|
|
272
273
|
# Subscribable
|
|
273
274
|
SUBSCRIBABLE__RETRY_NOT_SUPPORTED => 'This reactive store does not support retry(); use create_reactive_store_from_data_publisher_factory for a retryable store',
|
|
275
|
+
SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR => 'The stream store closed in an error state but did not report an error.',
|
|
274
276
|
|
|
275
277
|
# Fixed-points
|
|
276
278
|
FIXED_POINTS__STRICT_MODE_PRECISION_LOSS => 'Value has more than 9 fractional digits and cannot be represented exactly as a Sol amount',
|
|
@@ -9,7 +9,9 @@ module Solana::Ruby::Kit
|
|
|
9
9
|
InflationReward = T.let(
|
|
10
10
|
Struct.new(
|
|
11
11
|
:amount, # Integer (Lamports) — reward credited
|
|
12
|
-
:commission, # Integer — vote account commission at reward time
|
|
12
|
+
:commission, # Integer, nilable — vote account commission at reward time;
|
|
13
|
+
# null once the vote account reports commission through
|
|
14
|
+
# getVoteAccounts' inflationRewardsCommissionBps instead
|
|
13
15
|
:effective_slot, # Integer — slot in which rewards were delivered
|
|
14
16
|
:epoch, # Integer — epoch for which reward occurred
|
|
15
17
|
:post_balance, # Integer (Lamports) — post-reward account balance
|
|
@@ -46,7 +48,7 @@ module Solana::Ruby::Kit
|
|
|
46
48
|
|
|
47
49
|
InflationReward.new(
|
|
48
50
|
amount: Kernel.Integer(entry['amount']),
|
|
49
|
-
commission: Kernel.Integer(entry['commission']),
|
|
51
|
+
commission: entry['commission'] ? Kernel.Integer(entry['commission']) : nil,
|
|
50
52
|
effective_slot: Kernel.Integer(entry['effectiveSlot']),
|
|
51
53
|
epoch: Kernel.Integer(entry['epoch']),
|
|
52
54
|
post_balance: Kernel.Integer(entry['postBalance'])
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Rpc
|
|
6
|
+
module Api
|
|
7
|
+
# One signature-level entry returned by getTransactionsForAddress when
|
|
8
|
+
# `transaction_details` is `:signatures`.
|
|
9
|
+
# Mirrors TypeScript's GetTransactionsForAddressSignature.
|
|
10
|
+
TransactionsForAddressSignature = T.let(
|
|
11
|
+
Struct.new(
|
|
12
|
+
:block_time, # Integer | nil — Unix timestamp of block production, nil if unavailable
|
|
13
|
+
:confirmation_status, # Symbol | nil — :processed, :confirmed, or :finalized
|
|
14
|
+
:err, # Hash | nil — transaction error, nil if succeeded
|
|
15
|
+
:memo, # String | nil — memo associated with the transaction
|
|
16
|
+
:signature, # String — base-58 encoded transaction signature
|
|
17
|
+
:slot, # Integer — slot containing the transaction
|
|
18
|
+
:transaction_index, # Integer — 0-based index within the block
|
|
19
|
+
keyword_init: true
|
|
20
|
+
),
|
|
21
|
+
T.untyped
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# The result envelope shared by every mode of getTransactionsForAddress.
|
|
25
|
+
#
|
|
26
|
+
# `data` holds the matching transactions in the requested sort order. Its
|
|
27
|
+
# element type depends on `transaction_details`: TransactionsForAddressSignature
|
|
28
|
+
# structs for `:signatures`, and raw (String-keyed) response hashes for
|
|
29
|
+
# `:full` — the same shape `Rpc::Api::GetTransaction#get_transaction`
|
|
30
|
+
# returns, so each element can be handed straight to
|
|
31
|
+
# `TransactionIntrospection.decode_transaction_from_rpc_response`.
|
|
32
|
+
#
|
|
33
|
+
# `pagination_token` is the cursor to pass back as `pagination_token` on a
|
|
34
|
+
# subsequent call, or nil when there are no more results.
|
|
35
|
+
#
|
|
36
|
+
# Mirrors TypeScript's GetTransactionsForAddressApiResponse.
|
|
37
|
+
TransactionsForAddressPage = T.let(
|
|
38
|
+
Struct.new(
|
|
39
|
+
:data, # Array<TransactionsForAddressSignature> | Array<Hash>
|
|
40
|
+
:pagination_token, # String | nil — cursor for the next page
|
|
41
|
+
keyword_init: true
|
|
42
|
+
),
|
|
43
|
+
T.untyped
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Returns confirmed transactions that load the given address, with
|
|
47
|
+
# server-side filtering, sorting, and cursor-based pagination.
|
|
48
|
+
#
|
|
49
|
+
# This combines the discovery step of getSignaturesForAddress with
|
|
50
|
+
# filtering and pagination the server applies itself. Note: check your RPC
|
|
51
|
+
# provider for support for this method.
|
|
52
|
+
#
|
|
53
|
+
# Mirrors TypeScript's GetTransactionsForAddressApi.getTransactionsForAddress.
|
|
54
|
+
# See https://solana.com/docs/rpc/http/gettransactionsforaddress
|
|
55
|
+
module GetTransactionsForAddress
|
|
56
|
+
extend T::Sig
|
|
57
|
+
|
|
58
|
+
# Maps the snake_case keys of a Ruby +filters+ hash onto the wire names.
|
|
59
|
+
# Comparison operators (:eq, :gt, :gte, :lt, :lte) pass through as-is.
|
|
60
|
+
FILTER_KEYS = T.let(
|
|
61
|
+
{
|
|
62
|
+
block_time: 'blockTime',
|
|
63
|
+
signature: 'signature',
|
|
64
|
+
slot: 'slot',
|
|
65
|
+
status: 'status',
|
|
66
|
+
token_accounts: 'tokenAccounts'
|
|
67
|
+
}.freeze,
|
|
68
|
+
T::Hash[Symbol, String]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
COMPARISON_KEYS = T.let(%i[eq gt gte lt lte].freeze, T::Array[Symbol])
|
|
72
|
+
|
|
73
|
+
# @param address [String] base-58 address to search for.
|
|
74
|
+
# @param transaction_details [Symbol] :signatures (default) or :full.
|
|
75
|
+
# @param encoding [String, nil] 'base58', 'base64', 'json' (server default),
|
|
76
|
+
# or 'jsonParsed'. Only valid with `transaction_details: :full`.
|
|
77
|
+
# @param max_supported_transaction_version [Integer, nil] newest transaction
|
|
78
|
+
# version to accept. Omit to receive only legacy transactions (in which
|
|
79
|
+
# case no `version` is returned). Only valid with `transaction_details: :full`.
|
|
80
|
+
# @param commitment [Symbol, nil] :confirmed or :finalized. This method does
|
|
81
|
+
# not support :processed.
|
|
82
|
+
# @param filters [Hash, nil] server-side filters, combined with AND. Supports
|
|
83
|
+
# `block_time:` ({eq/gt/gte/lt/lte}), `signature:` ({gt/gte/lt/lte}),
|
|
84
|
+
# `slot:` ({gt/gte/lt/lte}), `status:` (:any, :succeeded, :failed), and
|
|
85
|
+
# `token_accounts:` (:none, :balance_changed, :all).
|
|
86
|
+
# @param limit [Integer, nil] max results. The server caps this at 1000 for
|
|
87
|
+
# `:signatures` and 100 for `:full`.
|
|
88
|
+
# @param min_context_slot [Integer, nil] reject data older than this slot.
|
|
89
|
+
# @param pagination_token [String, nil] cursor from a previous response's
|
|
90
|
+
# `pagination_token`, formatted "<slot>:<position>".
|
|
91
|
+
# @param sort_order [Symbol, nil] :desc (newest first, server default) or :asc.
|
|
92
|
+
# @return [TransactionsForAddressPage]
|
|
93
|
+
sig do
|
|
94
|
+
params(
|
|
95
|
+
address: String,
|
|
96
|
+
transaction_details: Symbol,
|
|
97
|
+
encoding: T.nilable(String),
|
|
98
|
+
max_supported_transaction_version: T.nilable(Integer),
|
|
99
|
+
commitment: T.nilable(Symbol),
|
|
100
|
+
filters: T.nilable(T::Hash[Symbol, T.untyped]),
|
|
101
|
+
limit: T.nilable(Integer),
|
|
102
|
+
min_context_slot: T.nilable(Integer),
|
|
103
|
+
pagination_token: T.nilable(String),
|
|
104
|
+
sort_order: T.nilable(Symbol)
|
|
105
|
+
).returns(T.untyped)
|
|
106
|
+
end
|
|
107
|
+
def get_transactions_for_address(
|
|
108
|
+
address,
|
|
109
|
+
transaction_details: :signatures,
|
|
110
|
+
encoding: nil,
|
|
111
|
+
max_supported_transaction_version: nil,
|
|
112
|
+
commitment: nil,
|
|
113
|
+
filters: nil,
|
|
114
|
+
limit: nil,
|
|
115
|
+
min_context_slot: nil,
|
|
116
|
+
pagination_token: nil,
|
|
117
|
+
sort_order: nil
|
|
118
|
+
)
|
|
119
|
+
unless %i[signatures full].include?(transaction_details)
|
|
120
|
+
Kernel.raise ArgumentError, "transaction_details must be :signatures or :full, got #{transaction_details.inspect}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The RPC rejects `processed` for this method; TypeScript encodes that
|
|
124
|
+
# as `Exclude<Commitment, 'processed'>`, so fail fast here too.
|
|
125
|
+
if commitment == :processed
|
|
126
|
+
Kernel.raise ArgumentError, 'getTransactionsForAddress does not support the :processed commitment'
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if transaction_details == :signatures
|
|
130
|
+
if encoding
|
|
131
|
+
Kernel.raise ArgumentError, 'encoding is only valid with transaction_details: :full'
|
|
132
|
+
end
|
|
133
|
+
if max_supported_transaction_version
|
|
134
|
+
Kernel.raise ArgumentError, 'max_supported_transaction_version is only valid with transaction_details: :full'
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
config = { 'transactionDetails' => transaction_details.to_s }
|
|
139
|
+
config['encoding'] = encoding if encoding
|
|
140
|
+
config['maxSupportedTransactionVersion'] = max_supported_transaction_version if max_supported_transaction_version
|
|
141
|
+
config['commitment'] = commitment.to_s if commitment
|
|
142
|
+
config['limit'] = limit if limit
|
|
143
|
+
config['minContextSlot'] = min_context_slot if min_context_slot
|
|
144
|
+
config['paginationToken'] = pagination_token if pagination_token
|
|
145
|
+
config['sortOrder'] = sort_order.to_s if sort_order
|
|
146
|
+
config['filters'] = build_filters(filters) if filters && !filters.empty?
|
|
147
|
+
|
|
148
|
+
raw = transport.request('getTransactionsForAddress', [address, config])
|
|
149
|
+
rows = raw['data'] || []
|
|
150
|
+
|
|
151
|
+
data = if transaction_details == :full
|
|
152
|
+
# Returned verbatim so each element can be fed to
|
|
153
|
+
# TransactionIntrospection.decode_transaction_from_rpc_response.
|
|
154
|
+
rows
|
|
155
|
+
else
|
|
156
|
+
rows.map do |tx|
|
|
157
|
+
TransactionsForAddressSignature.new(
|
|
158
|
+
block_time: tx['blockTime'],
|
|
159
|
+
confirmation_status: tx['confirmationStatus']&.to_sym,
|
|
160
|
+
err: tx['err'],
|
|
161
|
+
memo: tx['memo'],
|
|
162
|
+
signature: tx['signature'],
|
|
163
|
+
slot: Kernel.Integer(tx['slot']),
|
|
164
|
+
transaction_index: tx['transactionIndex'] && Kernel.Integer(tx['transactionIndex'])
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
TransactionsForAddressPage.new(data: data, pagination_token: raw['paginationToken'])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# ── Private helpers ────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
# Converts the snake_case +filters+ hash into its wire form. Symbol values
|
|
175
|
+
# (`status`, `token_accounts`) are lowerCamelCased so `:balance_changed`
|
|
176
|
+
# reaches the server as `"balanceChanged"`.
|
|
177
|
+
sig { params(filters: T::Hash[Symbol, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
|
178
|
+
def build_filters(filters)
|
|
179
|
+
filters.each_with_object({}) do |(key, value), out|
|
|
180
|
+
wire_key = FILTER_KEYS[key]
|
|
181
|
+
Kernel.raise ArgumentError, "unknown filter #{key.inspect}" if wire_key.nil?
|
|
182
|
+
|
|
183
|
+
out[wire_key] = if value.is_a?(Hash)
|
|
184
|
+
value.each_with_object({}) do |(op, operand), cmp|
|
|
185
|
+
unless COMPARISON_KEYS.include?(op)
|
|
186
|
+
Kernel.raise ArgumentError, "unknown comparison #{op.inspect} in filter #{key.inspect}"
|
|
187
|
+
end
|
|
188
|
+
cmp[op.to_s] = operand
|
|
189
|
+
end
|
|
190
|
+
elsif value.is_a?(Symbol)
|
|
191
|
+
camelize(value)
|
|
192
|
+
else
|
|
193
|
+
value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
private :build_filters
|
|
198
|
+
|
|
199
|
+
sig { params(value: Symbol).returns(String) }
|
|
200
|
+
def camelize(value)
|
|
201
|
+
head, *rest = value.to_s.split('_')
|
|
202
|
+
([head] + rest.map(&:capitalize)).join
|
|
203
|
+
end
|
|
204
|
+
private :camelize
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -24,6 +24,7 @@ require_relative 'api/get_block_time'
|
|
|
24
24
|
require_relative 'api/get_epoch_schedule'
|
|
25
25
|
require_relative 'api/get_inflation_reward'
|
|
26
26
|
require_relative 'api/get_signatures_for_address'
|
|
27
|
+
require_relative 'api/get_transactions_for_address'
|
|
27
28
|
|
|
28
29
|
module Solana::Ruby::Kit
|
|
29
30
|
module Rpc
|
|
@@ -64,6 +65,7 @@ module Solana::Ruby::Kit
|
|
|
64
65
|
include Api::GetEpochSchedule
|
|
65
66
|
include Api::GetInflationReward
|
|
66
67
|
include Api::GetSignaturesForAddress
|
|
68
|
+
include Api::GetTransactionsForAddress
|
|
67
69
|
|
|
68
70
|
sig { returns(Transport) }
|
|
69
71
|
attr_reader :transport
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative 'reactive_stream_store'
|
|
6
|
+
|
|
7
|
+
module Solana::Ruby::Kit
|
|
8
|
+
module Subscribable
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Adapts a ReactiveStreamStore into an Enumerator, so a *push*-based
|
|
14
|
+
# reactive store can be driven by *pull*-based code that consumes it with
|
|
15
|
+
# `#each`, `#next`, `#take`, or any other Enumerable method.
|
|
16
|
+
#
|
|
17
|
+
# The bridge only *observes* the store; it does not open or tear down the
|
|
18
|
+
# connection. The caller owns the store's lifecycle. The bridge subscribes,
|
|
19
|
+
# yields the store's current and subsequent values, and unsubscribes when
|
|
20
|
+
# iteration ends — however it ends (exhaustion, error, `break`, or abort).
|
|
21
|
+
# It does not reset the store; that is the caller's decision.
|
|
22
|
+
#
|
|
23
|
+
# This is the store-backed counterpart to AsyncIterable.from_publisher.
|
|
24
|
+
# That helper turns a raw DataPublisher into an Enumerator and queues every
|
|
25
|
+
# message so none are dropped. `bridge_store_to_async_iterable` instead sits
|
|
26
|
+
# on top of a ReactiveStreamStore, so it reflects the store's
|
|
27
|
+
# loading/loaded/error/retrying lifecycle — and, because a store only ever
|
|
28
|
+
# holds the *latest* snapshot, it is latest-wins rather than fully buffered.
|
|
29
|
+
#
|
|
30
|
+
# On iteration it seeds from the store's current snapshot, then yields its
|
|
31
|
+
# lifecycle:
|
|
32
|
+
# - 'loaded' → yields the value (the one already present when iteration
|
|
33
|
+
# begins, then each subsequent update), unless the optional
|
|
34
|
+
# +should_yield+ predicate rejects it. Latest-wins: if several
|
|
35
|
+
# notifications land between pulls, only the most recent
|
|
36
|
+
# unconsumed value is yielded.
|
|
37
|
+
# - 'error' → raises, so the consuming loop propagates it. Substitutes a
|
|
38
|
+
# SolanaError::SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR
|
|
39
|
+
# sentinel when the store reports an error with a nil payload.
|
|
40
|
+
# An error takes precedence over a buffered value: if a loaded
|
|
41
|
+
# value is still pending when an error arrives, that value is
|
|
42
|
+
# dropped and the error propagates.
|
|
43
|
+
# - 'loading' / 'retrying' → carry no new value and no error; nothing to yield.
|
|
44
|
+
#
|
|
45
|
+
# +signal+ follows the same convention as the rest of Subscribable: a lambda
|
|
46
|
+
# called to test for abort, which *raises* once aborted. When it raises,
|
|
47
|
+
# iteration ends cleanly (no error) — an abort is teardown, so the store's
|
|
48
|
+
# incidental abort-driven error state is not surfaced. Because a Ruby
|
|
49
|
+
# consumer parks in a blocking wait rather than on an event loop, the signal
|
|
50
|
+
# is re-tested every +poll_interval+ seconds while parked; it is checked
|
|
51
|
+
# immediately on every store change regardless. With no +signal+, the
|
|
52
|
+
# enumerator parks indefinitely between values — a subscription never
|
|
53
|
+
# completes on its own, so `break` out of the loop to stop.
|
|
54
|
+
#
|
|
55
|
+
# @param store [ReactiveStreamStore] a store to observe. Connect it yourself.
|
|
56
|
+
# @param signal [Proc, nil] raises once aborted; ends iteration cleanly.
|
|
57
|
+
# @param should_yield [Proc, nil] gate run against each loaded value before
|
|
58
|
+
# it is yielded. Return false to drop the value.
|
|
59
|
+
# @param poll_interval [Float] seconds between signal re-tests while parked.
|
|
60
|
+
# @return [Enumerator]
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# store = Subscribable.create_reactive_store_from_data_publisher(
|
|
64
|
+
# publisher, data_channel: :slotNotification, error_channel: :error
|
|
65
|
+
# )
|
|
66
|
+
# Subscribable.bridge_store_to_async_iterable(store).each do |notification|
|
|
67
|
+
# puts "Latest slot: #{notification['slot']}"
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# Mirrors bridgeStoreToAsyncIterable from @solana/subscribable.
|
|
71
|
+
sig do
|
|
72
|
+
params(
|
|
73
|
+
store: ReactiveStreamStore,
|
|
74
|
+
signal: T.nilable(T.proc.void),
|
|
75
|
+
should_yield: T.nilable(T.proc.params(value: T.untyped).returns(T::Boolean)),
|
|
76
|
+
poll_interval: Float
|
|
77
|
+
).returns(T::Enumerator[T.untyped])
|
|
78
|
+
end
|
|
79
|
+
def bridge_store_to_async_iterable(store, signal: nil, should_yield: nil, poll_interval: 0.05)
|
|
80
|
+
Enumerator.new do |yielder|
|
|
81
|
+
mutex = Mutex.new
|
|
82
|
+
cond = ConditionVariable.new
|
|
83
|
+
# Latest-wins single-slot buffer. Both are one-element arrays used as
|
|
84
|
+
# "present or nil" boxes so a legitimately-nil value is distinguishable
|
|
85
|
+
# from "nothing buffered".
|
|
86
|
+
latest = T.let(nil, T.nilable(T::Array[T.untyped]))
|
|
87
|
+
failure = T.let(nil, T.nilable(T::Array[T.untyped]))
|
|
88
|
+
|
|
89
|
+
on_change = Kernel.lambda do
|
|
90
|
+
state = store.get_unified_state
|
|
91
|
+
case state.status
|
|
92
|
+
when 'loaded'
|
|
93
|
+
# Drop a value the gate rejects; park again rather than yielding it.
|
|
94
|
+
next if should_yield && !should_yield.call(state.data)
|
|
95
|
+
mutex.synchronize do
|
|
96
|
+
latest = [state.data]
|
|
97
|
+
cond.broadcast
|
|
98
|
+
end
|
|
99
|
+
when 'error'
|
|
100
|
+
# A nil error would otherwise surface as a value-less success;
|
|
101
|
+
# substitute a sentinel so the failure propagates.
|
|
102
|
+
err = state.error || SolanaError.new(SolanaError::SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR)
|
|
103
|
+
mutex.synchronize do
|
|
104
|
+
failure = [err]
|
|
105
|
+
cond.broadcast
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
aborted = Kernel.lambda do
|
|
111
|
+
next false if signal.nil?
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
signal.call
|
|
115
|
+
false
|
|
116
|
+
rescue StandardError
|
|
117
|
+
true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
unsubscribe = store.subscribe(&on_change)
|
|
122
|
+
# Seed from the store's current snapshot: the caller may already have
|
|
123
|
+
# connected (and a value or error may already be present) before
|
|
124
|
+
# iteration began. The bridge never connects the store itself.
|
|
125
|
+
on_change.call
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
Kernel.loop do
|
|
129
|
+
# Abort wins over everything: end cleanly without raising.
|
|
130
|
+
break if aborted.call
|
|
131
|
+
|
|
132
|
+
action = T.let(:park, Symbol)
|
|
133
|
+
payload = T.let(nil, T.untyped)
|
|
134
|
+
|
|
135
|
+
mutex.synchronize do
|
|
136
|
+
if failure
|
|
137
|
+
action = :raise
|
|
138
|
+
payload = failure[0]
|
|
139
|
+
elsif latest
|
|
140
|
+
action = :yield
|
|
141
|
+
payload = latest[0]
|
|
142
|
+
latest = nil
|
|
143
|
+
else
|
|
144
|
+
cond.wait(mutex, signal ? poll_interval : nil)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
case action
|
|
149
|
+
when :yield then yielder.yield(payload)
|
|
150
|
+
when :raise then Kernel.raise(coerce_error(payload))
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
ensure
|
|
154
|
+
unsubscribe.call
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The store's error channel carries whatever the publisher emitted, which
|
|
160
|
+
# need not be an Exception. Ruby can only raise exceptions, so wrap anything
|
|
161
|
+
# else rather than losing the failure.
|
|
162
|
+
sig { params(error: T.untyped).returns(Exception) }
|
|
163
|
+
def coerce_error(error)
|
|
164
|
+
case error
|
|
165
|
+
when Exception then error
|
|
166
|
+
when String then RuntimeError.new(error)
|
|
167
|
+
else RuntimeError.new(error.inspect)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
private_class_method :coerce_error
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative 'subscribable/data_publisher'
|
|
|
8
8
|
require_relative 'subscribable/async_iterable'
|
|
9
9
|
require_relative 'subscribable/reactive_stream_store'
|
|
10
10
|
require_relative 'subscribable/reactive_action_store'
|
|
11
|
+
require_relative 'subscribable/bridge_store_to_async_iterable'
|
|
11
12
|
|
|
12
13
|
module Solana::Ruby::Kit
|
|
13
14
|
module Subscribable
|
|
@@ -14,7 +14,7 @@ module Solana::Ruby::Kit
|
|
|
14
14
|
module TransactionIntrospection
|
|
15
15
|
extend T::Sig
|
|
16
16
|
|
|
17
|
-
# The result of decoding a
|
|
17
|
+
# The result of decoding a confirmed-transaction RPC response: the
|
|
18
18
|
# CompiledTransactionMessage, the loaded ALT addresses pulled from `meta`
|
|
19
19
|
# (if any), and — for `'base64'` and `'base58'` responses — the
|
|
20
20
|
# re-encodable wire-format Transactions::Transaction.
|
|
@@ -33,14 +33,28 @@ module Solana::Ruby::Kit
|
|
|
33
33
|
|
|
34
34
|
module_function
|
|
35
35
|
|
|
36
|
-
# Decodes a
|
|
36
|
+
# Decodes a confirmed-transaction RPC response (any of `encoding: 'base64'`,
|
|
37
37
|
# `'base58'`, or `'json'`) into a CompiledTransactionMessage plus, for
|
|
38
38
|
# `'base64'` and `'base58'`, a re-encodable Transactions::Transaction.
|
|
39
39
|
# The JSON path does not produce a Transaction: the server has already
|
|
40
40
|
# decompiled the wire format, so there are no message bytes to carry.
|
|
41
41
|
#
|
|
42
|
-
# +rpc_tx+ is
|
|
43
|
-
# `
|
|
42
|
+
# +rpc_tx+ is a raw (String-keyed) JSON hash. Only the `transaction` /
|
|
43
|
+
# `meta` / `version` envelope is read — not a method-specific response
|
|
44
|
+
# shape — so results from any method that returns confirmed transactions in
|
|
45
|
+
# these encodings work, and extra fields (`slot`, `blockTime`,
|
|
46
|
+
# `transactionIndex`, …) are ignored:
|
|
47
|
+
#
|
|
48
|
+
# # getTransaction — a single response hash
|
|
49
|
+
# rpc_tx = rpc.get_transaction(signature, encoding: 'base64')
|
|
50
|
+
# TransactionIntrospection.decode_transaction_from_rpc_response(rpc_tx)
|
|
51
|
+
#
|
|
52
|
+
# # getTransactionsForAddress — one hash per element of `data`
|
|
53
|
+
# page = rpc.get_transactions_for_address(
|
|
54
|
+
# address, transaction_details: :full, encoding: 'base64',
|
|
55
|
+
# max_supported_transaction_version: 0
|
|
56
|
+
# )
|
|
57
|
+
# page.data.map { |tx| TransactionIntrospection.decode_transaction_from_rpc_response(tx) }
|
|
44
58
|
#
|
|
45
59
|
# `'jsonParsed'` is not supported — its instructions arrive pre-parsed by
|
|
46
60
|
# the server and lack raw bytes, so they cannot be round-tripped. Passing
|
|
@@ -135,7 +149,10 @@ module Solana::Ruby::Kit
|
|
|
135
149
|
|
|
136
150
|
# The envelope only carries `version` when `maxSupportedTransactionVersion`
|
|
137
151
|
# was set on the request; otherwise the response is necessarily legacy.
|
|
138
|
-
|
|
152
|
+
# A `version` key that is present but null counts as absent — some methods
|
|
153
|
+
# include the key unconditionally. Ruby's `||` is safe here where
|
|
154
|
+
# TypeScript needs `??`: 0 (v0) is truthy in Ruby, so a real v0 survives.
|
|
155
|
+
version = rpc_tx['version'] || :legacy
|
|
139
156
|
version = :legacy if version == 'legacy'
|
|
140
157
|
|
|
141
158
|
compiled_message = CompiledTransactionMessage.new(
|
|
@@ -12,7 +12,7 @@ module Solana::Ruby::Kit
|
|
|
12
12
|
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
-
# Returns the inner instructions in a `
|
|
15
|
+
# Returns the inner instructions in a transaction `meta` as
|
|
16
16
|
# TracedInstructions.
|
|
17
17
|
#
|
|
18
18
|
# The RPC returns inner instructions in a different shape from the wire
|
|
@@ -21,8 +21,10 @@ module Solana::Ruby::Kit
|
|
|
21
21
|
# data, resolves the indices against the supplied AccountMeta list, and
|
|
22
22
|
# tags each instruction with an `inner` trace.
|
|
23
23
|
#
|
|
24
|
-
# +meta+ is the raw (String-keyed) `meta` hash from
|
|
25
|
-
#
|
|
24
|
+
# +meta+ is the raw (String-keyed) `meta` hash from any method that returns
|
|
25
|
+
# transaction metadata without `jsonParsed` encoding — `getTransaction`,
|
|
26
|
+
# `getTransactionsForAddress`, or `getBlock`. Returns an empty array if it
|
|
27
|
+
# carries no `innerInstructions`.
|
|
26
28
|
#
|
|
27
29
|
# Raises if any `programIdIndex` or account index falls outside the
|
|
28
30
|
# supplied `account_metas` list.
|
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: 7.0.0
|
|
4
|
+
version: 7.0.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paul Zupan, Idhra Inc.
|
|
@@ -255,6 +255,7 @@ files:
|
|
|
255
255
|
- lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb
|
|
256
256
|
- lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb
|
|
257
257
|
- lib/solana/ruby/kit/rpc/api/get_transaction.rb
|
|
258
|
+
- lib/solana/ruby/kit/rpc/api/get_transactions_for_address.rb
|
|
258
259
|
- lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb
|
|
259
260
|
- lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb
|
|
260
261
|
- lib/solana/ruby/kit/rpc/api/request_airdrop.rb
|
|
@@ -289,6 +290,7 @@ files:
|
|
|
289
290
|
- lib/solana/ruby/kit/signers/keypair_signer.rb
|
|
290
291
|
- lib/solana/ruby/kit/subscribable.rb
|
|
291
292
|
- lib/solana/ruby/kit/subscribable/async_iterable.rb
|
|
293
|
+
- lib/solana/ruby/kit/subscribable/bridge_store_to_async_iterable.rb
|
|
292
294
|
- lib/solana/ruby/kit/subscribable/data_publisher.rb
|
|
293
295
|
- lib/solana/ruby/kit/subscribable/reactive_action_store.rb
|
|
294
296
|
- lib/solana/ruby/kit/subscribable/reactive_stream_store.rb
|