Module: Solana::Ruby::Kit::InstructionPlans

Extended by:
T::Sig
Defined in:
lib/solana/ruby/kit/instruction_plans/instruction_plan.rb,
lib/solana/ruby/kit/instruction_plans/max_instructions.rb,
lib/solana/ruby/kit/instruction_plans/transaction_plan.rb,
lib/solana/ruby/kit/instruction_plans/transaction_planner.rb,
lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb,
lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb

Defined Under Namespace

Classes: CanceledStatus, FailedStatus, MessagePacker, MessagePackerInstructionPlan, ParallelInstructionPlan, ParallelTransactionPlan, ParallelTransactionPlanResult, SequentialInstructionPlan, SequentialTransactionPlan, SequentialTransactionPlanResult, SingleInstructionPlan, SingleTransactionPlan, SingleTransactionPlanResult, SuccessfulStatus

Constant Summary collapse

DEFAULT_MAX_INSTRUCTIONS_PER_TRANSACTION =

The default maximum number of top-level instructions per planned transaction message.

Intentionally lower than the transaction format's instruction limit to leave headroom for inner instructions (CPIs), which are not visible at planning time.

T.let(16, Integer)
TRANSACTION_INSTRUCTION_LIMIT =

The hard maximum number of top-level instructions the transaction format can encode.

T.let(64, Integer)

Class Method Summary collapse

Class Method Details

.assert_max_instructions_per_transaction(num_instructions, max_instructions) ⇒ Object



50
51
52
53
54
55
56
57
# File 'lib/solana/ruby/kit/instruction_plans/max_instructions.rb', line 50

def assert_max_instructions_per_transaction(num_instructions, max_instructions)
  return if num_instructions <= max_instructions

  Kernel.raise SolanaError.new(
    SolanaError::INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDED,
    { max_instructions: max_instructions, num_instructions: num_instructions }
  )
end

.assert_valid_max_instructions_per_transaction(max_instructions) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/solana/ruby/kit/instruction_plans/max_instructions.rb', line 33

def assert_valid_max_instructions_per_transaction(max_instructions)
  return if max_instructions.nil?

  if max_instructions <= 0 || max_instructions > TRANSACTION_INSTRUCTION_LIMIT
    Kernel.raise SolanaError.new(
      SolanaError::INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION,
      {
        max_instructions:              max_instructions,
        transaction_instruction_limit: TRANSACTION_INSTRUCTION_LIMIT
      }
    )
  end
end

.canceled_single_transaction_plan_result(message, context = {}) ⇒ Object



144
145
146
147
148
149
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 144

def canceled_single_transaction_plan_result(message, context = {})
  SingleTransactionPlanResult.new(
    message: message,
    status:  CanceledStatus.new(context: context.dup.freeze)
  )
end

.create_transaction_plan_executor(execute_transaction_message:) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb', line 40

def create_transaction_plan_executor(execute_transaction_message:)
  ->(transaction_plan) {
    state  = { canceled: false }
    result = executor_traverse(transaction_plan, execute_transaction_message, state)

    if state[:canceled]
      cause = executor_find_error(result)
      err   = SolanaError.new(
        SolanaError::INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN,
        { cause: cause }
      )
      # Store the full result tree as a non-enumerable attribute for recovery.
      err.instance_variable_set(:@transaction_plan_result, result)
      err.define_singleton_method(:transaction_plan_result) { result }
      Kernel.raise err
    end

    result
  }
end

.create_transaction_planner(create_transaction_message:, on_transaction_message_updated: ->(msg) { msg }, max_instructions_per_transaction: nil) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/solana/ruby/kit/instruction_plans/transaction_planner.rb', line 45

def create_transaction_planner(
  create_transaction_message:,
  on_transaction_message_updated: ->(msg) { msg },
  max_instructions_per_transaction: nil
)
  ctx = {
    create_transaction_message:     create_transaction_message,
    on_transaction_message_updated: on_transaction_message_updated
  }
  config_max_instructions_per_transaction = max_instructions_per_transaction

  ->(instruction_plan, max_instructions_per_transaction: nil) {
    resolved_max = max_instructions_per_transaction || config_max_instructions_per_transaction

    # Reject up front any configured maximum the transaction format could never satisfy,
    # rather than discovering it mid-plan when a message fails to compile.
    InstructionPlans.assert_valid_max_instructions_per_transaction(resolved_max)

    mutable = planner_traverse(
      instruction_plan,
      ctx.merge(parent: nil, parent_candidates: [], max_instructions_per_transaction: resolved_max)
    )
    Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN) unless mutable
    planner_freeze(mutable)
  }
