solana-ruby-kit 8.2.0 → 8.4.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 +164 -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 +115 -5
- data/lib/solana/ruby/kit/codecs/data_structures.rb +246 -47
- data/lib/solana/ruby/kit/codecs/numbers.rb +96 -61
- 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 +18 -1
- 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 +31 -26
- data/lib/solana/ruby/kit/instruction_plans/message_packer_errors.rb +131 -0
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb +2 -2
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb +1 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_planner.rb +11 -14
- data/lib/solana/ruby/kit/instruction_plans.rb +1 -1
- 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_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 +8 -7
- 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 +1 -1
- data/lib/solana/ruby/kit/transaction_messages/resource_limit_validation.rb +1 -1
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +1 -1
- 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 +10 -9
- data/solana-ruby-kit.gemspec +1 -1
- metadata +6 -6
- data/lib/solana/ruby/kit/instruction_plans/max_instructions.rb +0 -59
|
@@ -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
|
|
@@ -95,6 +95,15 @@ module Solana::Ruby::Kit
|
|
|
95
95
|
CODECS__INVALID_BYTE_LENGTH = :SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH
|
|
96
96
|
CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR = :SOLANA_ERROR__CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR
|
|
97
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
|
|
102
|
+
# context: { codec_description:, min:, max:, value: }
|
|
103
|
+
CODECS__NUMBER_OUT_OF_RANGE = :SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE
|
|
104
|
+
# context: { codec_description:, hex_sentinel:, sentinel: }
|
|
105
|
+
CODECS__SENTINEL_MISSING_AT_END_OF_BYTES = :SOLANA_ERROR__CODECS__SENTINEL_MISSING_AT_END_OF_BYTES
|
|
106
|
+
CODECS__SENTINEL_MUST_NOT_BE_EMPTY = :SOLANA_ERROR__CODECS__SENTINEL_MUST_NOT_BE_EMPTY
|
|
98
107
|
|
|
99
108
|
# ── RPC / JSON-RPC ────────────────────────────────────────────────────────
|
|
100
109
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
|
|
@@ -134,6 +143,8 @@ module Solana::Ruby::Kit
|
|
|
134
143
|
INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION = :SOLANA_ERROR__INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION
|
|
135
144
|
# context: { max_instructions:, num_instructions: }
|
|
136
145
|
INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED = :SOLANA_ERROR__INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED
|
|
146
|
+
# context: { reason: }
|
|
147
|
+
INSTRUCTION_PLANS__MESSAGE_REJECTED_BY_PACKER = :SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_REJECTED_BY_PACKER
|
|
137
148
|
|
|
138
149
|
# ── Transaction introspection ────────────────────────────────────────────
|
|
139
150
|
TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION = :SOLANA_ERROR__TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION
|
|
@@ -233,7 +244,7 @@ module Solana::Ruby::Kit
|
|
|
233
244
|
CODECS__EXPECTED_POSITIVE_BYTE_LENGTH => 'Expected a positive byte length, got %{byte_length}',
|
|
234
245
|
CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH => 'Encoder fixed size (%{encoder_size}) does not match decoder fixed size (%{decoder_size})',
|
|
235
246
|
CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH => 'Encoder max size (%{encoder_max}) does not match decoder max size (%{decoder_max})',
|
|
236
|
-
CODECS__INVALID_NUMBER_OF_ITEMS => 'Expected %{expected} items
|
|
247
|
+
CODECS__INVALID_NUMBER_OF_ITEMS => 'Expected [%{codec_description}] to have %{expected} items, got %{actual}.',
|
|
237
248
|
CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE => 'Enum discriminator %{discriminator} is out of range [0, %{max}]',
|
|
238
249
|
CODECS__UNION_VARIANT_OUT_OF_RANGE => 'Union variant index %{index} is out of range',
|
|
239
250
|
CODECS__OFFSET_OUT_OF_RANGE => 'Codec offset %{offset} is out of range for byte array of length %{byte_length}',
|
|
@@ -244,6 +255,11 @@ module Solana::Ruby::Kit
|
|
|
244
255
|
CODECS__INVALID_BYTE_LENGTH => 'Expected %{expected} bytes but got %{actual}',
|
|
245
256
|
CODECS__EXPECTED_ZERO_VALUE_TO_MATCH_ITEM_FIXED_DISCRIMINATOR => 'Expected zero value to match fixed discriminator',
|
|
246
257
|
CODECS__FIXED_NULLABLE_CANNOT_WRAP_VARIABLE_SIZE_CODEC => 'A fixed-size nullable codec cannot wrap a variable-size codec',
|
|
258
|
+
CODECS__INVALID_UTF8_BYTES => 'Invalid UTF-8 byte sequence at offset %{offset}',
|
|
259
|
+
CODECS__INVALID_UTF8_STRING => 'Invalid UTF-8 string at index %{index}',
|
|
260
|
+
CODECS__NUMBER_OUT_OF_RANGE => 'Codec [%{codec_description}] expected number to be in the range [%{min}, %{max}], got %{value}.',
|
|
261
|
+
CODECS__SENTINEL_MISSING_AT_END_OF_BYTES => 'Codec [%{codec_description}] expected sentinel [%{hex_sentinel}] to terminate the collection, but reached the end of the byte array without it.',
|
|
262
|
+
CODECS__SENTINEL_MUST_NOT_BE_EMPTY => 'The sentinel must not be empty.',
|
|
247
263
|
|
|
248
264
|
# RPC
|
|
249
265
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER => 'Integer overflow while serializing large integer %{value}',
|
|
@@ -281,6 +297,7 @@ module Solana::Ruby::Kit
|
|
|
281
297
|
INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
|
|
282
298
|
INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION => 'The configured maximum of %{max_instructions} instructions per transaction is invalid. It must be a positive integer no greater than the transaction format limit of %{transaction_instruction_limit} instructions per transaction.',
|
|
283
299
|
INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED => 'Planning this transaction message would require %{num_instructions} instructions, which exceeds the configured maximum of %{max_instructions} instructions per transaction.',
|
|
300
|
+
INSTRUCTION_PLANS__MESSAGE_REJECTED_BY_PACKER => 'The message packer rejected the provided transaction message: %{reason}.',
|
|
284
301
|
|
|
285
302
|
# Transaction introspection
|
|
286
303
|
TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION => "Cannot decode a 'jsonParsed'-encoded getTransaction response; its instructions are pre-parsed and lack raw bytes. Fetch with encoding: 'json' or 'base64' instead.",
|
|
@@ -5,7 +5,7 @@ require_relative '../errors'
|
|
|
5
5
|
require_relative '../instructions/instruction'
|
|
6
6
|
require_relative '../transaction_messages/transaction_message'
|
|
7
7
|
require_relative '../transactions/compiler'
|
|
8
|
-
require_relative '
|
|
8
|
+
require_relative 'message_packer_errors'
|
|
9
9
|
|
|
10
10
|
module Solana::Ruby::Kit
|
|
11
11
|
module InstructionPlans
|
|
@@ -84,6 +84,14 @@ module Solana::Ruby::Kit
|
|
|
84
84
|
#
|
|
85
85
|
# +max_instructions+ caps the number of top-level instructions allowed in the
|
|
86
86
|
# returned message (defaults to 16; must be a positive integer no greater than 64).
|
|
87
|
+
#
|
|
88
|
+
# A custom packer can enforce those limits with
|
|
89
|
+
# InstructionPlans.resolve_max_instructions_per_transaction,
|
|
90
|
+
# .assert_max_instructions_per_transaction and .assert_message_can_accommodate_size,
|
|
91
|
+
# and may raise INSTRUCTION_PLANS__MESSAGE_REJECTED_BY_PACKER (with a +reason:+) to
|
|
92
|
+
# refuse a message for any other reason - e.g. a constraint specific to the
|
|
93
|
+
# instructions being packed. The planner then tries another message; see
|
|
94
|
+
# InstructionPlans.message_packer_error_that_requires_new_candidate?.
|
|
87
95
|
sig do
|
|
88
96
|
params(
|
|
89
97
|
message: TransactionMessages::TransactionMessage,
|
|
@@ -97,7 +105,7 @@ module Solana::Ruby::Kit
|
|
|
97
105
|
|
|
98
106
|
# ── Factory helpers ────────────────────────────────────────────────────────
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
extend self
|
|
101
109
|
|
|
102
110
|
# Wraps a single instruction in a plan.
|
|
103
111
|
# Mirrors `singleInstructionPlan(instruction)`.
|
|
@@ -148,8 +156,7 @@ module Solana::Ruby::Kit
|
|
|
148
156
|
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
|
|
149
157
|
end
|
|
150
158
|
|
|
151
|
-
InstructionPlans.
|
|
152
|
-
resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
|
|
159
|
+
resolved_max = InstructionPlans.resolve_max_instructions_per_transaction(max_instructions)
|
|
153
160
|
InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)
|
|
154
161
|
|
|
155
162
|
base_ix = get_instruction.call(offset, 0)
|
|
@@ -194,8 +201,7 @@ module Solana::Ruby::Kit
|
|
|
194
201
|
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
|
|
195
202
|
end
|
|
196
203
|
|
|
197
|
-
InstructionPlans.
|
|
198
|
-
resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
|
|
204
|
+
resolved_max = InstructionPlans.resolve_max_instructions_per_transaction(max_instructions)
|
|
199
205
|
InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)
|
|
200
206
|
|
|
201
207
|
original_size = Transactions.get_transaction_message_size(message)
|
|
@@ -212,18 +218,16 @@ module Solana::Ruby::Kit
|
|
|
212
218
|
end
|
|
213
219
|
|
|
214
220
|
next_packed = TransactionMessages.append_instructions(packed, [T.must(instructions[i])])
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
226
|
-
end
|
|
221
|
+
next_size = Transactions.get_transaction_message_size(next_packed)
|
|
222
|
+
size_limit = Transactions::TRANSACTION_SIZE_LIMIT
|
|
223
|
+
|
|
224
|
+
if i == start_idx
|
|
225
|
+
# The count was already asserted above, so the first instruction can
|
|
226
|
+
# only fail to fit because of the transaction size limit.
|
|
227
|
+
InstructionPlans.assert_message_can_accommodate_size(
|
|
228
|
+
current_size: original_size, next_size: next_size, size_limit: size_limit
|
|
229
|
+
)
|
|
230
|
+
elsif next_size > size_limit
|
|
227
231
|
idx = i
|
|
228
232
|
return packed
|
|
229
233
|
end
|
|
@@ -240,7 +244,9 @@ module Solana::Ruby::Kit
|
|
|
240
244
|
end
|
|
241
245
|
|
|
242
246
|
# Creates a MessagePackerInstructionPlan that splits +total_size+ bytes into
|
|
243
|
-
# chunks of REALLOC_LIMIT (10,240) bytes
|
|
247
|
+
# chunks of at most REALLOC_LIMIT (10,240) bytes and creates one instruction
|
|
248
|
+
# per chunk, calling +get_instruction.(size)+. A +total_size+ that is an exact
|
|
249
|
+
# multiple of the limit yields only full-sized chunks, and zero yields none.
|
|
244
250
|
# Mirrors `getReallocMessagePackerInstructionPlan({ getInstruction, totalSize })`.
|
|
245
251
|
sig do
|
|
246
252
|
params(
|
|
@@ -249,13 +255,12 @@ module Solana::Ruby::Kit
|
|
|
249
255
|
).returns(MessagePackerInstructionPlan)
|
|
250
256
|
end
|
|
251
257
|
def get_realloc_message_packer_instruction_plan(total_size:, get_instruction:)
|
|
252
|
-
realloc_limit
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
get_instruction.call(chunk)
|
|
258
|
+
realloc_limit = 10_240
|
|
259
|
+
instructions = []
|
|
260
|
+
remaining = total_size
|
|
261
|
+
while remaining.positive?
|
|
262
|
+
instructions << get_instruction.call([realloc_limit, remaining].min)
|
|
263
|
+
remaining -= realloc_limit
|
|
259
264
|
end
|
|
260
265
|
|
|
261
266
|
get_message_packer_instruction_plan_from_instructions(instructions)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module InstructionPlans
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
# The default maximum number of top-level instructions per planned transaction message.
|
|
11
|
+
#
|
|
12
|
+
# Intentionally lower than the transaction format's instruction limit to leave headroom
|
|
13
|
+
# for inner instructions (CPIs), which are not visible at planning time.
|
|
14
|
+
DEFAULT_MAX_INSTRUCTIONS_PER_TRANSACTION = T.let(16, Integer)
|
|
15
|
+
|
|
16
|
+
# The hard maximum number of top-level instructions the transaction format can encode.
|
|
17
|
+
TRANSACTION_INSTRUCTION_LIMIT = T.let(64, Integer)
|
|
18
|
+
|
|
19
|
+
# The error codes the transaction planner treats as "try another message": the message
|
|
20
|
+
# holds too many instructions, would be too large, or a message packer refused it for a
|
|
21
|
+
# custom reason. Upstream's list also has four `TRANSACTION__TOO_MANY_*` compile errors;
|
|
22
|
+
# Ruby's compiler raises none of them, so they are not listed.
|
|
23
|
+
MESSAGE_PACKER_ERRORS_THAT_REQUIRE_NEW_CANDIDATE = T.let(
|
|
24
|
+
[
|
|
25
|
+
SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED,
|
|
26
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
|
|
27
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_REJECTED_BY_PACKER
|
|
28
|
+
].freeze,
|
|
29
|
+
T::Array[Symbol]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
extend self
|
|
33
|
+
|
|
34
|
+
# Resolves the maximum number of instructions a message packer may put in a transaction
|
|
35
|
+
# message: the default of 16 when +max_instructions+ is nil, otherwise the given value,
|
|
36
|
+
# which must be a positive integer no greater than TRANSACTION_INSTRUCTION_LIMIT.
|
|
37
|
+
#
|
|
38
|
+
# This is typically the first thing a custom MessagePacker does with the
|
|
39
|
+
# +max_instructions+ it receives in +pack_message_to_capacity+:
|
|
40
|
+
#
|
|
41
|
+
# pack_proc: ->(message, max_instructions) {
|
|
42
|
+
# max = InstructionPlans.resolve_max_instructions_per_transaction(max_instructions)
|
|
43
|
+
# InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, max)
|
|
44
|
+
# # ...
|
|
45
|
+
# }
|
|
46
|
+
#
|
|
47
|
+
# Raises INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION for an out-of-range value.
|
|
48
|
+
# Mirrors `resolveMaxInstructionsPerTransaction(maxInstructions)`.
|
|
49
|
+
sig { params(max_instructions: T.nilable(Integer)).returns(Integer) }
|
|
50
|
+
def resolve_max_instructions_per_transaction(max_instructions = nil)
|
|
51
|
+
return DEFAULT_MAX_INSTRUCTIONS_PER_TRANSACTION if max_instructions.nil?
|
|
52
|
+
|
|
53
|
+
if max_instructions <= 0 || max_instructions > TRANSACTION_INSTRUCTION_LIMIT
|
|
54
|
+
Kernel.raise SolanaError.new(
|
|
55
|
+
SolanaError::INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION,
|
|
56
|
+
{
|
|
57
|
+
max_instructions: max_instructions,
|
|
58
|
+
transaction_instruction_limit: TRANSACTION_INSTRUCTION_LIMIT
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
max_instructions
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Raises if a message holding +num_instructions+ instructions would exceed
|
|
67
|
+
# +max_instructions+. Use it in a custom MessagePacker before appending an instruction -
|
|
68
|
+
# passing the count the message would have after the append - so the planner knows to
|
|
69
|
+
# pack that instruction into another message.
|
|
70
|
+
#
|
|
71
|
+
# Raises INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED.
|
|
72
|
+
# Mirrors `assertMaxInstructionsPerTransaction(numInstructions, maxInstructions)`.
|
|
73
|
+
sig { params(num_instructions: Integer, max_instructions: Integer).void }
|
|
74
|
+
def assert_max_instructions_per_transaction(num_instructions, max_instructions)
|
|
75
|
+
return if num_instructions <= max_instructions
|
|
76
|
+
|
|
77
|
+
Kernel.raise SolanaError.new(
|
|
78
|
+
SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED,
|
|
79
|
+
{ max_instructions: max_instructions, num_instructions: num_instructions }
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Raises if a message cannot grow from +current_size+ to +next_size+ bytes without
|
|
84
|
+
# exceeding +size_limit+. Use it in a custom MessagePacker after appending the next
|
|
85
|
+
# instruction(s), so the planner knows to pack them into another message. It takes sizes
|
|
86
|
+
# rather than messages so callers who already computed them do not pay for it twice:
|
|
87
|
+
#
|
|
88
|
+
# next_message = TransactionMessages.append_instructions(message, [ix])
|
|
89
|
+
# InstructionPlans.assert_message_can_accommodate_size(
|
|
90
|
+
# current_size: Transactions.get_transaction_message_size(message),
|
|
91
|
+
# next_size: Transactions.get_transaction_message_size(next_message),
|
|
92
|
+
# size_limit: Transactions::TRANSACTION_SIZE_LIMIT
|
|
93
|
+
# )
|
|
94
|
+
#
|
|
95
|
+
# Raises INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN, reporting how many bytes
|
|
96
|
+
# were required and how many were free.
|
|
97
|
+
# Mirrors `assertMessageCanAccommodateSize({ currentSize, nextSize, sizeLimit })`.
|
|
98
|
+
sig { params(current_size: Integer, next_size: Integer, size_limit: Integer).void }
|
|
99
|
+
def assert_message_can_accommodate_size(current_size:, next_size:, size_limit:)
|
|
100
|
+
return if next_size <= size_limit
|
|
101
|
+
|
|
102
|
+
Kernel.raise SolanaError.new(
|
|
103
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
|
|
104
|
+
{ num_bytes_required: next_size - current_size, num_free_bytes: size_limit - current_size }
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Whether +error+, raised while packing instructions into a message, means the message
|
|
109
|
+
# cannot take them and a new candidate message is required (see
|
|
110
|
+
# MESSAGE_PACKER_ERRORS_THAT_REQUIRE_NEW_CANDIDATE). Any other error is unexpected and
|
|
111
|
+
# should propagate:
|
|
112
|
+
#
|
|
113
|
+
# begin
|
|
114
|
+
# message = packer.pack_message_to_capacity(message)
|
|
115
|
+
# rescue => e
|
|
116
|
+
# raise unless InstructionPlans.message_packer_error_that_requires_new_candidate?(e)
|
|
117
|
+
# message = packer.pack_message_to_capacity(create_new_message)
|
|
118
|
+
# end
|
|
119
|
+
#
|
|
120
|
+
# A custom packer refuses a message for any other reason by raising
|
|
121
|
+
# INSTRUCTION_PLANS__MESSAGE_REJECTED_BY_PACKER with a +reason:+.
|
|
122
|
+
# Mirrors `isMessagePackerErrorThatRequiresNewCandidate(error)`.
|
|
123
|
+
sig { params(error: T.anything).returns(T::Boolean) }
|
|
124
|
+
def message_packer_error_that_requires_new_candidate?(error)
|
|
125
|
+
case error
|
|
126
|
+
when SolanaError then MESSAGE_PACKER_ERRORS_THAT_REQUIRE_NEW_CANDIDATE.include?(error.code)
|
|
127
|
+
else false
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -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.
|
|
@@ -111,7 +111,7 @@ module Solana::Ruby::Kit
|
|
|
111
111
|
|
|
112
112
|
# ── Private helpers ────────────────────────────────────────────────────────
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
extend self
|
|
115
115
|
|
|
116
116
|
sig { params(plan: T.untyped, execute_fn: T.untyped, state: T.untyped).returns(T.untyped) }
|
|
117
117
|
|
|
@@ -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.
|
|
@@ -54,11 +54,11 @@ module Solana::Ruby::Kit
|
|
|
54
54
|
config_max_instructions_per_transaction = max_instructions_per_transaction
|
|
55
55
|
|
|
56
56
|
->(instruction_plan, max_instructions_per_transaction: nil) {
|
|
57
|
-
resolved_max = max_instructions_per_transaction || config_max_instructions_per_transaction
|
|
58
|
-
|
|
59
57
|
# Reject up front any configured maximum the transaction format could never satisfy,
|
|
60
58
|
# rather than discovering it mid-plan when a message fails to compile.
|
|
61
|
-
InstructionPlans.
|
|
59
|
+
resolved_max = InstructionPlans.resolve_max_instructions_per_transaction(
|
|
60
|
+
max_instructions_per_transaction || config_max_instructions_per_transaction
|
|
61
|
+
)
|
|
62
62
|
|
|
63
63
|
mutable = planner_traverse(
|
|
64
64
|
instruction_plan,
|
|
@@ -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
|
|
|
@@ -194,17 +194,14 @@ module Solana::Ruby::Kit
|
|
|
194
194
|
if Transactions.get_transaction_message_size(updated) <= Transactions::TRANSACTION_SIZE_LIMIT
|
|
195
195
|
InstructionPlans.assert_max_instructions_per_transaction(
|
|
196
196
|
updated.instructions.length,
|
|
197
|
-
|
|
197
|
+
ctx[:max_instructions_per_transaction]
|
|
198
198
|
)
|
|
199
199
|
candidate[:message] = updated
|
|
200
200
|
return candidate
|
|
201
201
|
end
|
|
202
202
|
rescue SolanaError => e
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED
|
|
206
|
-
].include?(e.code)
|
|
207
|
-
Kernel.raise
|
|
203
|
+
Kernel.raise unless InstructionPlans.message_packer_error_that_requires_new_candidate?(e)
|
|
204
|
+
# Try the next candidate.
|
|
208
205
|
end
|
|
209
206
|
end
|
|
210
207
|
nil
|
|
@@ -228,7 +225,7 @@ module Solana::Ruby::Kit
|
|
|
228
225
|
|
|
229
226
|
InstructionPlans.assert_max_instructions_per_transaction(
|
|
230
227
|
updated_msg.instructions.length,
|
|
231
|
-
|
|
228
|
+
ctx[:max_instructions_per_transaction]
|
|
232
229
|
)
|
|
233
230
|
|
|
234
231
|
updated_msg
|
|
@@ -253,7 +250,7 @@ module Solana::Ruby::Kit
|
|
|
253
250
|
end
|
|
254
251
|
InstructionPlans.assert_max_instructions_per_transaction(
|
|
255
252
|
updated.instructions.length,
|
|
256
|
-
|
|
253
|
+
ctx[:max_instructions_per_transaction]
|
|
257
254
|
)
|
|
258
255
|
updated
|
|
259
256
|
when :message_packer
|
|
@@ -263,7 +260,7 @@ module Solana::Ruby::Kit
|
|
|
263
260
|
msg = packer.pack_message_to_capacity(msg, max_instructions: ctx[:max_instructions_per_transaction])
|
|
264
261
|
InstructionPlans.assert_max_instructions_per_transaction(
|
|
265
262
|
msg.instructions.length,
|
|
266
|
-
|
|
263
|
+
ctx[:max_instructions_per_transaction]
|
|
267
264
|
)
|
|
268
265
|
end
|
|
269
266
|
msg
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# Mirrors @solana/instruction-plans.
|
|
5
5
|
# An InstructionPlan describes operations that may span multiple transactions.
|
|
6
|
-
require_relative 'instruction_plans/
|
|
6
|
+
require_relative 'instruction_plans/message_packer_errors'
|
|
7
7
|
require_relative 'instruction_plans/instruction_plan'
|
|
8
8
|
require_relative 'instruction_plans/transaction_plan'
|
|
9
9
|
require_relative 'instruction_plans/transaction_plan_result'
|
|
@@ -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) }
|