solana-ruby-kit 0.1.9 → 7.0.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/lib/solana/ruby/kit/errors.rb +24 -1
- data/lib/solana/ruby/kit/instruction_plans/instruction_plan.rb +34 -7
- data/lib/solana/ruby/kit/instruction_plans/max_instructions.rb +59 -0
- data/lib/solana/ruby/kit/instruction_plans/transaction_planner.rb +49 -9
- data/lib/solana/ruby/kit/instruction_plans.rb +1 -0
- data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +2 -0
- data/lib/solana/ruby/kit/transaction_introspection/compiled_transaction_message.rb +168 -0
- data/lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb +159 -0
- data/lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb +86 -0
- data/lib/solana/ruby/kit/transaction_introspection/get_instructions.rb +134 -0
- data/lib/solana/ruby/kit/transaction_introspection/loaded_addresses.rb +19 -0
- data/lib/solana/ruby/kit/transaction_introspection/types.rb +29 -0
- data/lib/solana/ruby/kit/transaction_introspection/walk_instructions.rb +60 -0
- data/lib/solana/ruby/kit/transaction_introspection.rb +13 -0
- data/lib/solana/ruby/kit/version.rb +1 -1
- data/lib/solana/ruby/kit.rb +2 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b2766067cfcd434e5c657e1fde1dab4b44b23880e0f514f924d28a94ea0ca4a
|
|
4
|
+
data.tar.gz: 32814612b342df38df079aaba08b8ea17c9128ac9f54279ab3abad622ba02492
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bcfbd20e6f610a47053cb385511931004590646ff04e0edf4587e5699df3db8bb148af2b706cf025e8399f26e4736ae9db35ebfd04d68be287df8302bc9f4de
|
|
7
|
+
data.tar.gz: d40b332b7cc427730b17b01a091c53815c596c0ac114bca93f52bed4d35775cd2b57ca5146893e2c94d46cebcbe0f51ccf98d2c6e2cad308cd51c2ff7b022d62
|
|
@@ -56,6 +56,10 @@ module Solana::Ruby::Kit
|
|
|
56
56
|
TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS
|
|
57
57
|
TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING
|
|
58
58
|
TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND
|
|
59
|
+
# context: { index: }
|
|
60
|
+
TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE
|
|
61
|
+
# context: { unsupported_version: }
|
|
62
|
+
TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED = :SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED
|
|
59
63
|
TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE = :SOLANA_ERROR__TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE
|
|
60
64
|
TRANSACTIONS__BLOCKHASH_NOT_FOUND = :SOLANA_ERROR__TRANSACTIONS__BLOCKHASH_NOT_FOUND
|
|
61
65
|
TRANSACTIONS__FAILED_TRANSACTION_PLAN = :SOLANA_ERROR__TRANSACTIONS__FAILED_TRANSACTION_PLAN
|
|
@@ -118,6 +122,16 @@ module Solana::Ruby::Kit
|
|
|
118
122
|
INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE = :SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE
|
|
119
123
|
INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN
|
|
120
124
|
INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN
|
|
125
|
+
# context: { max_instructions:, transaction_instruction_limit: }
|
|
126
|
+
INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION = :SOLANA_ERROR__INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION
|
|
127
|
+
# context: { max_instructions:, num_instructions: }
|
|
128
|
+
INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED = :SOLANA_ERROR__INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED
|
|
129
|
+
|
|
130
|
+
# ── Transaction introspection ────────────────────────────────────────────
|
|
131
|
+
TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION = :SOLANA_ERROR__TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION
|
|
132
|
+
TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE = :SOLANA_ERROR__TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE
|
|
133
|
+
# context: { needed_bytes: }
|
|
134
|
+
TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE = :SOLANA_ERROR__TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE
|
|
121
135
|
|
|
122
136
|
# ── Wallet Standard ───────────────────────────────────────────────────────
|
|
123
137
|
WALLET_STANDARD__INVALID_WIRE_FORMAT = :SOLANA_ERROR__WALLET_STANDARD__INVALID_WIRE_FORMAT
|
|
@@ -184,7 +198,9 @@ module Solana::Ruby::Kit
|
|
|
184
198
|
TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE => 'Transaction version %{version} is out of range',
|
|
185
199
|
TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS => 'Failed to decompile address lookup table contents',
|
|
186
200
|
TRANSACTIONS__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING => 'Failed to decompile transaction: fee payer is missing',
|
|
187
|
-
TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found',
|
|
201
|
+
TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND => 'Failed to decompile instruction: program address not found at index %{index}',
|
|
202
|
+
TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE => 'Failed to decompile instruction: account index %{index} is out of range',
|
|
203
|
+
TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED => 'Transaction version %{unsupported_version} is not supported',
|
|
188
204
|
TRANSACTIONS__SEND_TRANSACTION_PREFLIGHT_FAILURE => 'Transaction simulation failed: %{message}',
|
|
189
205
|
TRANSACTIONS__BLOCKHASH_NOT_FOUND => 'Blockhash not found',
|
|
190
206
|
TRANSACTIONS__FAILED_TRANSACTION_PLAN => 'Failed to execute transaction plan',
|
|
@@ -245,6 +261,13 @@ module Solana::Ruby::Kit
|
|
|
245
261
|
INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE => 'Message packer is already complete; no more instructions to pack',
|
|
246
262
|
INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN => 'Instruction plan is empty and produced no transaction messages',
|
|
247
263
|
INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
|
|
264
|
+
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.',
|
|
265
|
+
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.',
|
|
266
|
+
|
|
267
|
+
# Transaction introspection
|
|
268
|
+
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.",
|
|
269
|
+
TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE => 'Unrecognized getTransaction response shape; expected a base64, base58, or json-encoded transaction.',
|
|
270
|
+
TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE => 'Malformed compiled transaction message: expected at least %{needed_bytes} byte(s)',
|
|
248
271
|
|
|
249
272
|
# Subscribable
|
|
250
273
|
SUBSCRIBABLE__RETRY_NOT_SUPPORTED => 'This reactive store does not support retry(); use create_reactive_store_from_data_publisher_factory for a retryable store',
|
|
@@ -5,6 +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 'max_instructions'
|
|
8
9
|
|
|
9
10
|
module Solana::Ruby::Kit
|
|
10
11
|
module InstructionPlans
|
|
@@ -68,9 +69,17 @@ module Solana::Ruby::Kit
|
|
|
68
69
|
# Packs as many instructions as possible into +message+ and returns the
|
|
69
70
|
# updated message. Raises SolanaError if the message is too small or the
|
|
70
71
|
# packer is already done.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
#
|
|
73
|
+
# +max_instructions+ caps the number of top-level instructions allowed in the
|
|
74
|
+
# returned message (defaults to 16; must be a positive integer no greater than 64).
|
|
75
|
+
sig do
|
|
76
|
+
params(
|
|
77
|
+
message: TransactionMessages::TransactionMessage,
|
|
78
|
+
max_instructions: T.nilable(Integer)
|
|
79
|
+
).returns(TransactionMessages::TransactionMessage)
|
|
80
|
+
end
|
|
81
|
+
def pack_message_to_capacity(message, max_instructions: nil)
|
|
82
|
+
@pack_proc.call(message, max_instructions)
|
|
74
83
|
end
|
|
75
84
|
end
|
|
76
85
|
|
|
@@ -122,11 +131,15 @@ module Solana::Ruby::Kit
|
|
|
122
131
|
offset = T.let(0, Integer)
|
|
123
132
|
MessagePacker.new(
|
|
124
133
|
done_proc: -> { offset >= total_length },
|
|
125
|
-
pack_proc: ->(message) {
|
|
134
|
+
pack_proc: ->(message, max_instructions) {
|
|
126
135
|
if offset >= total_length
|
|
127
136
|
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
|
|
128
137
|
end
|
|
129
138
|
|
|
139
|
+
InstructionPlans.assert_valid_max_instructions_per_transaction(max_instructions)
|
|
140
|
+
resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
|
|
141
|
+
InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)
|
|
142
|
+
|
|
130
143
|
base_ix = get_instruction.call(offset, 0)
|
|
131
144
|
with_base = TransactionMessages.append_instructions(message, [base_ix])
|
|
132
145
|
base_size = Transactions.get_transaction_message_size(with_base)
|
|
@@ -164,18 +177,30 @@ module Solana::Ruby::Kit
|
|
|
164
177
|
idx = T.let(0, Integer)
|
|
165
178
|
MessagePacker.new(
|
|
166
179
|
done_proc: -> { idx >= instructions.length },
|
|
167
|
-
pack_proc: ->(message) {
|
|
180
|
+
pack_proc: ->(message, max_instructions) {
|
|
168
181
|
if idx >= instructions.length
|
|
169
182
|
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
|
|
170
183
|
end
|
|
171
184
|
|
|
185
|
+
InstructionPlans.assert_valid_max_instructions_per_transaction(max_instructions)
|
|
186
|
+
resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
|
|
187
|
+
InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)
|
|
188
|
+
|
|
172
189
|
original_size = Transactions.get_transaction_message_size(message)
|
|
173
190
|
packed = T.let(message, TransactionMessages::TransactionMessage)
|
|
174
191
|
start_idx = idx
|
|
175
192
|
|
|
176
193
|
(idx...instructions.length).each do |i|
|
|
177
|
-
|
|
178
|
-
|
|
194
|
+
# Stop once the message is full, before compiling a message that would exceed
|
|
195
|
+
# the instruction limit (which would raise). The assertion above guarantees at
|
|
196
|
+
# least the first instruction fits, so reaching the limit here is a graceful stop.
|
|
197
|
+
if packed.instructions.length >= resolved_max
|
|
198
|
+
idx = i
|
|
199
|
+
return packed
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
next_packed = TransactionMessages.append_instructions(packed, [instructions[i]])
|
|
203
|
+
size = Transactions.get_transaction_message_size(next_packed)
|
|
179
204
|
|
|
180
205
|
if size > Transactions::TRANSACTION_SIZE_LIMIT
|
|
181
206
|
if i == start_idx
|
|
@@ -190,6 +215,8 @@ module Solana::Ruby::Kit
|
|
|
190
215
|
idx = i
|
|
191
216
|
return packed
|
|
192
217
|
end
|
|
218
|
+
|
|
219
|
+
packed = next_packed
|
|
193
220
|
end
|
|
194
221
|
|
|
195
222
|
idx = instructions.length
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Resolves the effective maximum number of instructions allowed in a transaction message,
|
|
22
|
+
# falling back to the default when no value is provided.
|
|
23
|
+
# Mirrors `resolveMaxInstructions(maxInstructions)`.
|
|
24
|
+
sig { params(max_instructions: T.nilable(Integer)).returns(Integer) }
|
|
25
|
+
def resolve_max_instructions(max_instructions)
|
|
26
|
+
max_instructions || DEFAULT_MAX_INSTRUCTIONS_PER_TRANSACTION
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Asserts that a configured maximum number of instructions per transaction is valid.
|
|
30
|
+
# +nil+ is allowed and falls back to the default.
|
|
31
|
+
# Mirrors `assertValidMaxInstructionsPerTransaction(maxInstructions)`.
|
|
32
|
+
sig { params(max_instructions: T.nilable(Integer)).void }
|
|
33
|
+
def assert_valid_max_instructions_per_transaction(max_instructions)
|
|
34
|
+
return if max_instructions.nil?
|
|
35
|
+
|
|
36
|
+
if max_instructions <= 0 || max_instructions > TRANSACTION_INSTRUCTION_LIMIT
|
|
37
|
+
Kernel.raise SolanaError.new(
|
|
38
|
+
SolanaError::INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION,
|
|
39
|
+
{
|
|
40
|
+
max_instructions: max_instructions,
|
|
41
|
+
transaction_instruction_limit: TRANSACTION_INSTRUCTION_LIMIT
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raises if +num_instructions+ exceeds +max_instructions+.
|
|
48
|
+
# Mirrors `assertMaxInstructionsPerTransaction(numInstructions, maxInstructions)`.
|
|
49
|
+
sig { params(num_instructions: Integer, max_instructions: Integer).void }
|
|
50
|
+
def assert_max_instructions_per_transaction(num_instructions, max_instructions)
|
|
51
|
+
return if num_instructions <= max_instructions
|
|
52
|
+
|
|
53
|
+
Kernel.raise SolanaError.new(
|
|
54
|
+
SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED,
|
|
55
|
+
{ max_instructions: max_instructions, num_instructions: num_instructions }
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -23,27 +23,45 @@ module Solana::Ruby::Kit
|
|
|
23
23
|
# Called after instructions are appended to a message. Must return the
|
|
24
24
|
# (possibly further updated) message. Defaults to identity.
|
|
25
25
|
#
|
|
26
|
-
#
|
|
26
|
+
# max_instructions_per_transaction: Integer (optional)
|
|
27
|
+
# Maximum number of top-level instructions allowed in each planned transaction.
|
|
28
|
+
# Must be a positive integer no greater than 64. Defaults to 16. Can be overridden
|
|
29
|
+
# per-call via the returned planner's `max_instructions_per_transaction:` kwarg.
|
|
30
|
+
#
|
|
31
|
+
# The returned planner is a lambda:
|
|
32
|
+
# planner.call(instruction_plan, max_instructions_per_transaction: nil) -> TransactionPlan
|
|
27
33
|
#
|
|
28
34
|
# Mirrors `createTransactionPlanner(config)` from @solana/instruction-plans.
|
|
29
35
|
# NOTE: Ruby translation is synchronous; abort-signal support is omitted.
|
|
30
36
|
sig do
|
|
31
37
|
params(
|
|
32
|
-
create_transaction_message:
|
|
33
|
-
on_transaction_message_updated:
|
|
38
|
+
create_transaction_message: T.untyped,
|
|
39
|
+
on_transaction_message_updated: T.untyped,
|
|
40
|
+
max_instructions_per_transaction: T.nilable(Integer)
|
|
34
41
|
).returns(T.untyped)
|
|
35
42
|
end
|
|
36
43
|
def create_transaction_planner(
|
|
37
44
|
create_transaction_message:,
|
|
38
|
-
on_transaction_message_updated: ->(msg) { msg }
|
|
45
|
+
on_transaction_message_updated: ->(msg) { msg },
|
|
46
|
+
max_instructions_per_transaction: nil
|
|
39
47
|
)
|
|
40
48
|
ctx = {
|
|
41
49
|
create_transaction_message: create_transaction_message,
|
|
42
50
|
on_transaction_message_updated: on_transaction_message_updated
|
|
43
51
|
}
|
|
52
|
+
config_max_instructions_per_transaction = max_instructions_per_transaction
|
|
53
|
+
|
|
54
|
+
->(instruction_plan, max_instructions_per_transaction: nil) {
|
|
55
|
+
resolved_max = max_instructions_per_transaction || config_max_instructions_per_transaction
|
|
56
|
+
|
|
57
|
+
# Reject up front any configured maximum the transaction format could never satisfy,
|
|
58
|
+
# rather than discovering it mid-plan when a message fails to compile.
|
|
59
|
+
InstructionPlans.assert_valid_max_instructions_per_transaction(resolved_max)
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
mutable = planner_traverse(
|
|
62
|
+
instruction_plan,
|
|
63
|
+
ctx.merge(parent: nil, parent_candidates: [], max_instructions_per_transaction: resolved_max)
|
|
64
|
+
)
|
|
47
65
|
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN) unless mutable
|
|
48
66
|
planner_freeze(mutable)
|
|
49
67
|
}
|
|
@@ -136,7 +154,7 @@ module Solana::Ruby::Kit
|
|
|
136
154
|
candidates = ctx[:parent_candidates].dup
|
|
137
155
|
|
|
138
156
|
until packer.done?
|
|
139
|
-
predicate = ->(msg) { packer.pack_message_to_capacity(msg) }
|
|
157
|
+
predicate = ->(msg) { packer.pack_message_to_capacity(msg, max_instructions: ctx[:max_instructions_per_transaction]) }
|
|
140
158
|
candidate = planner_select_and_mutate_candidate(ctx, candidates, &predicate)
|
|
141
159
|
unless candidate
|
|
142
160
|
msg = planner_create_new_message(ctx, &predicate)
|
|
@@ -162,11 +180,18 @@ module Solana::Ruby::Kit
|
|
|
162
180
|
begin
|
|
163
181
|
updated = ctx[:on_transaction_message_updated].call(predicate.call(candidate[:message]))
|
|
164
182
|
if Transactions.get_transaction_message_size(updated) <= Transactions::TRANSACTION_SIZE_LIMIT
|
|
183
|
+
InstructionPlans.assert_max_instructions_per_transaction(
|
|
184
|
+
updated.instructions.length,
|
|
185
|
+
InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
|
|
186
|
+
)
|
|
165
187
|
candidate[:message] = updated
|
|
166
188
|
return candidate
|
|
167
189
|
end
|
|
168
190
|
rescue SolanaError => e
|
|
169
|
-
next if
|
|
191
|
+
next if [
|
|
192
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
|
|
193
|
+
SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED
|
|
194
|
+
].include?(e.code)
|
|
170
195
|
Kernel.raise
|
|
171
196
|
end
|
|
172
197
|
end
|
|
@@ -187,6 +212,11 @@ module Solana::Ruby::Kit
|
|
|
187
212
|
)
|
|
188
213
|
end
|
|
189
214
|
|
|
215
|
+
InstructionPlans.assert_max_instructions_per_transaction(
|
|
216
|
+
updated_msg.instructions.length,
|
|
217
|
+
InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
|
|
218
|
+
)
|
|
219
|
+
|
|
190
220
|
updated_msg
|
|
191
221
|
end
|
|
192
222
|
|
|
@@ -205,11 +235,21 @@ module Solana::Ruby::Kit
|
|
|
205
235
|
num_free_bytes: Transactions::TRANSACTION_SIZE_LIMIT - base_size }
|
|
206
236
|
)
|
|
207
237
|
end
|
|
238
|
+
InstructionPlans.assert_max_instructions_per_transaction(
|
|
239
|
+
updated.instructions.length,
|
|
240
|
+
InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
|
|
241
|
+
)
|
|
208
242
|
updated
|
|
209
243
|
when :message_packer
|
|
210
244
|
packer = plan.get_message_packer.call
|
|
211
245
|
msg = message
|
|
212
|
-
|
|
246
|
+
until packer.done?
|
|
247
|
+
msg = packer.pack_message_to_capacity(msg, max_instructions: ctx[:max_instructions_per_transaction])
|
|
248
|
+
InstructionPlans.assert_max_instructions_per_transaction(
|
|
249
|
+
msg.instructions.length,
|
|
250
|
+
InstructionPlans.resolve_max_instructions(ctx[:max_instructions_per_transaction])
|
|
251
|
+
)
|
|
252
|
+
end
|
|
213
253
|
msg
|
|
214
254
|
else
|
|
215
255
|
Kernel.raise SolanaError.new(
|
|
@@ -3,6 +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/max_instructions'
|
|
6
7
|
require_relative 'instruction_plans/instruction_plan'
|
|
7
8
|
require_relative 'instruction_plans/transaction_plan'
|
|
8
9
|
require_relative 'instruction_plans/transaction_plan_result'
|
|
@@ -12,6 +12,7 @@ module Solana::Ruby::Kit
|
|
|
12
12
|
:activated_stake,
|
|
13
13
|
:epoch_vote_account,
|
|
14
14
|
:commission,
|
|
15
|
+
:inflation_rewards_commission_bps,
|
|
15
16
|
:last_vote,
|
|
16
17
|
:epoch_credits,
|
|
17
18
|
:root_slot,
|
|
@@ -45,6 +46,7 @@ module Solana::Ruby::Kit
|
|
|
45
46
|
activated_stake: Kernel.Integer(v['activatedStake']),
|
|
46
47
|
epoch_vote_account: v['epochVoteAccount'],
|
|
47
48
|
commission: Kernel.Integer(v['commission']),
|
|
49
|
+
inflation_rewards_commission_bps: v['inflationRewardsCommissionBps'] ? Kernel.Integer(v['inflationRewardsCommissionBps']) : nil,
|
|
48
50
|
last_vote: Kernel.Integer(v['lastVote']),
|
|
49
51
|
epoch_credits: v['epochCredits'],
|
|
50
52
|
root_slot: v['rootSlot'] ? Kernel.Integer(v['rootSlot']) : nil
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../addresses/address'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
|
|
7
|
+
module Solana::Ruby::Kit
|
|
8
|
+
module TransactionIntrospection
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
# The signer/readonly account counts from a compiled transaction message.
|
|
12
|
+
# Mirrors the `header` field of TypeScript's `CompiledTransactionMessage`.
|
|
13
|
+
class CompiledTransactionMessageHeader < T::Struct
|
|
14
|
+
const :num_signer_accounts, Integer
|
|
15
|
+
const :num_readonly_signer_accounts, Integer
|
|
16
|
+
const :num_readonly_non_signer_accounts, Integer
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A single instruction inside a compiled transaction message, with account
|
|
20
|
+
# indices and data still unresolved (see GetInstructions for resolution).
|
|
21
|
+
class CompiledInstruction < T::Struct
|
|
22
|
+
const :program_address_index, Integer
|
|
23
|
+
const :account_indices, T::Array[Integer]
|
|
24
|
+
const :data, String # binary; may be empty
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A decoded transaction message: the account roles, static account list,
|
|
28
|
+
# instructions, and lifetime token (recent blockhash or durable nonce value).
|
|
29
|
+
#
|
|
30
|
+
# Mirrors TypeScript's `CompiledTransactionMessage & CompiledTransactionMessageWithLifetime`.
|
|
31
|
+
# Ruby normalizes legacy/v0/v1 instruction representations into a single
|
|
32
|
+
# `instructions` array (TypeScript keeps v1's headers/payloads split) since
|
|
33
|
+
# nothing downstream needs the raw per-version shape.
|
|
34
|
+
class CompiledTransactionMessage < T::Struct
|
|
35
|
+
const :version, T.untyped # :legacy or Integer (0 or 1)
|
|
36
|
+
const :header, CompiledTransactionMessageHeader
|
|
37
|
+
const :static_accounts, T::Array[Addresses::Address]
|
|
38
|
+
const :instructions, T::Array[CompiledInstruction]
|
|
39
|
+
const :lifetime_token, String # base58-encoded blockhash or nonce value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# Decodes legacy or v0 compiled transaction message bytes (the
|
|
45
|
+
# `message_bytes` of a `Transactions::Transaction`) into a
|
|
46
|
+
# CompiledTransactionMessage.
|
|
47
|
+
#
|
|
48
|
+
# v1 messages use a different (message-first) wire envelope and are not
|
|
49
|
+
# decoded here — Ruby's wire compiler (`Transactions.compile_transaction_message`)
|
|
50
|
+
# does not yet produce v1 transactions either, so there is nothing to
|
|
51
|
+
# round-trip. Raises SolanaError::TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED
|
|
52
|
+
# for any version this decoder cannot handle.
|
|
53
|
+
#
|
|
54
|
+
# Mirrors `getCompiledTransactionMessageDecoder()` from @solana/transaction-messages.
|
|
55
|
+
sig { params(message_bytes: String).returns(CompiledTransactionMessage) }
|
|
56
|
+
def decode_compiled_transaction_message(message_bytes)
|
|
57
|
+
bytes = message_bytes.b
|
|
58
|
+
offset = 0
|
|
59
|
+
|
|
60
|
+
first_byte = bytes.getbyte(0).to_i
|
|
61
|
+
if (first_byte & 0x80) != 0
|
|
62
|
+
version = first_byte & 0x7f
|
|
63
|
+
unless version == 0
|
|
64
|
+
Kernel.raise SolanaError.new(
|
|
65
|
+
SolanaError::TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED,
|
|
66
|
+
{ unsupported_version: version }
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
offset += 1
|
|
70
|
+
else
|
|
71
|
+
version = :legacy
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
num_signer_accounts, offset = read_byte(bytes, offset)
|
|
75
|
+
num_readonly_signer_accounts, offset = read_byte(bytes, offset)
|
|
76
|
+
num_readonly_non_signer_accounts, offset = read_byte(bytes, offset)
|
|
77
|
+
|
|
78
|
+
num_accounts, offset = decode_compact_u16(bytes, offset)
|
|
79
|
+
static_accounts = T.let([], T::Array[Addresses::Address])
|
|
80
|
+
num_accounts.times do
|
|
81
|
+
static_accounts << Addresses.address(Addresses.encode_address(read_bytes(bytes, offset, 32)))
|
|
82
|
+
offset += 32
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
lifetime_token = Addresses.encode_address(read_bytes(bytes, offset, 32))
|
|
86
|
+
offset += 32
|
|
87
|
+
|
|
88
|
+
num_instructions, offset = decode_compact_u16(bytes, offset)
|
|
89
|
+
instructions = T.let([], T::Array[CompiledInstruction])
|
|
90
|
+
num_instructions.times do
|
|
91
|
+
program_address_index, offset = read_byte(bytes, offset)
|
|
92
|
+
|
|
93
|
+
num_ix_accounts, offset = decode_compact_u16(bytes, offset)
|
|
94
|
+
account_indices = T.let([], T::Array[Integer])
|
|
95
|
+
num_ix_accounts.times do
|
|
96
|
+
index, offset = read_byte(bytes, offset)
|
|
97
|
+
account_indices << index
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
data_length, offset = decode_compact_u16(bytes, offset)
|
|
101
|
+
data = read_bytes(bytes, offset, data_length)
|
|
102
|
+
offset += data_length
|
|
103
|
+
|
|
104
|
+
instructions << CompiledInstruction.new(
|
|
105
|
+
program_address_index: program_address_index,
|
|
106
|
+
account_indices: account_indices,
|
|
107
|
+
data: data
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
CompiledTransactionMessage.new(
|
|
112
|
+
version: version,
|
|
113
|
+
header: CompiledTransactionMessageHeader.new(
|
|
114
|
+
num_signer_accounts: num_signer_accounts,
|
|
115
|
+
num_readonly_signer_accounts: num_readonly_signer_accounts,
|
|
116
|
+
num_readonly_non_signer_accounts: num_readonly_non_signer_accounts
|
|
117
|
+
),
|
|
118
|
+
static_accounts: static_accounts,
|
|
119
|
+
instructions: instructions,
|
|
120
|
+
lifetime_token: lifetime_token
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
sig { params(bytes: String, offset: Integer, length: Integer).returns(String) }
|
|
127
|
+
def read_bytes(bytes, offset, length)
|
|
128
|
+
slice = bytes[offset, length]
|
|
129
|
+
if slice.nil? || slice.bytesize != length
|
|
130
|
+
Kernel.raise SolanaError.new(
|
|
131
|
+
SolanaError::TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE,
|
|
132
|
+
{ needed_bytes: offset + length }
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
slice.b
|
|
136
|
+
end
|
|
137
|
+
private_class_method :read_bytes
|
|
138
|
+
|
|
139
|
+
sig { params(bytes: String, offset: Integer).returns([Integer, Integer]) }
|
|
140
|
+
def read_byte(bytes, offset)
|
|
141
|
+
byte = bytes.getbyte(offset)
|
|
142
|
+
if byte.nil?
|
|
143
|
+
Kernel.raise SolanaError.new(
|
|
144
|
+
SolanaError::TRANSACTION_INTROSPECTION__MALFORMED_COMPILED_MESSAGE,
|
|
145
|
+
{ needed_bytes: offset + 1 }
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
[byte, offset + 1]
|
|
149
|
+
end
|
|
150
|
+
private_class_method :read_byte
|
|
151
|
+
|
|
152
|
+
# Reads a Solana compact-u16 from +bytes+ starting at +offset+.
|
|
153
|
+
# Returns [decoded_integer, next_offset].
|
|
154
|
+
sig { params(bytes: String, offset: Integer).returns([Integer, Integer]) }
|
|
155
|
+
def decode_compact_u16(bytes, offset)
|
|
156
|
+
value = 0
|
|
157
|
+
shift = 0
|
|
158
|
+
Kernel.loop do
|
|
159
|
+
byte, offset = read_byte(bytes, offset)
|
|
160
|
+
value |= (byte & 0x7f) << shift
|
|
161
|
+
shift += 7
|
|
162
|
+
break unless (byte & 0x80) != 0
|
|
163
|
+
end
|
|
164
|
+
[value, offset]
|
|
165
|
+
end
|
|
166
|
+
private_class_method :decode_compact_u16
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'base64'
|
|
5
|
+
require_relative '../addresses'
|
|
6
|
+
require_relative '../encoding/base58'
|
|
7
|
+
require_relative '../errors'
|
|
8
|
+
require_relative '../transactions/transaction'
|
|
9
|
+
require_relative '../wallet_standard'
|
|
10
|
+
require_relative 'compiled_transaction_message'
|
|
11
|
+
require_relative 'loaded_addresses'
|
|
12
|
+
|
|
13
|
+
module Solana::Ruby::Kit
|
|
14
|
+
module TransactionIntrospection
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
# The result of decoding a `getTransaction` response: the
|
|
18
|
+
# CompiledTransactionMessage, the loaded ALT addresses pulled from `meta`
|
|
19
|
+
# (if any), and — for `'base64'` and `'base58'` responses — the
|
|
20
|
+
# re-encodable wire-format Transactions::Transaction.
|
|
21
|
+
#
|
|
22
|
+
# `transaction` is nil for `encoding: 'json'` responses: the server has
|
|
23
|
+
# already decompiled the wire format, so there are no message bytes to
|
|
24
|
+
# round-trip. Fetch with `encoding: 'base64'` if you need a re-encodable
|
|
25
|
+
# Transaction.
|
|
26
|
+
#
|
|
27
|
+
# Mirrors `DecodedRpcTransaction` from @solana/transaction-introspection.
|
|
28
|
+
class DecodedRpcTransaction < T::Struct
|
|
29
|
+
const :compiled_message, CompiledTransactionMessage
|
|
30
|
+
const :loaded_addresses, LoadedAddresses
|
|
31
|
+
const :transaction, T.nilable(Transactions::Transaction)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# Decodes a `getTransaction` response (any of `encoding: 'base64'`,
|
|
37
|
+
# `'base58'`, or `'json'`) into a CompiledTransactionMessage plus, for
|
|
38
|
+
# `'base64'` and `'base58'`, a re-encodable Transactions::Transaction.
|
|
39
|
+
# The JSON path does not produce a Transaction: the server has already
|
|
40
|
+
# decompiled the wire format, so there are no message bytes to carry.
|
|
41
|
+
#
|
|
42
|
+
# +rpc_tx+ is the raw (String-keyed) JSON hash returned by
|
|
43
|
+
# `Rpc::Api::GetTransaction#get_transaction`.
|
|
44
|
+
#
|
|
45
|
+
# `'jsonParsed'` is not supported — its instructions arrive pre-parsed by
|
|
46
|
+
# the server and lack raw bytes, so they cannot be round-tripped. Passing
|
|
47
|
+
# a `'jsonParsed'` response raises
|
|
48
|
+
# SolanaError::TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION;
|
|
49
|
+
# any other unrecognized input raises
|
|
50
|
+
# SolanaError::TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE.
|
|
51
|
+
#
|
|
52
|
+
# A transaction version this decoder cannot handle (anything but legacy
|
|
53
|
+
# or v0) raises SolanaError::TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED —
|
|
54
|
+
# Ruby's wire compiler does not produce v1 transactions either, so there
|
|
55
|
+
# is nothing to decompile for that version yet.
|
|
56
|
+
#
|
|
57
|
+
# Mirrors `decodeTransactionFromRpcResponse()`.
|
|
58
|
+
sig { params(rpc_tx: T::Hash[String, T.untyped]).returns(DecodedRpcTransaction) }
|
|
59
|
+
def decode_transaction_from_rpc_response(rpc_tx)
|
|
60
|
+
tx_field = rpc_tx['transaction']
|
|
61
|
+
|
|
62
|
+
if tx_field.is_a?(Array)
|
|
63
|
+
encoding = tx_field[1]
|
|
64
|
+
return decode_from_base64(rpc_tx) if encoding == 'base64'
|
|
65
|
+
return decode_from_base58(rpc_tx) if encoding == 'base58'
|
|
66
|
+
elsif tx_field.is_a?(Hash) && tx_field['message'].is_a?(Hash)
|
|
67
|
+
# An `encoding: 'json'` message carries the compiled-message `header`
|
|
68
|
+
# (signer/readonly counts). A `jsonParsed` message has no `header` —
|
|
69
|
+
# the server has already resolved account roles onto `accountKeys` —
|
|
70
|
+
# so checking for it distinguishes the two encodings.
|
|
71
|
+
return decode_from_json(rpc_tx) if tx_field['message']['header'].is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
Kernel.raise SolanaError.new(SolanaError::TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Kernel.raise SolanaError.new(SolanaError::TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
sig { params(meta: T.untyped).returns(LoadedAddresses) }
|
|
82
|
+
def get_loaded_addresses(meta)
|
|
83
|
+
loaded = meta.is_a?(Hash) ? meta['loadedAddresses'] : nil
|
|
84
|
+
return EMPTY_LOADED_ADDRESSES unless loaded.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
LoadedAddresses.new(
|
|
87
|
+
readonly: (loaded['readonly'] || []).map { |a| Addresses.address(a) },
|
|
88
|
+
writable: (loaded['writable'] || []).map { |a| Addresses.address(a) }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
private_class_method :get_loaded_addresses
|
|
92
|
+
|
|
93
|
+
sig { params(rpc_tx: T::Hash[String, T.untyped]).returns(DecodedRpcTransaction) }
|
|
94
|
+
def decode_from_base64(rpc_tx)
|
|
95
|
+
b64 = rpc_tx['transaction'][0]
|
|
96
|
+
transaction = WalletStandard.decode_wire_transaction(b64)
|
|
97
|
+
DecodedRpcTransaction.new(
|
|
98
|
+
compiled_message: decode_compiled_transaction_message(transaction.message_bytes),
|
|
99
|
+
loaded_addresses: get_loaded_addresses(rpc_tx['meta']),
|
|
100
|
+
transaction: transaction
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
private_class_method :decode_from_base64
|
|
104
|
+
|
|
105
|
+
sig { params(rpc_tx: T::Hash[String, T.untyped]).returns(DecodedRpcTransaction) }
|
|
106
|
+
def decode_from_base58(rpc_tx)
|
|
107
|
+
b58 = rpc_tx['transaction'][0]
|
|
108
|
+
transaction = WalletStandard.decode_wire_transaction(Encoding::Base58.decode(b58))
|
|
109
|
+
DecodedRpcTransaction.new(
|
|
110
|
+
compiled_message: decode_compiled_transaction_message(transaction.message_bytes),
|
|
111
|
+
loaded_addresses: get_loaded_addresses(rpc_tx['meta']),
|
|
112
|
+
transaction: transaction
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
private_class_method :decode_from_base58
|
|
116
|
+
|
|
117
|
+
sig { params(rpc_tx: T::Hash[String, T.untyped]).returns(DecodedRpcTransaction) }
|
|
118
|
+
def decode_from_json(rpc_tx)
|
|
119
|
+
message = rpc_tx['transaction']['message']
|
|
120
|
+
|
|
121
|
+
header = CompiledTransactionMessageHeader.new(
|
|
122
|
+
num_signer_accounts: message['header']['numRequiredSignatures'],
|
|
123
|
+
num_readonly_signer_accounts: message['header']['numReadonlySignedAccounts'],
|
|
124
|
+
num_readonly_non_signer_accounts: message['header']['numReadonlyUnsignedAccounts']
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
instructions = message['instructions'].map do |ix|
|
|
128
|
+
data_b58 = ix['data']
|
|
129
|
+
CompiledInstruction.new(
|
|
130
|
+
program_address_index: ix['programIdIndex'],
|
|
131
|
+
account_indices: ix['accounts'] || [],
|
|
132
|
+
data: data_b58.nil? || data_b58.empty? ? ''.b : Encoding::Base58.decode(data_b58)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The envelope only carries `version` when `maxSupportedTransactionVersion`
|
|
137
|
+
# was set on the request; otherwise the response is necessarily legacy.
|
|
138
|
+
version = rpc_tx.key?('version') ? rpc_tx['version'] : :legacy
|
|
139
|
+
version = :legacy if version == 'legacy'
|
|
140
|
+
|
|
141
|
+
compiled_message = CompiledTransactionMessage.new(
|
|
142
|
+
version: version,
|
|
143
|
+
header: header,
|
|
144
|
+
static_accounts: message['accountKeys'].map { |a| Addresses.address(a) },
|
|
145
|
+
instructions: instructions,
|
|
146
|
+
# For durable-nonce transactions, `recentBlockhash` is the nonce value, not a
|
|
147
|
+
# blockhash — either way it is the message's lifetime token.
|
|
148
|
+
lifetime_token: message['recentBlockhash']
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
DecodedRpcTransaction.new(
|
|
152
|
+
compiled_message: compiled_message,
|
|
153
|
+
loaded_addresses: get_loaded_addresses(rpc_tx['meta']),
|
|
154
|
+
transaction: nil
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
private_class_method :decode_from_json
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../encoding/base58'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
require_relative '../instructions/instruction'
|
|
7
|
+
require_relative 'types'
|
|
8
|
+
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module TransactionIntrospection
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Returns the inner instructions in a `getTransaction` response as
|
|
16
|
+
# TracedInstructions.
|
|
17
|
+
#
|
|
18
|
+
# The RPC returns inner instructions in a different shape from the wire
|
|
19
|
+
# format: indices reference the same flat account list as the outer
|
|
20
|
+
# instructions, but `data` is a base58-encoded string. This decodes the
|
|
21
|
+
# data, resolves the indices against the supplied AccountMeta list, and
|
|
22
|
+
# tags each instruction with an `inner` trace.
|
|
23
|
+
#
|
|
24
|
+
# +meta+ is the raw (String-keyed) `meta` hash from a `getTransaction`
|
|
25
|
+
# response. Returns an empty array if it carries no `innerInstructions`.
|
|
26
|
+
#
|
|
27
|
+
# Raises if any `programIdIndex` or account index falls outside the
|
|
28
|
+
# supplied `account_metas` list.
|
|
29
|
+
#
|
|
30
|
+
# Mirrors `getInnerInstructionsFromMeta()`.
|
|
31
|
+
sig do
|
|
32
|
+
params(
|
|
33
|
+
meta: T.nilable(T::Hash[String, T.untyped]),
|
|
34
|
+
account_metas: T::Array[Instructions::AccountMeta]
|
|
35
|
+
).returns(T::Array[TracedInstruction])
|
|
36
|
+
end
|
|
37
|
+
def get_inner_instructions_from_meta(meta, account_metas)
|
|
38
|
+
groups = meta && meta['innerInstructions']
|
|
39
|
+
return [] unless groups
|
|
40
|
+
|
|
41
|
+
result = T.let([], T::Array[TracedInstruction])
|
|
42
|
+
|
|
43
|
+
groups.each do |group|
|
|
44
|
+
outer_index = group['index']
|
|
45
|
+
|
|
46
|
+
group['instructions'].each_with_index do |ix, inner_index|
|
|
47
|
+
program_meta = account_metas[ix['programIdIndex']]
|
|
48
|
+
if program_meta.nil?
|
|
49
|
+
Kernel.raise SolanaError.new(
|
|
50
|
+
SolanaError::TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
|
|
51
|
+
{ index: ix['programIdIndex'] }
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
accounts = (ix['accounts'] || []).map do |i|
|
|
56
|
+
account_meta = account_metas[i]
|
|
57
|
+
if account_meta.nil?
|
|
58
|
+
Kernel.raise SolanaError.new(
|
|
59
|
+
SolanaError::TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE,
|
|
60
|
+
{ index: i }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
account_meta
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
data_b58 = ix['data']
|
|
67
|
+
data = data_b58.nil? || data_b58.empty? ? nil : Encoding::Base58.decode(data_b58)
|
|
68
|
+
|
|
69
|
+
trace = { kind: :inner, outer_index: outer_index, inner_index: inner_index }
|
|
70
|
+
trace[:stack_height] = ix['stackHeight'] unless ix['stackHeight'].nil?
|
|
71
|
+
|
|
72
|
+
result << TracedInstruction.new(
|
|
73
|
+
instruction: Instructions::Instruction.new(
|
|
74
|
+
program_address: program_meta.address,
|
|
75
|
+
accounts: accounts.empty? ? nil : accounts,
|
|
76
|
+
data: data
|
|
77
|
+
),
|
|
78
|
+
trace: trace
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative '../instructions/instruction'
|
|
6
|
+
require_relative '../instructions/roles'
|
|
7
|
+
require_relative 'compiled_transaction_message'
|
|
8
|
+
require_relative 'loaded_addresses'
|
|
9
|
+
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module TransactionIntrospection
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Builds the full ordered list of AccountMetas for a compiled transaction
|
|
17
|
+
# message.
|
|
18
|
+
#
|
|
19
|
+
# The order matches the runtime's resolution order:
|
|
20
|
+
# 1. Static accounts, with role bits derived from the message header
|
|
21
|
+
# (writable signers, readonly signers, writable non-signers, readonly
|
|
22
|
+
# non-signers).
|
|
23
|
+
# 2. ALT-loaded writable accounts (always non-signer, writable).
|
|
24
|
+
# 3. ALT-loaded readonly accounts (always non-signer, readonly).
|
|
25
|
+
#
|
|
26
|
+
# Inner-instruction account indices reference the same flat list, so this
|
|
27
|
+
# helper is also useful for resolving inner instructions.
|
|
28
|
+
#
|
|
29
|
+
# Mirrors `getAccountMetasFromCompiledTransactionMessage()`.
|
|
30
|
+
sig do
|
|
31
|
+
params(
|
|
32
|
+
compiled_message: CompiledTransactionMessage,
|
|
33
|
+
loaded_addresses: T.nilable(LoadedAddresses)
|
|
34
|
+
).returns(T::Array[Instructions::AccountMeta])
|
|
35
|
+
end
|
|
36
|
+
def get_account_metas_from_compiled_transaction_message(compiled_message, loaded_addresses = nil)
|
|
37
|
+
header = compiled_message.header
|
|
38
|
+
static_accounts = compiled_message.static_accounts
|
|
39
|
+
|
|
40
|
+
num_writable_signer_accounts =
|
|
41
|
+
header.num_signer_accounts - header.num_readonly_signer_accounts
|
|
42
|
+
num_writable_non_signer_accounts =
|
|
43
|
+
static_accounts.length - header.num_signer_accounts - header.num_readonly_non_signer_accounts
|
|
44
|
+
|
|
45
|
+
metas = T.let([], T::Array[Instructions::AccountMeta])
|
|
46
|
+
i = 0
|
|
47
|
+
num_writable_signer_accounts.times do
|
|
48
|
+
metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::WRITABLE_SIGNER)
|
|
49
|
+
i += 1
|
|
50
|
+
end
|
|
51
|
+
header.num_readonly_signer_accounts.times do
|
|
52
|
+
metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::READONLY_SIGNER)
|
|
53
|
+
i += 1
|
|
54
|
+
end
|
|
55
|
+
num_writable_non_signer_accounts.times do
|
|
56
|
+
metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::WRITABLE)
|
|
57
|
+
i += 1
|
|
58
|
+
end
|
|
59
|
+
header.num_readonly_non_signer_accounts.times do
|
|
60
|
+
metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::READONLY)
|
|
61
|
+
i += 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if loaded_addresses
|
|
65
|
+
loaded_addresses.writable.each { |addr| metas << Instructions::AccountMeta.new(address: addr, role: Instructions::AccountRole::WRITABLE) }
|
|
66
|
+
loaded_addresses.readonly.each { |addr| metas << Instructions::AccountMeta.new(address: addr, role: Instructions::AccountRole::READONLY) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
metas
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns the outer instructions of a compiled transaction message as
|
|
73
|
+
# Instructions::Instruction objects, with account indices resolved to
|
|
74
|
+
# AccountMetas and data exposed as a raw binary String. `accounts` and
|
|
75
|
+
# `data` are omitted (nil) when empty.
|
|
76
|
+
#
|
|
77
|
+
# Mirrors `getInstructionsFromCompiledTransactionMessage()`.
|
|
78
|
+
sig do
|
|
79
|
+
params(
|
|
80
|
+
compiled_message: CompiledTransactionMessage,
|
|
81
|
+
loaded_addresses: T.nilable(LoadedAddresses)
|
|
82
|
+
).returns(T::Array[Instructions::Instruction])
|
|
83
|
+
end
|
|
84
|
+
def get_instructions_from_compiled_transaction_message(compiled_message, loaded_addresses = nil)
|
|
85
|
+
metas = get_account_metas_from_compiled_transaction_message(compiled_message, loaded_addresses)
|
|
86
|
+
get_instructions_from_compiled_transaction_message_with_metas(compiled_message, metas)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Internal variant of get_instructions_from_compiled_transaction_message
|
|
90
|
+
# that takes pre-built AccountMetas. Used by WalkInstructions to avoid
|
|
91
|
+
# rebuilding the meta list when it is already needed for resolving inner
|
|
92
|
+
# instructions.
|
|
93
|
+
sig do
|
|
94
|
+
params(
|
|
95
|
+
compiled_message: CompiledTransactionMessage,
|
|
96
|
+
account_metas: T::Array[Instructions::AccountMeta]
|
|
97
|
+
).returns(T::Array[Instructions::Instruction])
|
|
98
|
+
end
|
|
99
|
+
def get_instructions_from_compiled_transaction_message_with_metas(compiled_message, account_metas)
|
|
100
|
+
compiled_message.instructions.map { |ix| resolve_instruction(ix, account_metas) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
sig { params(ix: CompiledInstruction, metas: T::Array[Instructions::AccountMeta]).returns(Instructions::Instruction) }
|
|
106
|
+
def resolve_instruction(ix, metas)
|
|
107
|
+
program_meta = metas[ix.program_address_index]
|
|
108
|
+
if program_meta.nil?
|
|
109
|
+
Kernel.raise SolanaError.new(
|
|
110
|
+
SolanaError::TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
|
|
111
|
+
{ index: ix.program_address_index }
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
accounts = ix.account_indices.map do |i|
|
|
116
|
+
account_meta = metas[i]
|
|
117
|
+
if account_meta.nil?
|
|
118
|
+
Kernel.raise SolanaError.new(
|
|
119
|
+
SolanaError::TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE,
|
|
120
|
+
{ index: i }
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
account_meta
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Instructions::Instruction.new(
|
|
127
|
+
program_address: program_meta.address,
|
|
128
|
+
accounts: accounts.empty? ? nil : accounts,
|
|
129
|
+
data: ix.data.empty? ? nil : ix.data
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
private_class_method :resolve_instruction
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../addresses/address'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module TransactionIntrospection
|
|
8
|
+
# Loaded ALT addresses as returned by `getTransaction`'s `meta.loadedAddresses`.
|
|
9
|
+
# The two arrays are kept in the same order the runtime uses to resolve
|
|
10
|
+
# instruction account indices.
|
|
11
|
+
# Mirrors `LoadedAddresses` from @solana/transaction-introspection.
|
|
12
|
+
class LoadedAddresses < T::Struct
|
|
13
|
+
const :readonly, T::Array[Addresses::Address]
|
|
14
|
+
const :writable, T::Array[Addresses::Address]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
EMPTY_LOADED_ADDRESSES = T.let(LoadedAddresses.new(readonly: [], writable: []), LoadedAddresses)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../instructions/instruction'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module TransactionIntrospection
|
|
8
|
+
# A ResolvedInstruction carrying its location in the transaction as a
|
|
9
|
+
# `trace` Hash.
|
|
10
|
+
#
|
|
11
|
+
# TypeScript flattens `TracedInstruction` into the instruction itself
|
|
12
|
+
# (`ResolvedInstruction & { trace }`), so callers there access
|
|
13
|
+
# `ix.programAddress` directly. Ruby's `Instructions::Instruction` is a
|
|
14
|
+
# `T::Struct`, which is final and cannot be subclassed (see
|
|
15
|
+
# `Transactions::FullySignedTransaction` for the same constraint), so here
|
|
16
|
+
# the instruction and its trace are held as sibling fields instead:
|
|
17
|
+
# `traced.instruction.program_address`, `traced.trace`.
|
|
18
|
+
#
|
|
19
|
+
# `trace` is one of:
|
|
20
|
+
# { kind: :outer, index: Integer }
|
|
21
|
+
# { kind: :inner, outer_index: Integer, inner_index: Integer, stack_height: T.nilable(Integer) }
|
|
22
|
+
#
|
|
23
|
+
# Mirrors `TracedInstruction` / `InstructionTrace` from @solana/transaction-introspection.
|
|
24
|
+
class TracedInstruction < T::Struct
|
|
25
|
+
const :instruction, Instructions::Instruction
|
|
26
|
+
const :trace, T::Hash[Symbol, T.untyped]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'compiled_transaction_message'
|
|
5
|
+
require_relative 'get_inner_instructions'
|
|
6
|
+
require_relative 'get_instructions'
|
|
7
|
+
require_relative 'loaded_addresses'
|
|
8
|
+
require_relative 'types'
|
|
9
|
+
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module TransactionIntrospection
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Returns every instruction in a confirmed transaction as
|
|
17
|
+
# TracedInstructions, in the order an explorer displays them: each outer
|
|
18
|
+
# instruction followed immediately by the inner instructions its CPIs
|
|
19
|
+
# produced.
|
|
20
|
+
#
|
|
21
|
+
# If +meta+ is nil, only outer instructions are returned. If
|
|
22
|
+
# +loaded_addresses+ is nil, only static accounts are used to resolve
|
|
23
|
+
# indices — pass the `loadedAddresses` from a `getTransaction` response's
|
|
24
|
+
# `meta` for v0 transactions that load accounts from address lookup tables.
|
|
25
|
+
#
|
|
26
|
+
# Mirrors `walkInstructions({ compiledMessage, loadedAddresses, meta })`.
|
|
27
|
+
sig do
|
|
28
|
+
params(
|
|
29
|
+
compiled_message: CompiledTransactionMessage,
|
|
30
|
+
loaded_addresses: T.nilable(LoadedAddresses),
|
|
31
|
+
meta: T.nilable(T::Hash[String, T.untyped])
|
|
32
|
+
).returns(T::Array[TracedInstruction])
|
|
33
|
+
end
|
|
34
|
+
def walk_instructions(compiled_message:, loaded_addresses: nil, meta: nil)
|
|
35
|
+
account_metas = get_account_metas_from_compiled_transaction_message(compiled_message, loaded_addresses)
|
|
36
|
+
outer_instructions = get_instructions_from_compiled_transaction_message_with_metas(compiled_message, account_metas)
|
|
37
|
+
|
|
38
|
+
inner_by_outer_index = T.let({}, T::Hash[Integer, T::Array[TracedInstruction]])
|
|
39
|
+
if meta
|
|
40
|
+
get_inner_instructions_from_meta(meta, account_metas).each do |inner|
|
|
41
|
+
outer_index = inner.trace.fetch(:outer_index)
|
|
42
|
+
(inner_by_outer_index[outer_index] ||= []) << inner
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
result = T.let([], T::Array[TracedInstruction])
|
|
47
|
+
outer_instructions.each_with_index do |instruction, index|
|
|
48
|
+
result << TracedInstruction.new(instruction: instruction, trace: { kind: :outer, index: index })
|
|
49
|
+
group = inner_by_outer_index.delete(index)
|
|
50
|
+
result.concat(group) if group
|
|
51
|
+
end
|
|
52
|
+
# Inner groups whose index matches no outer instruction can only come
|
|
53
|
+
# from malformed input (e.g. `meta` paired with the wrong message).
|
|
54
|
+
# Append them rather than dropping them so no instruction is ever lost.
|
|
55
|
+
inner_by_outer_index.each_value { |group| result.concat(group) }
|
|
56
|
+
|
|
57
|
+
result
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Helpers for inspecting confirmed Solana transactions and walking their
|
|
5
|
+
# outer and inner instructions.
|
|
6
|
+
# Mirrors @solana/transaction-introspection.
|
|
7
|
+
require_relative 'transaction_introspection/loaded_addresses'
|
|
8
|
+
require_relative 'transaction_introspection/compiled_transaction_message'
|
|
9
|
+
require_relative 'transaction_introspection/types'
|
|
10
|
+
require_relative 'transaction_introspection/get_instructions'
|
|
11
|
+
require_relative 'transaction_introspection/get_inner_instructions'
|
|
12
|
+
require_relative 'transaction_introspection/walk_instructions'
|
|
13
|
+
require_relative 'transaction_introspection/decode_rpc_transaction'
|
data/lib/solana/ruby/kit.rb
CHANGED
|
@@ -51,6 +51,7 @@ require_relative 'kit/sysvars'
|
|
|
51
51
|
require_relative 'kit/transaction_confirmation'
|
|
52
52
|
require_relative 'kit/instruction_plans'
|
|
53
53
|
require_relative 'kit/resource_limit_estimation'
|
|
54
|
+
require_relative 'kit/transaction_introspection'
|
|
54
55
|
|
|
55
56
|
# Solana::Ruby::Kit is a Ruby translation of @anza-xyz/kit — the JavaScript SDK for
|
|
56
57
|
# building Solana apps — into idiomatic Ruby with Sorbet static types.
|
|
@@ -80,6 +81,7 @@ require_relative 'kit/resource_limit_estimation'
|
|
|
80
81
|
# Solana::Ruby::Kit::TransactionConfirmation — confirmation polling (@solana/transaction-confirmation)
|
|
81
82
|
# Solana::Ruby::Kit::InstructionPlans — multi-tx planning (@solana/instruction-plans)
|
|
82
83
|
# Solana::Ruby::Kit::WalletStandard — server-side sig verify (@solana/wallet-standard)
|
|
84
|
+
# Solana::Ruby::Kit::TransactionIntrospection — decode/walk instructions (@solana/transaction-introspection)
|
|
83
85
|
|
|
84
86
|
module Solana::Ruby::Kit
|
|
85
87
|
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.
|
|
4
|
+
version: 7.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paul Zupan, Idhra Inc.
|
|
@@ -209,6 +209,7 @@ files:
|
|
|
209
209
|
- lib/solana/ruby/kit/functional.rb
|
|
210
210
|
- lib/solana/ruby/kit/instruction_plans.rb
|
|
211
211
|
- lib/solana/ruby/kit/instruction_plans/instruction_plan.rb
|
|
212
|
+
- lib/solana/ruby/kit/instruction_plans/max_instructions.rb
|
|
212
213
|
- lib/solana/ruby/kit/instruction_plans/plans.rb
|
|
213
214
|
- lib/solana/ruby/kit/instruction_plans/transaction_plan.rb
|
|
214
215
|
- lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb
|
|
@@ -298,6 +299,14 @@ files:
|
|
|
298
299
|
- lib/solana/ruby/kit/sysvars/last_restart_slot.rb
|
|
299
300
|
- lib/solana/ruby/kit/sysvars/rent.rb
|
|
300
301
|
- lib/solana/ruby/kit/transaction_confirmation.rb
|
|
302
|
+
- lib/solana/ruby/kit/transaction_introspection.rb
|
|
303
|
+
- lib/solana/ruby/kit/transaction_introspection/compiled_transaction_message.rb
|
|
304
|
+
- lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb
|
|
305
|
+
- lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb
|
|
306
|
+
- lib/solana/ruby/kit/transaction_introspection/get_instructions.rb
|
|
307
|
+
- lib/solana/ruby/kit/transaction_introspection/loaded_addresses.rb
|
|
308
|
+
- lib/solana/ruby/kit/transaction_introspection/types.rb
|
|
309
|
+
- lib/solana/ruby/kit/transaction_introspection/walk_instructions.rb
|
|
301
310
|
- lib/solana/ruby/kit/transaction_messages.rb
|
|
302
311
|
- lib/solana/ruby/kit/transaction_messages/compute_budget.rb
|
|
303
312
|
- lib/solana/ruby/kit/transaction_messages/transaction_message.rb
|