solana-ruby-kit 7.1.1.2 → 8.3.0
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/README.md +106 -12
- data/lib/solana/ruby/kit/accounts/maybe_account.rb +1 -1
- data/lib/solana/ruby/kit/addresses/address.rb +1 -1
- data/lib/solana/ruby/kit/addresses/curve.rb +1 -1
- data/lib/solana/ruby/kit/addresses/program_derived_address.rb +1 -1
- data/lib/solana/ruby/kit/addresses/public_key.rb +1 -1
- data/lib/solana/ruby/kit/codecs/codec.rb +95 -1
- data/lib/solana/ruby/kit/codecs/data_structures.rb +51 -8
- data/lib/solana/ruby/kit/codecs/numbers.rb +49 -50
- data/lib/solana/ruby/kit/codecs/strings.rb +132 -6
- data/lib/solana/ruby/kit/encoding/base58.rb +1 -1
- data/lib/solana/ruby/kit/errors.rb +12 -0
- data/lib/solana/ruby/kit/fast_stable_stringify.rb +1 -1
- data/lib/solana/ruby/kit/functional.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/instruction_plan.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/max_instructions.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb +107 -2
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_planner.rb +2 -2
- data/lib/solana/ruby/kit/instructions/accounts.rb +1 -1
- data/lib/solana/ruby/kit/instructions/instruction.rb +1 -1
- data/lib/solana/ruby/kit/instructions/roles.rb +1 -1
- data/lib/solana/ruby/kit/keys/key_pair.rb +1 -1
- data/lib/solana/ruby/kit/keys/private_key.rb +1 -1
- data/lib/solana/ruby/kit/keys/public_key.rb +1 -1
- data/lib/solana/ruby/kit/keys/signatures.rb +1 -1
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +1 -1
- data/lib/solana/ruby/kit/offchain_messages/message_v1.rb +1 -1
- data/lib/solana/ruby/kit/options/option.rb +1 -1
- data/lib/solana/ruby/kit/plugin_core.rb +1 -1
- data/lib/solana/ruby/kit/programs/associated_token_account.rb +1 -1
- data/lib/solana/ruby/kit/programs/stake_program.rb +1 -1
- data/lib/solana/ruby/kit/programs/system_program.rb +1 -1
- data/lib/solana/ruby/kit/programs.rb +1 -1
- data/lib/solana/ruby/kit/promises.rb +1 -1
- data/lib/solana/ruby/kit/resource_limit_estimation.rb +1 -1
- data/lib/solana/ruby/kit/rpc/api/get_ag_genesis_cert.rb +72 -0
- data/lib/solana/ruby/kit/rpc/client.rb +2 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/commitment.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/lamports.rb +1 -1
- data/lib/solana/ruby/kit/rpc_types/sol.rb +1 -1
- data/lib/solana/ruby/kit/signers/keypair_signer.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/async_iterable.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/bridge_store_to_async_iterable.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/reactive_action_store.rb +1 -1
- data/lib/solana/ruby/kit/subscribable/reactive_stream_store.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/clock.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +1 -1
- data/lib/solana/ruby/kit/sysvars/rent.rb +1 -1
- data/lib/solana/ruby/kit/sysvars.rb +1 -1
- data/lib/solana/ruby/kit/transaction_confirmation.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/compiled_transaction_message.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/get_instructions.rb +1 -1
- data/lib/solana/ruby/kit/transaction_introspection/walk_instructions.rb +1 -1
- data/lib/solana/ruby/kit/transaction_messages/compute_budget.rb +3 -1
- data/lib/solana/ruby/kit/transaction_messages/resource_limit_validation.rb +70 -0
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +1 -1
- data/lib/solana/ruby/kit/transaction_messages.rb +1 -0
- data/lib/solana/ruby/kit/transactions/compiler.rb +2 -2
- data/lib/solana/ruby/kit/transactions/transaction.rb +30 -10
- data/lib/solana/ruby/kit/version.rb +1 -1
- data/lib/solana/ruby/kit/wallet_standard.rb +3 -3
- metadata +3 -1
|
@@ -15,14 +15,134 @@ module Solana::Ruby::Kit
|
|
|
15
15
|
# defeated the "directly available as Codecs.x" intent in codecs.rb.
|
|
16
16
|
extend self
|
|
17
17
|
|
|
18
|
+
# Byte order mark, U+FEFF, in its UTF-8 encoding.
|
|
19
|
+
BOM = T.let("\xEF\xBB\xBF".b.freeze, String)
|
|
20
|
+
|
|
21
|
+
# Remove every null character from a decoded string.
|
|
22
|
+
# Mirrors `removeNullCharacters`.
|
|
23
|
+
sig { params(value: String).returns(String) }
|
|
24
|
+
def remove_null_characters(value)
|
|
25
|
+
value.delete("\x00")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Find the first malformed UTF-8 sequence in +bytes+, starting at +offset+.
|
|
29
|
+
# Returns the byte offset at which it starts, or -1 when the bytes are
|
|
30
|
+
# well-formed.
|
|
31
|
+
#
|
|
32
|
+
# This rejects everything the Unicode standard rejects: unexpected
|
|
33
|
+
# continuation bytes, overlong encodings, encoded surrogates, code points
|
|
34
|
+
# above U+10FFFF and truncated sequences. Ruby's own +valid_encoding?+
|
|
35
|
+
# answers the same question, but only yes or no - this reports *where*,
|
|
36
|
+
# which is what the error context needs.
|
|
37
|
+
# Mirrors `findMalformedUtf8SequenceOffset`.
|
|
38
|
+
sig { params(bytes: String, offset: Integer).returns(Integer) }
|
|
39
|
+
def find_malformed_utf8_sequence_offset(bytes, offset = 0)
|
|
40
|
+
arr = T.cast(T.unsafe(bytes.b).unpack('C*'), T::Array[Integer])
|
|
41
|
+
length = arr.length
|
|
42
|
+
index = offset
|
|
43
|
+
while index < length
|
|
44
|
+
lead = T.must(arr[index])
|
|
45
|
+
min = 0x80
|
|
46
|
+
max = 0xBF
|
|
47
|
+
if lead < 0x80
|
|
48
|
+
index += 1
|
|
49
|
+
next
|
|
50
|
+
elsif lead >= 0xC2 && lead <= 0xDF
|
|
51
|
+
count = 1
|
|
52
|
+
elsif lead >= 0xE0 && lead <= 0xEF
|
|
53
|
+
count = 2
|
|
54
|
+
min = 0xA0 if lead == 0xE0 # Overlong.
|
|
55
|
+
max = 0x9F if lead == 0xED # Encoded surrogate.
|
|
56
|
+
elsif lead >= 0xF0 && lead <= 0xF4
|
|
57
|
+
count = 3
|
|
58
|
+
min = 0x90 if lead == 0xF0 # Overlong.
|
|
59
|
+
max = 0x8F if lead == 0xF4 # Above U+10FFFF.
|
|
60
|
+
else
|
|
61
|
+
return index
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
start = index
|
|
65
|
+
index += 1
|
|
66
|
+
count.times do |i|
|
|
67
|
+
return start if index >= length
|
|
68
|
+
|
|
69
|
+
byte = T.must(arr[index])
|
|
70
|
+
in_range = i.zero? ? byte >= min && byte <= max : byte >= 0x80 && byte <= 0xBF
|
|
71
|
+
return start unless in_range
|
|
72
|
+
|
|
73
|
+
index += 1
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
-1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Raise unless +bytes+, from +offset+, form well-formed UTF-8.
|
|
80
|
+
# Mirrors `assertIsWellFormedUtf8Bytes`.
|
|
81
|
+
sig { params(bytes: String, offset: Integer).void }
|
|
82
|
+
def assert_is_well_formed_utf8_bytes(bytes, offset = 0)
|
|
83
|
+
malformed = find_malformed_utf8_sequence_offset(bytes, offset)
|
|
84
|
+
return if malformed == -1
|
|
85
|
+
|
|
86
|
+
Kernel.raise SolanaError.new(
|
|
87
|
+
SolanaError::CODECS__INVALID_UTF8_BYTES,
|
|
88
|
+
{ bytes: bytes.b, offset: malformed }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Raise unless +value+ can be encoded as UTF-8 without loss.
|
|
93
|
+
# Mirrors `assertIsWellFormedUtf8String`.
|
|
94
|
+
#
|
|
95
|
+
# Upstream guards against lone surrogates, which a JavaScript string can
|
|
96
|
+
# hold because it is a sequence of UTF-16 code units. A Ruby String is a
|
|
97
|
+
# byte sequence with an encoding tag, so the equivalent flaw is a String
|
|
98
|
+
# whose bytes are not valid UTF-8. Upstream's reported +index+ counts
|
|
99
|
+
# UTF-16 code units; the +index+ here is a byte offset.
|
|
100
|
+
sig { params(value: String).void }
|
|
101
|
+
def assert_is_well_formed_utf8_string(value)
|
|
102
|
+
index = find_malformed_utf8_sequence_offset(value.b)
|
|
103
|
+
return if index == -1
|
|
104
|
+
|
|
105
|
+
Kernel.raise SolanaError.new(
|
|
106
|
+
SolanaError::CODECS__INVALID_UTF8_STRING,
|
|
107
|
+
{ index: index, value: value }
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
18
111
|
# UTF-8 string codec.
|
|
19
112
|
# When +size+ is given the encoded bytes are fixed to that length
|
|
20
113
|
# (zero-padded or truncated); otherwise the codec is variable-length
|
|
21
114
|
# and must be used inside a size-prefixed container.
|
|
22
|
-
|
|
23
|
-
|
|
115
|
+
#
|
|
116
|
+
# +fatal+ rejects invalid UTF-8 instead of passing it through: on encode a
|
|
117
|
+
# String whose bytes are not valid UTF-8, on decode a malformed byte
|
|
118
|
+
# sequence.
|
|
119
|
+
#
|
|
120
|
+
# +ignore_bom+ follows TextDecoder's confusing spelling: the default,
|
|
121
|
+
# +false+, *strips* a leading byte order mark; +true+ keeps it.
|
|
122
|
+
#
|
|
123
|
+
# +remove_null_characters+ strips every null character from the decoded
|
|
124
|
+
# string, which is what makes fixed-size padded strings read back cleanly.
|
|
125
|
+
# It is on by default, matching upstream, and makes the codec lossy for
|
|
126
|
+
# strings that legitimately contain nulls - pass +false+ for a lossless
|
|
127
|
+
# round trip.
|
|
128
|
+
sig do
|
|
129
|
+
params(
|
|
130
|
+
size: T.nilable(Integer),
|
|
131
|
+
fatal: T::Boolean,
|
|
132
|
+
ignore_bom: T::Boolean,
|
|
133
|
+
remove_null_characters: T::Boolean
|
|
134
|
+
).returns(Codec)
|
|
135
|
+
end
|
|
136
|
+
def utf8_codec(size: nil, fatal: false, ignore_bom: false, remove_null_characters: true)
|
|
137
|
+
strip_nulls = remove_null_characters
|
|
24
138
|
enc = Encoder.new(fixed_size: size) do |v|
|
|
25
|
-
|
|
139
|
+
str = v.to_s
|
|
140
|
+
# Transcode anything that is not already UTF-8 (or raw bytes); a
|
|
141
|
+
# UTF-8 to UTF-8 `encode` is a no-op and would not validate, which is
|
|
142
|
+
# what `fatal` is for.
|
|
143
|
+
str = str.encode(::Encoding::UTF_8) unless [::Encoding::UTF_8, ::Encoding::BINARY].include?(str.encoding)
|
|
144
|
+
raw = str.b
|
|
145
|
+
assert_is_well_formed_utf8_string(raw) if fatal
|
|
26
146
|
if size
|
|
27
147
|
raw.bytesize <= size ? raw.ljust(size, "\x00") : raw.byteslice(0, size) || ''.b
|
|
28
148
|
else
|
|
@@ -32,9 +152,15 @@ module Solana::Ruby::Kit
|
|
|
32
152
|
dec = Decoder.new(fixed_size: size) do |bytes, offset|
|
|
33
153
|
len = size || (bytes.bytesize - offset)
|
|
34
154
|
slice = bytes.b.byteslice(offset, len) || ''.b
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
155
|
+
assert_is_well_formed_utf8_bytes(slice) if fatal
|
|
156
|
+
slice = T.must(slice.byteslice(BOM.bytesize..)) if !ignore_bom && slice.start_with?(BOM)
|
|
157
|
+
str = slice.force_encoding(::Encoding::UTF_8)
|
|
158
|
+
# Non-fatal decoding substitutes U+FFFD for malformed sequences, as
|
|
159
|
+
# TextDecoder does. `scrub` is Ruby's version of exactly that, and it
|
|
160
|
+
# also keeps the null-stripping below from raising on invalid bytes.
|
|
161
|
+
str = str.scrub unless fatal
|
|
162
|
+
str = remove_null_characters(str) if strip_nulls
|
|
163
|
+
[str, len]
|
|
38
164
|
end
|
|
39
165
|
Codec.new(enc, dec)
|
|
40
166
|
end
|
|
@@ -60,6 +60,10 @@ module Solana::Ruby::Kit
|
|
|
60
60
|
TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE
|
|
61
61
|
# context: { unsupported_version: }
|
|
62
62
|
TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED = :SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED
|
|
63
|
+
# context: { compute_unit_limit:, max_compute_unit_limit: }
|
|
64
|
+
TRANSACTION__COMPUTE_UNIT_LIMIT_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTION__COMPUTE_UNIT_LIMIT_OUT_OF_RANGE
|
|
65
|
+
# context: { heap_size:, max_heap_size:, min_heap_size:, multiple_of: }
|
|
66
|
+
TRANSACTION__INVALID_HEAP_SIZE = :SOLANA_ERROR__TRANSACTION__INVALID_HEAP_SIZE
|
|
63
67
|
TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE = :SOLANA_ERROR__TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE
|
|
64
68
|
TRANSACTIONS__BLOCKHASH_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__BLOCKHASH_NOT_FOUND
|
|
65
69
|
TRANSACTIONS__FAILED_TRANSACTION_PLAN = :SOLANA_ERROR__TRANSACTIONS__FAILED_TRANSACTION_PLAN
|
|
@@ -91,6 +95,10 @@ module Solana::Ruby::Kit
|
|
|
91
95
|
CODECS__INVALID_BYTE_LENGTH = :SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH
|
|
92
96
|
CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR = :SOLANA_ERROR__CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR
|
|
93
97
|
CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC = :SOLANA_ERROR__CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC
|
|
98
|
+
# context: { bytes:, offset: }
|
|
99
|
+
CODECS__INVALID_UTF8_BYTES = :SOLANA_ERROR__CODECS__INVALID_UTF8_BYTES
|
|
100
|
+
# context: { index:, value: }
|
|
101
|
+
CODECS__INVALID_UTF8_STRING = :SOLANA_ERROR__CODECS__INVALID_UTF8_STRING
|
|
94
102
|
|
|
95
103
|
# ── RPC / JSON-RPC ────────────────────────────────────────────────────────
|
|
96
104
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
|
|
@@ -207,6 +215,8 @@ module Solana::Ruby::Kit
|
|
|
207
215
|
TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found at index %{index}',
|
|
208
216
|
TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE => 'Failed to decompile instruction: account index %{index} is out of range',
|
|
209
217
|
TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED => 'Transaction version %{unsupported_version} is not supported',
|
|
218
|
+
TRANSACTION__COMPUTE_UNIT_LIMIT_OUT_OF_RANGE => 'Transaction compute unit limit must be an integer in the range [0, %{max_compute_unit_limit}]. `%{compute_unit_limit}` given',
|
|
219
|
+
TRANSACTION__INVALID_HEAP_SIZE => 'Transaction heap size must be an integer multiple of %{multiple_of} bytes in the range [%{min_heap_size}, %{max_heap_size}]. `%{heap_size}` given',
|
|
210
220
|
TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE => 'Transaction simulation failed: %{message}',
|
|
211
221
|
TRANSACTIONS__BLOCKHASH_NOT_FOUND => 'Blockhash not found',
|
|
212
222
|
TRANSACTIONS__FAILED_TRANSACTION_PLAN => 'Failed to execute transaction plan',
|
|
@@ -238,6 +248,8 @@ module Solana::Ruby::Kit
|
|
|
238
248
|
CODECS__INVALID_BYTE_LENGTH => 'Expected %{expected} bytes but got %{actual}',
|
|
239
249
|
CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR => 'Expected zero value to match fixed discriminator',
|
|
240
250
|
CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC => 'A fixed-size nullable codec cannot wrap a variable-size codec',
|
|
251
|
+
CODECS__INVALID_UTF8_BYTES => 'Invalid UTF-8 byte sequence at offset %{offset}',
|
|
252
|
+
CODECS__INVALID_UTF8_STRING => 'Invalid UTF-8 string at index %{index}',
|
|
241
253
|
|
|
242
254
|
# RPC
|
|
243
255
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER => 'Integer overflow while serializing large integer %{value}',
|
|
@@ -16,7 +16,7 @@ module Solana::Ruby::Kit
|
|
|
16
16
|
# The hard maximum number of top-level instructions the transaction format can encode.
|
|
17
17
|
TRANSACTION_INSTRUCTION_LIMIT = T.let(64, Integer)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
extend self
|
|
20
20
|
|
|
21
21
|
# Resolves the effective maximum number of instructions allowed in a transaction message,
|
|
22
22
|
# falling back to the default when no value is provided.
|
|
@@ -9,7 +9,7 @@ module Solana::Ruby::Kit
|
|
|
9
9
|
module InstructionPlans
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
extend self
|
|
13
13
|
|
|
14
14
|
# Creates a TransactionPlanExecutor — a callable that traverses a TransactionPlan,
|
|
15
15
|
# executing each single transaction message and collecting results.
|
|
@@ -58,9 +58,60 @@ module Solana::Ruby::Kit
|
|
|
58
58
|
}
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Creates a TransactionPlanExecutor that processes every leaf concurrently.
|
|
62
|
+
#
|
|
63
|
+
# It takes the same configuration as +create_transaction_plan_executor+ and its
|
|
64
|
+
# +execute_transaction_message+ callback follows the same contract: it receives a fresh
|
|
65
|
+
# mutable +context+ Hash for every leaf and returns the Hash a successful result should
|
|
66
|
+
# carry. On success the two are merged with the returned value winning; when the callback
|
|
67
|
+
# raises, the context accumulated so far is preserved on the failed result. The legacy
|
|
68
|
+
# one-argument callable shape is accepted here too.
|
|
69
|
+
#
|
|
70
|
+
# The difference is the traversal. This executor preserves the input plan's nesting, order
|
|
71
|
+
# and divisibility in the returned result, but it does not enforce the execution
|
|
72
|
+
# dependencies expressed by sequential plans: every leaf is started immediately, each on
|
|
73
|
+
# its own thread, with no concurrency limit. That makes it suitable for work that can be
|
|
74
|
+
# performed independently, such as signing or serializing transactions. For the same
|
|
75
|
+
# reason a callback that raises does not cancel the other leaves — each one runs to
|
|
76
|
+
# completion — and non-divisible sequential plans are supported rather than rejected.
|
|
77
|
+
#
|
|
78
|
+
# Once every leaf has settled, a result tree containing any failure raises
|
|
79
|
+
# INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN carrying the complete tree on its
|
|
80
|
+
# +transaction_plan_result+ reader.
|
|
81
|
+
#
|
|
82
|
+
# Because each leaf runs on its own thread, the callback must be safe to run concurrently
|
|
83
|
+
# with itself. Each leaf gets its own context Hash, so the contexts do not need guarding,
|
|
84
|
+
# but anything the callback shares between invocations does.
|
|
85
|
+
#
|
|
86
|
+
# Mirrors `createTransactionPlanExecutorWithConcurrentLeaves(config)` from
|
|
87
|
+
# @solana/instruction-plans.
|
|
88
|
+
# NOTE: Ruby translation has no abort signal; leaves are never canceled, only failed.
|
|
89
|
+
sig { params(execute_transaction_message: T.untyped).returns(T.untyped) }
|
|
90
|
+
def create_transaction_plan_executor_with_concurrent_leaves(execute_transaction_message:)
|
|
91
|
+
->(transaction_plan) {
|
|
92
|
+
# Traversal spawns every leaf's thread on the way down and returns a tree of thunks;
|
|
93
|
+
# resolving them afterwards is what joins the threads, so all leaves are already in
|
|
94
|
+
# flight before the first one is waited on.
|
|
95
|
+
result = concurrent_executor_traverse(transaction_plan, execute_transaction_message).call
|
|
96
|
+
|
|
97
|
+
cause = executor_find_error(result)
|
|
98
|
+
if cause
|
|
99
|
+
err = SolanaError.new(
|
|
100
|
+
SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
|
|
101
|
+
{ cause: cause }
|
|
102
|
+
)
|
|
103
|
+
err.instance_variable_set(:@transaction_plan_result, result)
|
|
104
|
+
err.define_singleton_method(:transaction_plan_result) { result }
|
|
105
|
+
Kernel.raise err
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
result
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
61
112
|
# ── Private helpers ────────────────────────────────────────────────────────
|
|
62
113
|
|
|
63
|
-
|
|
114
|
+
extend self
|
|
64
115
|
|
|
65
116
|
sig { params(plan: T.untyped, execute_fn: T.untyped, state: T.untyped).returns(T.untyped) }
|
|
66
117
|
|
|
@@ -145,6 +196,58 @@ module Solana::Ruby::Kit
|
|
|
145
196
|
returned
|
|
146
197
|
end
|
|
147
198
|
|
|
199
|
+
# Returns a thunk that resolves to this plan's result. Leaf threads are started as the
|
|
200
|
+
# tree is walked, so the whole plan is in flight by the time the outermost thunk is called.
|
|
201
|
+
sig { params(plan: T.untyped, execute_fn: T.untyped).returns(T.untyped) }
|
|
202
|
+
|
|
203
|
+
def concurrent_executor_traverse(plan, execute_fn)
|
|
204
|
+
case plan.kind
|
|
205
|
+
when :single
|
|
206
|
+
thread = Thread.new { concurrent_executor_execute_single(plan, execute_fn) }
|
|
207
|
+
-> { thread.value }
|
|
208
|
+
when :sequential, :parallel
|
|
209
|
+
thunks = plan.plans.map { |sub| concurrent_executor_traverse(sub, execute_fn) }
|
|
210
|
+
Kernel.lambda do
|
|
211
|
+
results = thunks.map(&:call)
|
|
212
|
+
if plan.kind == :parallel
|
|
213
|
+
parallel_transaction_plan_result(results)
|
|
214
|
+
elsif plan.divisible
|
|
215
|
+
sequential_transaction_plan_result(results)
|
|
216
|
+
else
|
|
217
|
+
# Unlike the sequential executor, non-divisible plans are supported: nothing
|
|
218
|
+
# about running every leaf independently depends on the plan being divisible.
|
|
219
|
+
non_divisible_sequential_transaction_plan_result(results)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
Kernel.raise SolanaError.new(
|
|
224
|
+
SolanaError::INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
|
|
225
|
+
{ kind: plan.kind }
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Runs one leaf. Identical to +executor_traverse_single+ except that a failure is recorded
|
|
231
|
+
# and nothing else — there is no shared cancellation state for it to trip.
|
|
232
|
+
sig { params(plan: T.untyped, execute_fn: T.untyped).returns(T.untyped) }
|
|
233
|
+
|
|
234
|
+
def concurrent_executor_execute_single(plan, execute_fn)
|
|
235
|
+
context = {}
|
|
236
|
+
|
|
237
|
+
begin
|
|
238
|
+
returned = executor_invoke(execute_fn, context, plan.message)
|
|
239
|
+
successful_single_transaction_plan_result(plan.message, nil, context.merge(returned))
|
|
240
|
+
rescue SolanaError => e
|
|
241
|
+
failed_single_transaction_plan_result(plan.message, e, context)
|
|
242
|
+
rescue => e
|
|
243
|
+
wrapped = SolanaError.new(
|
|
244
|
+
SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
|
|
245
|
+
{ cause: e }
|
|
246
|
+
)
|
|
247
|
+
failed_single_transaction_plan_result(plan.message, wrapped, context)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
148
251
|
sig { params(result: T.untyped).returns(T.untyped) }
|
|
149
252
|
|
|
150
253
|
def executor_find_error(result)
|
|
@@ -164,5 +267,7 @@ module Solana::Ruby::Kit
|
|
|
164
267
|
private_class_method :executor_traverse_single
|
|
165
268
|
private_class_method :executor_invoke
|
|
166
269
|
private_class_method :executor_find_error
|
|
270
|
+
private_class_method :concurrent_executor_traverse
|
|
271
|
+
private_class_method :concurrent_executor_execute_single
|
|
167
272
|
end
|
|
168
273
|
end
|
|
@@ -11,7 +11,7 @@ module Solana::Ruby::Kit
|
|
|
11
11
|
module InstructionPlans
|
|
12
12
|
extend T::Sig
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
extend self
|
|
15
15
|
|
|
16
16
|
# Creates a TransactionPlanner — a callable that converts an InstructionPlan
|
|
17
17
|
# into a TransactionPlan by packing instructions into transaction messages.
|
|
@@ -71,7 +71,7 @@ module Solana::Ruby::Kit
|
|
|
71
71
|
|
|
72
72
|
# ── Private helpers ────────────────────────────────────────────────────────
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
extend self
|
|
75
75
|
|
|
76
76
|
sig { params(plan: T.untyped, ctx: T.untyped).returns(T.untyped) }
|
|
77
77
|
|
|
@@ -19,7 +19,7 @@ module Solana::Ruby::Kit
|
|
|
19
19
|
|
|
20
20
|
ALL = T.let([READONLY, WRITABLE, READONLY_SIGNER, WRITABLE_SIGNER].freeze, T::Array[Integer])
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
extend self
|
|
23
23
|
|
|
24
24
|
# Returns true if the role requires the account to sign the transaction.
|
|
25
25
|
sig { params(role: Integer).returns(T::Boolean) }
|
|
@@ -57,7 +57,7 @@ module Solana::Ruby::Kit
|
|
|
57
57
|
# accepts Option<T>, T, or nil — useful for codec input.
|
|
58
58
|
OptionOrNullable = T.type_alias { T.untyped }
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
extend self
|
|
61
61
|
|
|
62
62
|
# Wraps a value in Some. Mirrors `some<T>(value)`.
|
|
63
63
|
sig { params(value: T.untyped).returns(Solana::Ruby::Kit::Options::Some[T.untyped]) }
|
|
@@ -40,7 +40,7 @@ module Solana::Ruby::Kit
|
|
|
40
40
|
DISCRIMINATOR_INITIALIZE = T.let(0, Integer)
|
|
41
41
|
DISCRIMINATOR_DELEGATE = T.let(2, Integer)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
extend self
|
|
44
44
|
|
|
45
45
|
# Builds the two instructions needed to create and initialise a new stake
|
|
46
46
|
# account funded from +from+.
|
|
@@ -27,7 +27,7 @@ module Solana::Ruby::Kit
|
|
|
27
27
|
# Instruction discriminators (u32 little-endian).
|
|
28
28
|
DISCRIMINATOR_TRANSFER = T.let(2, Integer)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
extend self
|
|
31
31
|
|
|
32
32
|
# Builds a System Program Transfer instruction that moves +lamports+ from
|
|
33
33
|
# +sender+ to +recipient+.
|
|
@@ -19,7 +19,7 @@ module Solana::Ruby::Kit
|
|
|
19
19
|
|
|
20
20
|
PROVISORY_LIMIT = T.let(0, Integer)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
extend self
|
|
23
23
|
|
|
24
24
|
# Returns a callable that estimates the resource limits required by a transaction message
|
|
25
25
|
# by simulating it with maximum limits set, then reading back the actual consumption.
|