end

.failed_single_transaction_plan_result(message, error, context = {}) ⇒ Object



130
131
132
133
134
135
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 130

def failed_single_transaction_plan_result(message, error, context = {})
  SingleTransactionPlanResult.new(
    message: message,
    status:  FailedStatus.new(error: error, context: context.dup.freeze)
  )
end

.flatten_instruction_plan(plan) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 266

def flatten_instruction_plan(plan)
  case plan
  when SingleInstructionPlan
    [plan.instruction]
  when SequentialInstructionPlan, ParallelInstructionPlan
    plan.plans.flat_map { |p| flatten_instruction_plan(p) }
  when MessagePackerInstructionPlan
    Kernel.raise ArgumentError, 'Cannot flatten a MessagePackerInstructionPlan (instructions are dynamically generated)'
  else
    Kernel.raise ArgumentError, "Unknown InstructionPlan type: #{plan.class}"
  end
end

.get_all_single_transaction_plans(plan) ⇒ Object



79
80
81
82
83
84
85
86
87
88
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 79

def get_all_single_transaction_plans(plan)
  case plan
  when SingleTransactionPlan
    [plan]
  when SequentialTransactionPlan, ParallelTransactionPlan
    plan.plans.flat_map { |p| get_all_single_transaction_plans(p) }
  else
    Kernel.raise ArgumentError, "Unknown TransactionPlan type: #{plan.class}"
  end
end

.get_linear_message_packer_instruction_plan(total_length:, get_instruction:) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 140

def get_linear_message_packer_instruction_plan(total_length:, get_instruction:)
  MessagePackerInstructionPlan.new(
    get_message_packer: -> {
      offset = T.let(0, Integer)
      MessagePacker.new(
        done_proc: -> { offset >= total_length },
        pack_proc:  ->(message, max_instructions) {
          if offset >= total_length
            Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
          end

          InstructionPlans.assert_valid_max_instructions_per_transaction(max_instructions)
          resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
          InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)

          base_ix    = get_instruction.call(offset, 0)
          with_base  = TransactionMessages.append_instructions(message, [base_ix])
          base_size  = Transactions.get_transaction_message_size(with_base)
          # -1 leeway for compact-u16 headers
          free_space = Transactions::TRANSACTION_SIZE_LIMIT - base_size - 1

          if free_space <= 0
            msg_size = Transactions.get_transaction_message_size(message)
            Kernel.raise SolanaError.new(
              SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
              {
                num_bytes_required: base_size - msg_size + 1,
                num_free_bytes:     Transactions::TRANSACTION_SIZE_LIMIT - msg_size - 1
              }
            )
          end

          length = [total_length - offset, free_space].min
          ix     = get_instruction.call(offset, length)
          offset += length
          TransactionMessages.append_instructions(message, [ix])
        }
      )
    }
  )
end

.get_message_packer_instruction_plan_from_instructions(instructions) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 186

def get_message_packer_instruction_plan_from_instructions(instructions)
  MessagePackerInstructionPlan.new(
    get_message_packer: -> {
      idx = T.let(0, Integer)
      MessagePacker.new(
        done_proc: -> { idx >= instructions.length },
        pack_proc:  ->(message, max_instructions) {
          if idx >= instructions.length
            Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__MESSAGE_PACKER_ALREADY_COMPLETE)
          end

          InstructionPlans.assert_valid_max_instructions_per_transaction(max_instructions)
          resolved_max = InstructionPlans.resolve_max_instructions(max_instructions)
          InstructionPlans.assert_max_instructions_per_transaction(message.instructions.length + 1, resolved_max)

          original_size = Transactions.get_transaction_message_size(message)
          packed        = T.let(message, TransactionMessages::TransactionMessage)
          start_idx     = idx

          (idx...instructions.length).each do |i|
            # Stop once the message is full, before compiling a message that would exceed
            # the instruction limit (which would raise). The assertion above guarantees at
            # least the first instruction fits, so reaching the limit here is a graceful stop.
            if packed.instructions.length >= resolved_max
              idx = i
              return packed
            end

            next_packed = TransactionMessages.append_instructions(packed, [T.must(instructions[i])])
            size        = Transactions.get_transaction_message_size(next_packed)

            if size > Transactions::TRANSACTION_SIZE_LIMIT
              if i == start_idx
                Kernel.raise SolanaError.new(
                  SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
                  {
                    num_bytes_required: size - original_size,
                    num_free_bytes:     Transactions::TRANSACTION_SIZE_LIMIT - original_size
                  }
                )
              end
              idx = i
              return packed
            end

            packed = next_packed
          end

          idx = instructions.length
          packed
        }
      )
    }
  )
