solana-ruby-kit 0.1.0 → 0.1.5
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 +29 -0
- data/lib/solana/ruby/kit/instruction_plans/instruction_plan.rb +251 -0
- data/lib/solana/ruby/kit/instruction_plans/plans.rb +2 -24
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan.rb +91 -0
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb +117 -0
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb +117 -0
- data/lib/solana/ruby/kit/instruction_plans/transaction_planner.rb +264 -0
- data/lib/solana/ruby/kit/instruction_plans.rb +5 -42
- data/lib/solana/ruby/kit/programs/associated_token_account.rb +133 -0
- data/lib/solana/ruby/kit/programs/system_program.rb +69 -0
- data/lib/solana/ruby/kit/programs.rb +2 -0
- data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +2 -1
- data/lib/solana/ruby/kit/transactions/compiler.rb +211 -0
- data/lib/solana/ruby/kit/transactions/transaction.rb +36 -0
- data/lib/solana/ruby/kit/transactions.rb +1 -0
- data/lib/solana/ruby/kit/version.rb +1 -1
- data/solana-ruby-kit.gemspec +1 -1
- metadata +11 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12771cac3706269f104b012ee691d4cd42856f61560d54b9e959308184c90be6
|
|
4
|
+
data.tar.gz: 52bc18d7dff9bce3c2f27115772275e8a2b0decd985faa4ef2f43723056030d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1f7feeaf404a2911bf4498377e2e2426f10357cd415cb70e4a57d70b05a81a73b51b817983e33bd1a9aa169ee9dcd9d80018c050964b0a51ada8afebba772d2
|
|
7
|
+
data.tar.gz: 6f3f9b4d8de10136120b08d4be8927ba13546edc902f81a3f46b64e89e5947b9623ef3645857c721c44bd5bc7d16c4ba13eee74d65bca03d09bee78fd0b3a932
|
|
@@ -46,6 +46,7 @@ module Solana::Ruby::Kit
|
|
|
46
46
|
|
|
47
47
|
# ── Transactions ──────────────────────────────────────────────────────────
|
|
48
48
|
TRANSACTIONS__TRANSACTION_NOT_SIGNABLE = :SOLANA_ERROR__TRANSACTIONS__TRANSACTION_NOT_SIGNABLE
|
|
49
|
+
TRANSACTIONS__EXCEEDS_SIZE_LIMIT = :SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT
|
|
49
50
|
TRANSACTIONS__MISSING_SIGNER = :SOLANA_ERROR__TRANSACTIONS__MISSING_SIGNER
|
|
50
51
|
TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE = :SOLANA_ERROR__TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE
|
|
51
52
|
TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS = :SOLANA_ERROR__TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS
|
|
@@ -87,6 +88,13 @@ module Solana::Ruby::Kit
|
|
|
87
88
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
|
|
88
89
|
RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER
|
|
89
90
|
RPC__TRANSPORT_HTTP_ERROR = :SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR
|
|
91
|
+
# JSON-RPC server errors (-32017..-32019); context keys mirror TypeScript SolanaErrorContext:
|
|
92
|
+
# EPOCH_REWARDS_PERIOD_ACTIVE: { slot:, current_block_height:, rewards_complete_block_height: }
|
|
93
|
+
# SLOT_NOT_EPOCH_BOUNDARY: { slot: }
|
|
94
|
+
# LONG_TERM_STORAGE_UNREACHABLE: (no context)
|
|
95
|
+
JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE
|
|
96
|
+
JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY
|
|
97
|
+
JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE = :SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE
|
|
90
98
|
RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST
|
|
91
99
|
RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID
|
|
92
100
|
RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED = :SOLANA_ERROR__RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED
|
|
@@ -101,9 +109,18 @@ module Solana::Ruby::Kit
|
|
|
101
109
|
OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG = :SOLANA_ERROR__OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG
|
|
102
110
|
OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN = :SOLANA_ERROR__OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN
|
|
103
111
|
|
|
112
|
+
# ── Instruction plans ─────────────────────────────────────────────────────
|
|
113
|
+
# context: { num_bytes_required:, num_free_bytes: }
|
|
114
|
+
INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN
|
|
115
|
+
INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE = :SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE
|
|
116
|
+
INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN
|
|
117
|
+
INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN = :SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN
|
|
118
|
+
|
|
104
119
|
# ── Invariant violations (internal) ──────────────────────────────────────
|
|
105
120
|
INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING
|
|
106
121
|
INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE = :SOLANA_ERROR__INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE
|
|
122
|
+
INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND = :SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND
|
|
123
|
+
INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND = :SOLANA_ERROR__INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND
|
|
107
124
|
|
|
108
125
|
ERROR_MESSAGES = T.let(
|
|
109
126
|
{
|
|
@@ -145,6 +162,7 @@ module Solana::Ruby::Kit
|
|
|
145
162
|
|
|
146
163
|
# Transactions
|
|
147
164
|
TRANSACTIONS__TRANSACTION_NOT_SIGNABLE => 'Transaction is not signable (missing fee payer or lifetime constraint)',
|
|
165
|
+
TRANSACTIONS__EXCEEDS_SIZE_LIMIT => 'Transaction wire size (%{actual_size} bytes) exceeds the limit of %{limit} bytes',
|
|
148
166
|
TRANSACTIONS__MISSING_SIGNER => 'Transaction is missing a required signer: %{address}',
|
|
149
167
|
TRANSACTIONS__VERSION_NUMBER_OUT_OF_RANGE => 'Transaction version %{version} is out of range',
|
|
150
168
|
TRANSACTIONS__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS => 'Failed to decompile address lookup table contents',
|
|
@@ -186,6 +204,9 @@ module Solana::Ruby::Kit
|
|
|
186
204
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER => 'Integer overflow while serializing large integer %{value}',
|
|
187
205
|
RPC__INTEGER_OVERFLOW_WHILE_DESERIALIZING_LARGE_INTEGER => 'Integer overflow while deserializing large integer %{value}',
|
|
188
206
|
RPC__TRANSPORT_HTTP_ERROR => 'HTTP transport error: %{status} %{message}',
|
|
207
|
+
JSON_RPC__SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE => 'Epoch rewards period still active at slot %{slot}',
|
|
208
|
+
JSON_RPC__SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY => "Rewards cannot be found because slot %{slot} is not the epoch boundary. This may be due to gap in the queried node's local ledger or long-term storage",
|
|
209
|
+
JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE => 'Failed to query long-term storage; please try again',
|
|
189
210
|
RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_REQUEST => 'Cannot create subscription request',
|
|
190
211
|
RPC_SUBSCRIPTIONS__EXPECTED_SERVER_SUBSCRIPTION_ID => 'Expected server to return a subscription ID',
|
|
191
212
|
RPC_SUBSCRIPTIONS__CHANNEL_CLOSED_BEFORE_MESSAGE_BUFFERED => 'WebSocket channel closed before message could be buffered',
|
|
@@ -200,9 +221,17 @@ module Solana::Ruby::Kit
|
|
|
200
221
|
OFFCHAIN_MESSAGES__MESSAGE_TOO_LONG => 'Offchain message is too long (%{length} bytes, max %{max})',
|
|
201
222
|
OFFCHAIN_MESSAGES__LEADING_ZERO_IN_SIGNING_DOMAIN => 'Offchain message signing domain must not start with a null byte',
|
|
202
223
|
|
|
224
|
+
# Instruction plans
|
|
225
|
+
INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN => 'Transaction message cannot accommodate the plan: requires %{num_bytes_required} bytes but only %{num_free_bytes} are available',
|
|
226
|
+
INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE => 'Message packer is already complete; no more instructions to pack',
|
|
227
|
+
INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN => 'Instruction plan is empty and produced no transaction messages',
|
|
228
|
+
INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN => 'Failed to execute transaction plan',
|
|
229
|
+
|
|
203
230
|
# Invariant violations
|
|
204
231
|
INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_STATE_MISSING => 'Subscription iterator state is missing (internal error)',
|
|
205
232
|
INVARIANT_VIOLATION__SUBSCRIPTION_ITERATOR_MUST_NOT_POLL_BEFORE_RESOLVING_EXISTING_MESSAGE_PROMISE => 'Subscription iterator must not poll before resolving existing message (internal error)',
|
|
233
|
+
INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND => 'Invalid instruction plan kind: %{kind} (internal error)',
|
|
234
|
+
INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND => 'Invalid transaction plan kind: %{kind} (internal error)',
|
|
206
235
|
}.freeze,
|
|
207
236
|
T::Hash[Symbol, String]
|
|
208
237
|
)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative '../instructions/instruction'
|
|
6
|
+
require_relative '../transaction_messages/transaction_message'
|
|
7
|
+
require_relative '../transactions/compiler'
|
|
8
|
+
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module InstructionPlans
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
# ── Plan types ─────────────────────────────────────────────────────────────
|
|
14
|
+
#
|
|
15
|
+
# InstructionPlan is a recursive tree that describes operations which may
|
|
16
|
+
# span multiple transactions. Mirrors @solana/instruction-plans.
|
|
17
|
+
#
|
|
18
|
+
# InstructionPlan = SingleInstructionPlan
|
|
19
|
+
# | SequentialInstructionPlan
|
|
20
|
+
# | ParallelInstructionPlan
|
|
21
|
+
# | MessagePackerInstructionPlan
|
|
22
|
+
|
|
23
|
+
# A plan that wraps a single instruction.
|
|
24
|
+
class SingleInstructionPlan < T::Struct
|
|
25
|
+
const :instruction, Instructions::Instruction
|
|
26
|
+
def kind = :single
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A plan whose children must execute in order.
|
|
30
|
+
# +divisible: true+ allows the planner to split children across transactions.
|
|
31
|
+
# +divisible: false+ means all children should be atomic (one tx or a bundle).
|
|
32
|
+
class SequentialInstructionPlan < T::Struct
|
|
33
|
+
const :plans, T::Array[T.untyped] # Array[InstructionPlan]
|
|
34
|
+
const :divisible, T::Boolean
|
|
35
|
+
def kind = :sequential
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A plan whose children may execute concurrently or be packed in any order.
|
|
39
|
+
class ParallelInstructionPlan < T::Struct
|
|
40
|
+
const :plans, T::Array[T.untyped] # Array[InstructionPlan]
|
|
41
|
+
def kind = :parallel
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Provides a MessagePacker that dynamically packs instructions into messages.
|
|
45
|
+
# The +get_message_packer+ proc returns a fresh MessagePacker each call.
|
|
46
|
+
class MessagePackerInstructionPlan < T::Struct
|
|
47
|
+
const :get_message_packer, T.untyped # Proc -> MessagePacker
|
|
48
|
+
def kind = :message_packer
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returned by MessagePackerInstructionPlan#get_message_packer.
|
|
52
|
+
# Mirrors the TypeScript MessagePacker interface.
|
|
53
|
+
class MessagePacker
|
|
54
|
+
extend T::Sig
|
|
55
|
+
|
|
56
|
+
sig { params(done_proc: T.untyped, pack_proc: T.untyped).void }
|
|
57
|
+
def initialize(done_proc:, pack_proc:)
|
|
58
|
+
@done_proc = T.let(done_proc, T.untyped)
|
|
59
|
+
@pack_proc = T.let(pack_proc, T.untyped)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns true when all instructions have been packed.
|
|
63
|
+
sig { returns(T::Boolean) }
|
|
64
|
+
def done?
|
|
65
|
+
@done_proc.call
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Packs as many instructions as possible into +message+ and returns the
|
|
69
|
+
# updated message. Raises SolanaError if the message is too small or the
|
|
70
|
+
# packer is already done.
|
|
71
|
+
sig { params(message: TransactionMessages::TransactionMessage).returns(TransactionMessages::TransactionMessage) }
|
|
72
|
+
def pack_message_to_capacity(message)
|
|
73
|
+
@pack_proc.call(message)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# ── Factory helpers ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
module_function
|
|
80
|
+
|
|
81
|
+
# Wraps a single instruction in a plan.
|
|
82
|
+
# Mirrors `singleInstructionPlan(instruction)`.
|
|
83
|
+
sig { params(instruction: Instructions::Instruction).returns(SingleInstructionPlan) }
|
|
84
|
+
def single_instruction_plan(instruction)
|
|
85
|
+
SingleInstructionPlan.new(instruction: instruction)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Creates a divisible sequential plan (children may be split across txs).
|
|
89
|
+
# Mirrors `sequentialInstructionPlan(plans)`.
|
|
90
|
+
# Accepts raw Instruction objects; wraps them in SingleInstructionPlan automatically.
|
|
91
|
+
sig { params(plans: T::Array[T.untyped]).returns(SequentialInstructionPlan) }
|
|
92
|
+
def sequential_instruction_plan(plans)
|
|
93
|
+
SequentialInstructionPlan.new(plans: parse_single_instruction_plans(plans), divisible: true)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Creates a non-divisible sequential plan (children must be atomic).
|
|
97
|
+
# Mirrors `nonDivisibleSequentialInstructionPlan(plans)`.
|
|
98
|
+
sig { params(plans: T::Array[T.untyped]).returns(SequentialInstructionPlan) }
|
|
99
|
+
def non_divisible_sequential_instruction_plan(plans)
|
|
100
|
+
SequentialInstructionPlan.new(plans: parse_single_instruction_plans(plans), divisible: false)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Creates a parallel plan (children may execute concurrently).
|
|
104
|
+
# Mirrors `parallelInstructionPlan(plans)`.
|
|
105
|
+
sig { params(plans: T::Array[T.untyped]).returns(ParallelInstructionPlan) }
|
|
106
|
+
def parallel_instruction_plan(plans)
|
|
107
|
+
ParallelInstructionPlan.new(plans: parse_single_instruction_plans(plans))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Creates a MessagePackerInstructionPlan that packs a fixed byte stream into
|
|
111
|
+
# instructions of maximum size, calling +get_instruction.(offset, length)+.
|
|
112
|
+
# Mirrors `getLinearMessagePackerInstructionPlan({ getInstruction, totalLength })`.
|
|
113
|
+
sig do
|
|
114
|
+
params(
|
|
115
|
+
total_length: Integer,
|
|
116
|
+
get_instruction: T.untyped # Proc(offset, length) -> Instruction
|
|
117
|
+
).returns(MessagePackerInstructionPlan)
|
|
118
|
+
end
|
|
119
|
+
def get_linear_message_packer_instruction_plan(total_length:, get_instruction:)
|
|
120
|
+
MessagePackerInstructionPlan.new(
|
|
121
|
+
get_message_packer: -> {
|
|
122
|
+
offset = T.let(0, Integer)
|
|
123
|
+
MessagePacker.new(
|
|
124
|
+
done_proc: -> { offset >= total_length },
|
|
125
|
+
pack_proc: ->(message) {
|
|
126
|
+
if offset >= total_length
|
|
127
|
+
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
base_ix = get_instruction.call(offset, 0)
|
|
131
|
+
with_base = TransactionMessages.append_instructions(message, [base_ix])
|
|
132
|
+
base_size = Transactions.get_transaction_message_size(with_base)
|
|
133
|
+
# -1 leeway for compact-u16 headers
|
|
134
|
+
free_space = Transactions::TRANSACTION_SIZE_LIMIT - base_size - 1
|
|
135
|
+
|
|
136
|
+
if free_space <= 0
|
|
137
|
+
msg_size = Transactions.get_transaction_message_size(message)
|
|
138
|
+
Kernel.raise SolanaError.new(
|
|
139
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
|
|
140
|
+
{
|
|
141
|
+
num_bytes_required: base_size - msg_size + 1,
|
|
142
|
+
num_free_bytes: Transactions::TRANSACTION_SIZE_LIMIT - msg_size - 1
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
length = [total_length - offset, free_space].min
|
|
148
|
+
ix = get_instruction.call(offset, length)
|
|
149
|
+
offset += length
|
|
150
|
+
TransactionMessages.append_instructions(message, [ix])
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Creates a MessagePackerInstructionPlan that iterates over a fixed list of
|
|
158
|
+
# instructions, packing as many as fit into each message.
|
|
159
|
+
# Mirrors `getMessagePackerInstructionPlanFromInstructions(instructions)`.
|
|
160
|
+
sig { params(instructions: T::Array[Instructions::Instruction]).returns(MessagePackerInstructionPlan) }
|
|
161
|
+
def get_message_packer_instruction_plan_from_instructions(instructions)
|
|
162
|
+
MessagePackerInstructionPlan.new(
|
|
163
|
+
get_message_packer: -> {
|
|
164
|
+
idx = T.let(0, Integer)
|
|
165
|
+
MessagePacker.new(
|
|
166
|
+
done_proc: -> { idx >= instructions.length },
|
|
167
|
+
pack_proc: ->(message) {
|
|
168
|
+
if idx >= instructions.length
|
|
169
|
+
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
original_size = Transactions.get_transaction_message_size(message)
|
|
173
|
+
packed = T.let(message, TransactionMessages::TransactionMessage)
|
|
174
|
+
start_idx = idx
|
|
175
|
+
|
|
176
|
+
(idx...instructions.length).each do |i|
|
|
177
|
+
packed = TransactionMessages.append_instructions(packed, [instructions[i]])
|
|
178
|
+
size = Transactions.get_transaction_message_size(packed)
|
|
179
|
+
|
|
180
|
+
if size > Transactions::TRANSACTION_SIZE_LIMIT
|
|
181
|
+
if i == start_idx
|
|
182
|
+
Kernel.raise SolanaError.new(
|
|
183
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
|
|
184
|
+
{
|
|
185
|
+
num_bytes_required: size - original_size,
|
|
186
|
+
num_free_bytes: Transactions::TRANSACTION_SIZE_LIMIT - original_size
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
idx = i
|
|
191
|
+
return packed
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
idx = instructions.length
|
|
196
|
+
packed
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Creates a MessagePackerInstructionPlan that splits +total_size+ bytes into
|
|
204
|
+
# chunks of REALLOC_LIMIT (10,240) bytes, calling +get_instruction.(size)+.
|
|
205
|
+
# Mirrors `getReallocMessagePackerInstructionPlan({ getInstruction, totalSize })`.
|
|
206
|
+
sig do
|
|
207
|
+
params(
|
|
208
|
+
total_size: Integer,
|
|
209
|
+
get_instruction: T.untyped # Proc(size) -> Instruction
|
|
210
|
+
).returns(MessagePackerInstructionPlan)
|
|
211
|
+
end
|
|
212
|
+
def get_realloc_message_packer_instruction_plan(total_size:, get_instruction:)
|
|
213
|
+
realloc_limit = 10_240
|
|
214
|
+
num_instructions = (total_size.to_f / realloc_limit).ceil
|
|
215
|
+
last_size = total_size % realloc_limit
|
|
216
|
+
|
|
217
|
+
instructions = num_instructions.times.map do |i|
|
|
218
|
+
chunk = (i == num_instructions - 1) ? last_size : realloc_limit
|
|
219
|
+
get_instruction.call(chunk)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
get_message_packer_instruction_plan_from_instructions(instructions)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Recursively extracts all instructions from a plan tree (depth-first).
|
|
226
|
+
sig { params(plan: T.untyped).returns(T::Array[Instructions::Instruction]) }
|
|
227
|
+
def flatten_instruction_plan(plan)
|
|
228
|
+
case plan
|
|
229
|
+
when SingleInstructionPlan
|
|
230
|
+
[plan.instruction]
|
|
231
|
+
when SequentialInstructionPlan, ParallelInstructionPlan
|
|
232
|
+
plan.plans.flat_map { |p| flatten_instruction_plan(p) }
|
|
233
|
+
when MessagePackerInstructionPlan
|
|
234
|
+
Kernel.raise ArgumentError, 'Cannot flatten a MessagePackerInstructionPlan (instructions are dynamically generated)'
|
|
235
|
+
else
|
|
236
|
+
Kernel.raise ArgumentError, "Unknown InstructionPlan type: #{plan.class}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
# Wraps bare Instruction objects in SingleInstructionPlan; passes plans through.
|
|
243
|
+
sig { params(items: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
|
244
|
+
def parse_single_instruction_plans(items)
|
|
245
|
+
items.map do |item|
|
|
246
|
+
item.is_a?(Instructions::Instruction) ? single_instruction_plan(item) : item
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
private_class_method :parse_single_instruction_plans
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# A plan that wraps a single instruction.
|
|
7
|
-
class SingleInstructionPlan < T::Struct
|
|
8
|
-
const :instruction, Instructions::Instruction
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
# A plan that executes a sequence of sub-plans in order.
|
|
12
|
-
# When +divisible+ is true the planner may split steps across transactions.
|
|
13
|
-
class SequentialInstructionPlan < T::Struct
|
|
14
|
-
const :steps, T::Array[T.untyped] # Array[InstructionPlan]
|
|
15
|
-
const :divisible, T::Boolean, default: false
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# A plan whose sub-plans may execute concurrently or be packed into the
|
|
19
|
-
# same transaction in any order.
|
|
20
|
-
class ParallelInstructionPlan < T::Struct
|
|
21
|
-
const :plans, T::Array[T.untyped] # Array[InstructionPlan]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Union type alias (used only in doc strings; Ruby is duck-typed).
|
|
25
|
-
# InstructionPlan = SingleInstructionPlan | SequentialInstructionPlan | ParallelInstructionPlan
|
|
26
|
-
end
|
|
27
|
-
end
|
|
4
|
+
# Kept for backwards compatibility — content moved to instruction_plan.rb.
|
|
5
|
+
require_relative 'instruction_plan'
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../transaction_messages/transaction_message'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module InstructionPlans
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
# ── Transaction plan types ────────────────────────────────────────────────
|
|
11
|
+
#
|
|
12
|
+
# A TransactionPlan is a recursive tree of compiled transaction messages that
|
|
13
|
+
# mirrors the structure of an InstructionPlan after the planner has resolved it.
|
|
14
|
+
#
|
|
15
|
+
# TransactionPlan = SingleTransactionPlan
|
|
16
|
+
# | SequentialTransactionPlan
|
|
17
|
+
# | ParallelTransactionPlan
|
|
18
|
+
|
|
19
|
+
class SingleTransactionPlan < T::Struct
|
|
20
|
+
const :message, TransactionMessages::TransactionMessage
|
|
21
|
+
def kind = :single
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class SequentialTransactionPlan < T::Struct
|
|
25
|
+
const :plans, T::Array[T.untyped] # Array[TransactionPlan]
|
|
26
|
+
const :divisible, T::Boolean
|
|
27
|
+
def kind = :sequential
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ParallelTransactionPlan < T::Struct
|
|
31
|
+
const :plans, T::Array[T.untyped] # Array[TransactionPlan]
|
|
32
|
+
def kind = :parallel
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# Creates a single-message plan.
|
|
38
|
+
# Mirrors `singleTransactionPlan(transactionMessage)`.
|
|
39
|
+
sig { params(message: TransactionMessages::TransactionMessage).returns(SingleTransactionPlan) }
|
|
40
|
+
def single_transaction_plan(message)
|
|
41
|
+
SingleTransactionPlan.new(message: message)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Creates a divisible sequential plan.
|
|
45
|
+
# Accepts TransactionMessage objects directly (wraps them automatically).
|
|
46
|
+
# Mirrors `sequentialTransactionPlan(plans)`.
|
|
47
|
+
sig { params(plans: T::Array[T.untyped]).returns(SequentialTransactionPlan) }
|
|
48
|
+
def sequential_transaction_plan(plans)
|
|
49
|
+
SequentialTransactionPlan.new(plans: parse_single_transaction_plans(plans), divisible: true)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Creates a non-divisible sequential plan.
|
|
53
|
+
# Mirrors `nonDivisibleSequentialTransactionPlan(plans)`.
|
|
54
|
+
sig { params(plans: T::Array[T.untyped]).returns(SequentialTransactionPlan) }
|
|
55
|
+
def non_divisible_sequential_transaction_plan(plans)
|
|
56
|
+
SequentialTransactionPlan.new(plans: parse_single_transaction_plans(plans), divisible: false)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates a parallel plan.
|
|
60
|
+
# Accepts TransactionMessage objects directly (wraps them automatically).
|
|
61
|
+
# Mirrors `parallelTransactionPlan(plans)`.
|
|
62
|
+
sig { params(plans: T::Array[T.untyped]).returns(ParallelTransactionPlan) }
|
|
63
|
+
def parallel_transaction_plan(plans)
|
|
64
|
+
ParallelTransactionPlan.new(plans: parse_single_transaction_plans(plans))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Recursively collects all SingleTransactionPlan leaves from a plan tree.
|
|
68
|
+
# Mirrors `getAllSingleTransactionPlans(transactionPlan)`.
|
|
69
|
+
sig { params(plan: T.untyped).returns(T::Array[SingleTransactionPlan]) }
|
|
70
|
+
def get_all_single_transaction_plans(plan)
|
|
71
|
+
case plan
|
|
72
|
+
when SingleTransactionPlan
|
|
73
|
+
[plan]
|
|
74
|
+
when SequentialTransactionPlan, ParallelTransactionPlan
|
|
75
|
+
plan.plans.flat_map { |p| get_all_single_transaction_plans(p) }
|
|
76
|
+
else
|
|
77
|
+
Kernel.raise ArgumentError, "Unknown TransactionPlan type: #{plan.class}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
sig { params(items: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
|
84
|
+
def parse_single_transaction_plans(items)
|
|
85
|
+
items.map do |item|
|
|
86
|
+
item.is_a?(TransactionMessages::TransactionMessage) ? single_transaction_plan(item) : item
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
private_class_method :parse_single_transaction_plans
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative 'transaction_plan'
|
|
6
|
+
require_relative 'transaction_plan_result'
|
|
7
|
+
|
|
8
|
+
module Solana::Ruby::Kit
|
|
9
|
+
module InstructionPlans
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
# Creates a TransactionPlanExecutor — a callable that traverses a TransactionPlan,
|
|
13
|
+
# executing each single transaction message and collecting results.
|
|
14
|
+
#
|
|
15
|
+
# Configuration:
|
|
16
|
+
# execute_transaction_message: ->(message) { { transaction:, context: } }
|
|
17
|
+
# Called for each SingleTransactionPlan. Must return a hash with:
|
|
18
|
+
# :transaction — the signed Transaction
|
|
19
|
+
# :context — optional Hash of extra data (defaults to {})
|
|
20
|
+
# Raise a SolanaError to signal failure; remaining plans will be canceled.
|
|
21
|
+
#
|
|
22
|
+
# The returned executor is a lambda: executor.call(transaction_plan) -> TransactionPlanResult
|
|
23
|
+
#
|
|
24
|
+
# Mirrors `createTransactionPlanExecutor(config)` from @solana/instruction-plans.
|
|
25
|
+
# NOTE: Ruby translation is synchronous; abort-signal support is omitted.
|
|
26
|
+
sig { params(execute_transaction_message: T.untyped).returns(T.untyped) }
|
|
27
|
+
def create_transaction_plan_executor(execute_transaction_message:)
|
|
28
|
+
->(transaction_plan) {
|
|
29
|
+
state = { canceled: false }
|
|
30
|
+
result = executor_traverse(transaction_plan, execute_transaction_message, state)
|
|
31
|
+
|
|
32
|
+
if state[:canceled]
|
|
33
|
+
cause = executor_find_error(result)
|
|
34
|
+
err = SolanaError.new(
|
|
35
|
+
SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
|
|
36
|
+
{ cause: cause }
|
|
37
|
+
)
|
|
38
|
+
# Store the full result tree as a non-enumerable attribute for recovery.
|
|
39
|
+
err.instance_variable_set(:@transaction_plan_result, result)
|
|
40
|
+
err.define_singleton_method(:transaction_plan_result) { @transaction_plan_result }
|
|
41
|
+
Kernel.raise err
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
result
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module_function :create_transaction_plan_executor
|
|
49
|
+
|
|
50
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
def executor_traverse(plan, execute_fn, state)
|
|
55
|
+
case plan.kind
|
|
56
|
+
when :sequential then executor_traverse_sequential(plan, execute_fn, state)
|
|
57
|
+
when :parallel then executor_traverse_parallel(plan, execute_fn, state)
|
|
58
|
+
when :single then executor_traverse_single(plan, execute_fn, state)
|
|
59
|
+
else
|
|
60
|
+
Kernel.raise SolanaError.new(
|
|
61
|
+
SolanaError::INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
|
|
62
|
+
{ kind: plan.kind }
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def executor_traverse_sequential(plan, execute_fn, state)
|
|
68
|
+
results = plan.plans.map { |sub| executor_traverse(sub, execute_fn, state) }
|
|
69
|
+
plan.divisible \
|
|
70
|
+
? sequential_transaction_plan_result(results) \
|
|
71
|
+
: non_divisible_sequential_transaction_plan_result(results)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def executor_traverse_parallel(plan, execute_fn, state)
|
|
75
|
+
results = plan.plans.map { |sub| executor_traverse(sub, execute_fn, state) }
|
|
76
|
+
parallel_transaction_plan_result(results)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def executor_traverse_single(plan, execute_fn, state)
|
|
80
|
+
return canceled_single_transaction_plan_result(plan.message) if state[:canceled]
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
result = execute_fn.call(plan.message)
|
|
84
|
+
transaction = result.fetch(:transaction)
|
|
85
|
+
context = result.fetch(:context, {})
|
|
86
|
+
successful_single_transaction_plan_result(plan.message, transaction, context)
|
|
87
|
+
rescue SolanaError => e
|
|
88
|
+
state[:canceled] = true
|
|
89
|
+
failed_single_transaction_plan_result(plan.message, e)
|
|
90
|
+
rescue => e
|
|
91
|
+
state[:canceled] = true
|
|
92
|
+
wrapped = SolanaError.new(
|
|
93
|
+
SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
|
|
94
|
+
{ cause: e }
|
|
95
|
+
)
|
|
96
|
+
failed_single_transaction_plan_result(plan.message, wrapped)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def executor_find_error(result)
|
|
101
|
+
return result.status.error if result.kind == :single && result.status.kind == :failed
|
|
102
|
+
return nil if result.kind == :single
|
|
103
|
+
|
|
104
|
+
result.plans.each do |sub|
|
|
105
|
+
err = executor_find_error(sub)
|
|
106
|
+
return err if err
|
|
107
|
+
end
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private_class_method :executor_traverse
|
|
112
|
+
private_class_method :executor_traverse_sequential
|
|
113
|
+
private_class_method :executor_traverse_parallel
|
|
114
|
+
private_class_method :executor_traverse_single
|
|
115
|
+
private_class_method :executor_find_error
|
|
116
|
+
end
|
|
117
|
+
end
|