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) ⇒ Object



110
111
112
113
114
115
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 110

def canceled_single_transaction_plan_result(message)
  SingleTransactionPlanResult.new(
    message: message,
    status:  CanceledStatus.new
  )
end

.create_transaction_plan_executor(execute_transaction_message:) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb', line 27

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) { @transaction_plan_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



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

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) ⇒ Object



99
100
101
102
103
104
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 99

def failed_single_transaction_plan_result(message, error)
  SingleTransactionPlanResult.new(
    message: message,
    status:  FailedStatus.new(error: error)
  )
end

.flatten_instruction_plan(plan) ⇒ Object



254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 254

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



70
71
72
73
74
75
76
77
78
79
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 70

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



128
129
130
131
132
133
134
135
136
137
138
139
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
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 128

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



174
175
176
177
178
179
180
181
182
183
184
185
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
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 174

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, [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



239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 239

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



108
109
110
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 108

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



55
56
57
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 55

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



67
68
69
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 67

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

.parallel_instruction_plan(plans) ⇒ Object



115
116
117
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 115

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

.parallel_transaction_plan(plans) ⇒ Object



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

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

.parallel_transaction_plan_result(plans) ⇒ Object



73
74
75
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 73

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



101
102
103
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 101

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

.sequential_transaction_plan(plans) ⇒ Object



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

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

.sequential_transaction_plan_result(plans) ⇒ Object



61
62
63
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 61

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

.single_instruction_plan(instruction) ⇒ Object



93
94
95
# File 'lib/solana/ruby/kit/instruction_plans/instruction_plan.rb', line 93

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

.single_transaction_plan(message) ⇒ Object



40
41
42
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan.rb', line 40

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

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



85
86
87
88
89
90
# File 'lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb', line 85

def successful_single_transaction_plan_result(message, transaction, context = {})
  SingleTransactionPlanResult.new(
    message: message,
    status:  SuccessfulStatus.new(transaction: transaction, context: context)
  )
end