end

.get_realloc_message_packer_instruction_plan(total_size:, get_instruction:) ⇒ Object



251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 251

def get_realloc_message_packer_instruction_plan(total_size:, get_instruction:)
  realloc_limit      = 10_240
  num_instructions   = (total_size.to_f / realloc_limit).ceil
  last_size          = total_size % realloc_limit

  instructions = num_instructions.times.map do |i|
    chunk = (i == num_instructions - 1) ? last_size : realloc_limit
    get_instruction.call(chunk)
  end

  get_message_packer_instruction_plan_from_instructions(instructions)
end

.non_divisible_sequential_instruction_plan(plans) ⇒ Object



120
121
122
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 120

def non_divisible_sequential_instruction_plan(plans)
  SequentialInstructionPlan.new(plans: parse_single_instruction_plans(plans), divisible: false)
end

.non_divisible_sequential_transaction_plan(plans) ⇒ Object



64
65
66
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 64

def non_divisible_sequential_transaction_plan(plans)
  SequentialTransactionPlan.new(plans: parse_single_transaction_plans(plans), divisible: false)
end

.non_divisible_sequential_transaction_plan_result(plans) ⇒ Object



89
90
91
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 89

def non_divisible_sequential_transaction_plan_result(plans)
  SequentialTransactionPlanResult.new(plans: plans, divisible: false)
end

.parallel_instruction_plan(plans) ⇒ Object



127
128
129
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 127

def parallel_instruction_plan(plans)
  ParallelInstructionPlan.new(plans: parse_single_instruction_plans(plans))
end

.parallel_transaction_plan(plans) ⇒ Object



72
73
74
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 72

def parallel_transaction_plan(plans)
  ParallelTransactionPlan.new(plans: parse_single_transaction_plans(plans))
end

.parallel_transaction_plan_result(plans) ⇒ Object



95
96
97
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 95

def parallel_transaction_plan_result(plans)
  ParallelTransactionPlanResult.new(plans: plans)
end

.resolve_max_instructions(max_instructions) ⇒ Object



25
26
27
# File 'lib/solana/ruby/kit/instruction_plans/max_instructions.rb', line 25

def resolve_max_instructions(max_instructions)
  max_instructions || DEFAULT_MAX_INSTRUCTIONS_PER_TRANSACTION
end

.sequential_instruction_plan(plans) ⇒ Object



113
114
115
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 113

def sequential_instruction_plan(plans)
  SequentialInstructionPlan.new(plans: parse_single_instruction_plans(plans), divisible: true)
end

.sequential_transaction_plan(plans) ⇒ Object



57
58
59
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 57

def sequential_transaction_plan(plans)
  SequentialTransactionPlan.new(plans: parse_single_transaction_plans(plans), divisible: true)
end

.sequential_transaction_plan_result(plans) ⇒ Object



83
84
85
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 83

def sequential_transaction_plan_result(plans)
  SequentialTransactionPlanResult.new(plans: plans, divisible: true)
end

.single_instruction_plan(instruction) ⇒ Object



105
106
107
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 105

def single_instruction_plan(instruction)
  SingleInstructionPlan.new(instruction: instruction)
end

.single_transaction_plan(message) ⇒ Object



49
50
51
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 49

def single_transaction_plan(message)
  SingleTransactionPlan.new(message: message)
end

.successful_single_transaction_plan_result(message, transaction = nil, context = {}) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 112

def successful_single_transaction_plan_result(message, transaction = nil, context = {})
  from_context = context[:transaction]
  transaction ||= from_context if from_context.is_a?(Transactions::Transaction)

  SingleTransactionPlanResult.new(
    message: message,
    status:  SuccessfulStatus.new(transaction: transaction, context: context.dup.freeze)
  )
end