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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative '../transaction_messages/transaction_message'
|
|
6
|
+
require_relative '../transactions/transaction'
|
|
7
|
+
|
|
8
|
+
module Solana::Ruby::Kit
|
|
9
|
+
module InstructionPlans
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
# ── Result status types ───────────────────────────────────────────────────
|
|
13
|
+
#
|
|
14
|
+
# Each single transaction produces one of three statuses:
|
|
15
|
+
# successful: { kind: :successful, transaction:, context: }
|
|
16
|
+
# failed: { kind: :failed, error: }
|
|
17
|
+
# canceled: { kind: :canceled }
|
|
18
|
+
#
|
|
19
|
+
# Mirrors TypeScript's TransactionPlanResultStatus union.
|
|
20
|
+
|
|
21
|
+
class SuccessfulStatus < T::Struct
|
|
22
|
+
const :transaction, Transactions::Transaction
|
|
23
|
+
const :context, T::Hash[T.untyped, T.untyped]
|
|
24
|
+
def kind = :successful
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class FailedStatus < T::Struct
|
|
28
|
+
const :error, SolanaError
|
|
29
|
+
def kind = :failed
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class CanceledStatus < T::Struct
|
|
33
|
+
def kind = :canceled
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ── Result plan types ──────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
class SingleTransactionPlanResult < T::Struct
|
|
39
|
+
const :message, TransactionMessages::TransactionMessage
|
|
40
|
+
const :status, T.untyped # SuccessfulStatus | FailedStatus | CanceledStatus
|
|
41
|
+
def kind = :single
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class SequentialTransactionPlanResult < T::Struct
|
|
45
|
+
const :plans, T::Array[T.untyped] # Array[TransactionPlanResult]
|
|
46
|
+
const :divisible, T::Boolean
|
|
47
|
+
def kind = :sequential
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class ParallelTransactionPlanResult < T::Struct
|
|
51
|
+
const :plans, T::Array[T.untyped] # Array[TransactionPlanResult]
|
|
52
|
+
def kind = :parallel
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
# ── Result factory helpers ────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
# Mirrors `sequentialTransactionPlanResult(plans)`.
|
|
60
|
+
sig { params(plans: T::Array[T.untyped]).returns(SequentialTransactionPlanResult) }
|
|
61
|
+
def sequential_transaction_plan_result(plans)
|
|
62
|
+
SequentialTransactionPlanResult.new(plans: plans, divisible: true)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Mirrors `nonDivisibleSequentialTransactionPlanResult(plans)`.
|
|
66
|
+
sig { params(plans: T::Array[T.untyped]).returns(SequentialTransactionPlanResult) }
|
|
67
|
+
def non_divisible_sequential_transaction_plan_result(plans)
|
|
68
|
+
SequentialTransactionPlanResult.new(plans: plans, divisible: false)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Mirrors `parallelTransactionPlanResult(plans)`.
|
|
72
|
+
sig { params(plans: T::Array[T.untyped]).returns(ParallelTransactionPlanResult) }
|
|
73
|
+
def parallel_transaction_plan_result(plans)
|
|
74
|
+
ParallelTransactionPlanResult.new(plans: plans)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Mirrors `successfulSingleTransactionPlanResult(message, transaction, context)`.
|
|
78
|
+
sig do
|
|
79
|
+
params(
|
|
80
|
+
message: TransactionMessages::TransactionMessage,
|
|
81
|
+
transaction: Transactions::Transaction,
|
|
82
|
+
context: T::Hash[T.untyped, T.untyped]
|
|
83
|
+
).returns(SingleTransactionPlanResult)
|
|
84
|
+
end
|
|
85
|
+
def successful_single_transaction_plan_result(message, transaction, context = {})
|
|
86
|
+
SingleTransactionPlanResult.new(
|
|
87
|
+
message: message,
|
|
88
|
+
status: SuccessfulStatus.new(transaction: transaction, context: context)
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Mirrors `failedSingleTransactionPlanResult(message, error)`.
|
|
93
|
+
sig do
|
|
94
|
+
params(
|
|
95
|
+
message: TransactionMessages::TransactionMessage,
|
|
96
|
+
error: SolanaError
|
|
97
|
+
).returns(SingleTransactionPlanResult)
|
|
98
|
+
end
|
|
99
|
+
def failed_single_transaction_plan_result(message, error)
|
|
100
|
+
SingleTransactionPlanResult.new(
|
|
101
|
+
message: message,
|
|
102
|
+
status: FailedStatus.new(error: error)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Mirrors `canceledSingleTransactionPlanResult(message)`.
|
|
107
|
+
sig do
|
|
108
|
+
params(message: TransactionMessages::TransactionMessage).returns(SingleTransactionPlanResult)
|
|
109
|
+
end
|
|
110
|
+
def canceled_single_transaction_plan_result(message)
|
|
111
|
+
SingleTransactionPlanResult.new(
|
|
112
|
+
message: message,
|
|
113
|
+
status: CanceledStatus.new
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative '../transaction_messages/transaction_message'
|
|
6
|
+
require_relative '../transactions/compiler'
|
|
7
|
+
require_relative 'instruction_plan'
|
|
8
|
+
require_relative 'transaction_plan'
|
|
9
|
+
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module InstructionPlans
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
# Creates a TransactionPlanner — a callable that converts an InstructionPlan
|
|
15
|
+
# into a TransactionPlan by packing instructions into transaction messages.
|
|
16
|
+
#
|
|
17
|
+
# Configuration:
|
|
18
|
+
# create_transaction_message: -> { TransactionMessage }
|
|
19
|
+
# Called whenever a new (empty) message is needed. Must return a message
|
|
20
|
+
# with at least a fee payer and blockhash already set.
|
|
21
|
+
#
|
|
22
|
+
# on_transaction_message_updated: ->(message) { TransactionMessage } (optional)
|
|
23
|
+
# Called after instructions are appended to a message. Must return the
|
|
24
|
+
# (possibly further updated) message. Defaults to identity.
|
|
25
|
+
#
|
|
26
|
+
# The returned planner is a lambda: planner.call(instruction_plan) -> TransactionPlan
|
|
27
|
+
#
|
|
28
|
+
# Mirrors `createTransactionPlanner(config)` from @solana/instruction-plans.
|
|
29
|
+
# NOTE: Ruby translation is synchronous; abort-signal support is omitted.
|
|
30
|
+
sig do
|
|
31
|
+
params(
|
|
32
|
+
create_transaction_message: T.untyped,
|
|
33
|
+
on_transaction_message_updated: T.untyped
|
|
34
|
+
).returns(T.untyped)
|
|
35
|
+
end
|
|
36
|
+
def create_transaction_planner(
|
|
37
|
+
create_transaction_message:,
|
|
38
|
+
on_transaction_message_updated: ->(msg) { msg }
|
|
39
|
+
)
|
|
40
|
+
ctx = {
|
|
41
|
+
create_transaction_message: create_transaction_message,
|
|
42
|
+
on_transaction_message_updated: on_transaction_message_updated
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
->(instruction_plan) {
|
|
46
|
+
mutable = planner_traverse(instruction_plan, ctx.merge(parent: nil, parent_candidates: []))
|
|
47
|
+
Kernel.raise SolanaError.new(SolanaError::INSTRUCTION_PLANS__EMPTY_INSTRUCTION_PLAN) unless mutable
|
|
48
|
+
planner_freeze(mutable)
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module_function :create_transaction_planner
|
|
53
|
+
|
|
54
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
def planner_traverse(plan, ctx)
|
|
59
|
+
case plan.kind
|
|
60
|
+
when :sequential then planner_traverse_sequential(plan, ctx)
|
|
61
|
+
when :parallel then planner_traverse_parallel(plan, ctx)
|
|
62
|
+
when :single then planner_traverse_single(plan, ctx)
|
|
63
|
+
when :message_packer then planner_traverse_message_packer(plan, ctx)
|
|
64
|
+
else
|
|
65
|
+
Kernel.raise SolanaError.new(
|
|
66
|
+
SolanaError::INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND,
|
|
67
|
+
{ kind: plan.kind }
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def planner_traverse_sequential(plan, ctx)
|
|
73
|
+
must_fit_in_parent = ctx[:parent] && (ctx[:parent].kind == :parallel || !plan.divisible)
|
|
74
|
+
|
|
75
|
+
if must_fit_in_parent
|
|
76
|
+
candidate = planner_select_and_mutate_candidate(ctx, ctx[:parent_candidates]) do |msg|
|
|
77
|
+
planner_fit_entire_plan(plan, msg, ctx)
|
|
78
|
+
end
|
|
79
|
+
return nil if candidate
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
candidate = (!must_fit_in_parent && !ctx[:parent_candidates].empty?) ? ctx[:parent_candidates][0] : nil
|
|
83
|
+
tx_plans = []
|
|
84
|
+
|
|
85
|
+
plan.plans.each do |sub_plan|
|
|
86
|
+
sub_ctx = ctx.merge(parent: plan, parent_candidates: candidate ? [candidate] : [])
|
|
87
|
+
tx_plan = planner_traverse(sub_plan, sub_ctx)
|
|
88
|
+
next unless tx_plan
|
|
89
|
+
|
|
90
|
+
candidate = planner_get_sequential_candidate(tx_plan)
|
|
91
|
+
new_plans =
|
|
92
|
+
tx_plan[:kind] == :sequential && (tx_plan[:divisible] || !plan.divisible) \
|
|
93
|
+
? tx_plan[:plans] \
|
|
94
|
+
: [tx_plan]
|
|
95
|
+
tx_plans.concat(new_plans)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return nil if tx_plans.empty?
|
|
99
|
+
return tx_plans[0] if tx_plans.length == 1
|
|
100
|
+
{ kind: :sequential, divisible: plan.divisible, plans: tx_plans }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def planner_traverse_parallel(plan, ctx)
|
|
104
|
+
candidates = ctx[:parent_candidates].dup
|
|
105
|
+
tx_plans = []
|
|
106
|
+
|
|
107
|
+
sorted = plan.plans.sort_by { |p| p.kind == :message_packer ? 1 : 0 }
|
|
108
|
+
|
|
109
|
+
sorted.each do |sub_plan|
|
|
110
|
+
sub_ctx = ctx.merge(parent: plan, parent_candidates: candidates)
|
|
111
|
+
tx_plan = planner_traverse(sub_plan, sub_ctx)
|
|
112
|
+
next unless tx_plan
|
|
113
|
+
|
|
114
|
+
candidates.concat(planner_get_parallel_candidates(tx_plan))
|
|
115
|
+
new_plans = tx_plan[:kind] == :parallel ? tx_plan[:plans] : [tx_plan]
|
|
116
|
+
tx_plans.concat(new_plans)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
return nil if tx_plans.empty?
|
|
120
|
+
return tx_plans[0] if tx_plans.length == 1
|
|
121
|
+
{ kind: :parallel, plans: tx_plans }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def planner_traverse_single(plan, ctx)
|
|
125
|
+
predicate = ->(msg) { TransactionMessages.append_instructions(msg, [plan.instruction]) }
|
|
126
|
+
candidate = planner_select_and_mutate_candidate(ctx, ctx[:parent_candidates], &predicate)
|
|
127
|
+
return nil if candidate
|
|
128
|
+
|
|
129
|
+
msg = planner_create_new_message(ctx, &predicate)
|
|
130
|
+
{ kind: :single, message: msg }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def planner_traverse_message_packer(plan, ctx)
|
|
134
|
+
packer = plan.get_message_packer.call
|
|
135
|
+
tx_plans = []
|
|
136
|
+
candidates = ctx[:parent_candidates].dup
|
|
137
|
+
|
|
138
|
+
until packer.done?
|
|
139
|
+
predicate = ->(msg) { packer.pack_message_to_capacity(msg) }
|
|
140
|
+
candidate = planner_select_and_mutate_candidate(ctx, candidates, &predicate)
|
|
141
|
+
unless candidate
|
|
142
|
+
msg = planner_create_new_message(ctx, &predicate)
|
|
143
|
+
new_plan = { kind: :single, message: msg }
|
|
144
|
+
tx_plans << new_plan
|
|
145
|
+
candidates << new_plan
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return nil if tx_plans.empty?
|
|
150
|
+
return tx_plans[0] if tx_plans.length == 1
|
|
151
|
+
|
|
152
|
+
if ctx[:parent]&.kind == :parallel
|
|
153
|
+
{ kind: :parallel, plans: tx_plans }
|
|
154
|
+
else
|
|
155
|
+
divisible = ctx[:parent]&.kind == :sequential ? ctx[:parent].divisible : true
|
|
156
|
+
{ kind: :sequential, divisible: divisible, plans: tx_plans }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def planner_select_and_mutate_candidate(ctx, candidates, &predicate)
|
|
161
|
+
candidates.each do |candidate|
|
|
162
|
+
begin
|
|
163
|
+
updated = ctx[:on_transaction_message_updated].call(predicate.call(candidate[:message]))
|
|
164
|
+
if Transactions.get_transaction_message_size(updated) <= Transactions::TRANSACTION_SIZE_LIMIT
|
|
165
|
+
candidate[:message] = updated
|
|
166
|
+
return candidate
|
|
167
|
+
end
|
|
168
|
+
rescue SolanaError => e
|
|
169
|
+
next if e.code == SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN
|
|
170
|
+
Kernel.raise
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def planner_create_new_message(ctx, &predicate)
|
|
177
|
+
new_msg = ctx[:create_transaction_message].call
|
|
178
|
+
updated_msg = ctx[:on_transaction_message_updated].call(predicate.call(new_msg))
|
|
179
|
+
updated_size = Transactions.get_transaction_message_size(updated_msg)
|
|
180
|
+
|
|
181
|
+
if updated_size > Transactions::TRANSACTION_SIZE_LIMIT
|
|
182
|
+
new_size = Transactions.get_transaction_message_size(new_msg)
|
|
183
|
+
Kernel.raise SolanaError.new(
|
|
184
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
|
|
185
|
+
{ num_bytes_required: updated_size - new_size,
|
|
186
|
+
num_free_bytes: Transactions::TRANSACTION_SIZE_LIMIT - new_size }
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
updated_msg
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def planner_fit_entire_plan(plan, message, ctx)
|
|
194
|
+
case plan.kind
|
|
195
|
+
when :sequential, :parallel
|
|
196
|
+
plan.plans.reduce(message) { |msg, sub| planner_fit_entire_plan(sub, msg, ctx) }
|
|
197
|
+
when :single
|
|
198
|
+
updated = TransactionMessages.append_instructions(message, [plan.instruction])
|
|
199
|
+
new_size = Transactions.get_transaction_message_size(updated)
|
|
200
|
+
if new_size > Transactions::TRANSACTION_SIZE_LIMIT
|
|
201
|
+
base_size = Transactions.get_transaction_message_size(message)
|
|
202
|
+
Kernel.raise SolanaError.new(
|
|
203
|
+
SolanaError::INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
|
|
204
|
+
{ num_bytes_required: new_size - base_size,
|
|
205
|
+
num_free_bytes: Transactions::TRANSACTION_SIZE_LIMIT - base_size }
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
updated
|
|
209
|
+
when :message_packer
|
|
210
|
+
packer = plan.get_message_packer.call
|
|
211
|
+
msg = message
|
|
212
|
+
msg = packer.pack_message_to_capacity(msg) until packer.done?
|
|
213
|
+
msg
|
|
214
|
+
else
|
|
215
|
+
Kernel.raise SolanaError.new(
|
|
216
|
+
SolanaError::INVARIANT_VIOLATION__INVALID_INSTRUCTION_PLAN_KIND,
|
|
217
|
+
{ kind: plan.kind }
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def planner_get_sequential_candidate(plan)
|
|
223
|
+
return plan if plan[:kind] == :single
|
|
224
|
+
return nil unless plan[:kind] == :sequential && plan[:plans]&.any?
|
|
225
|
+
planner_get_sequential_candidate(plan[:plans].last)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def planner_get_parallel_candidates(plan)
|
|
229
|
+
return [plan] if plan[:kind] == :single
|
|
230
|
+
(plan[:plans] || []).flat_map { |p| planner_get_parallel_candidates(p) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def planner_freeze(plan)
|
|
234
|
+
case plan[:kind]
|
|
235
|
+
when :single
|
|
236
|
+
single_transaction_plan(plan[:message])
|
|
237
|
+
when :sequential
|
|
238
|
+
frozen_plans = plan[:plans].map { |p| planner_freeze(p) }
|
|
239
|
+
plan[:divisible] \
|
|
240
|
+
? sequential_transaction_plan(frozen_plans) \
|
|
241
|
+
: non_divisible_sequential_transaction_plan(frozen_plans)
|
|
242
|
+
when :parallel
|
|
243
|
+
parallel_transaction_plan(plan[:plans].map { |p| planner_freeze(p) })
|
|
244
|
+
else
|
|
245
|
+
Kernel.raise SolanaError.new(
|
|
246
|
+
SolanaError::INVARIANT_VIOLATION__INVALID_TRANSACTION_PLAN_KIND,
|
|
247
|
+
{ kind: plan[:kind] }
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private_class_method :planner_traverse
|
|
253
|
+
private_class_method :planner_traverse_sequential
|
|
254
|
+
private_class_method :planner_traverse_parallel
|
|
255
|
+
private_class_method :planner_traverse_single
|
|
256
|
+
private_class_method :planner_traverse_message_packer
|
|
257
|
+
private_class_method :planner_select_and_mutate_candidate
|
|
258
|
+
private_class_method :planner_create_new_message
|
|
259
|
+
private_class_method :planner_fit_entire_plan
|
|
260
|
+
private_class_method :planner_get_sequential_candidate
|
|
261
|
+
private_class_method :planner_get_parallel_candidates
|
|
262
|
+
private_class_method :planner_freeze
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -3,45 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
# Mirrors @solana/instruction-plans.
|
|
5
5
|
# An InstructionPlan describes operations that may span multiple transactions.
|
|
6
|
-
require_relative 'instruction_plans/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
# Build a plan that wraps a single instruction.
|
|
15
|
-
sig { params(instruction: Instructions::Instruction).returns(SingleInstructionPlan) }
|
|
16
|
-
def single_instruction_plan(instruction)
|
|
17
|
-
SingleInstructionPlan.new(instruction: instruction)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Build a sequential plan from an array of sub-plans.
|
|
21
|
-
sig { params(steps: T::Array[T.untyped], divisible: T::Boolean).returns(SequentialInstructionPlan) }
|
|
22
|
-
def sequential_instruction_plan(steps, divisible: false)
|
|
23
|
-
SequentialInstructionPlan.new(steps: steps, divisible: divisible)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Build a parallel plan from an array of sub-plans.
|
|
27
|
-
sig { params(plans: T::Array[T.untyped]).returns(ParallelInstructionPlan) }
|
|
28
|
-
def parallel_instruction_plan(plans)
|
|
29
|
-
ParallelInstructionPlan.new(plans: plans)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Flatten a plan tree into a single ordered Array of Instructions.
|
|
33
|
-
sig { params(plan: T.untyped).returns(T::Array[Instructions::Instruction]) }
|
|
34
|
-
def flatten_instruction_plan(plan)
|
|
35
|
-
case plan
|
|
36
|
-
when SingleInstructionPlan
|
|
37
|
-
[plan.instruction]
|
|
38
|
-
when SequentialInstructionPlan
|
|
39
|
-
plan.steps.flat_map { |s| flatten_instruction_plan(s) }
|
|
40
|
-
when ParallelInstructionPlan
|
|
41
|
-
plan.plans.flat_map { |p| flatten_instruction_plan(p) }
|
|
42
|
-
else
|
|
43
|
-
Kernel.raise ArgumentError, "Unknown InstructionPlan type: #{plan.class}"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
6
|
+
require_relative 'instruction_plans/instruction_plan'
|
|
7
|
+
require_relative 'instruction_plans/transaction_plan'
|
|
8
|
+
require_relative 'instruction_plans/transaction_plan_result'
|
|
9
|
+
require_relative 'instruction_plans/transaction_planner'
|
|
10
|
+
require_relative 'instruction_plans/transaction_plan_executor'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../addresses/address'
|
|
5
|
+
require_relative '../addresses/program_derived_address'
|
|
6
|
+
require_relative '../instructions/instruction'
|
|
7
|
+
require_relative '../instructions/accounts'
|
|
8
|
+
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module Programs
|
|
11
|
+
# Ruby interface for the SPL Associated Token Account on-chain program.
|
|
12
|
+
#
|
|
13
|
+
# An Associated Token Account (ATA) is a Program Derived Address that holds
|
|
14
|
+
# SPL token balances for a given wallet + mint pair. The ATA address is
|
|
15
|
+
# deterministic: given a wallet and a mint, there is exactly one "canonical"
|
|
16
|
+
# token account for that combination.
|
|
17
|
+
#
|
|
18
|
+
# Seeds used for PDA derivation (all raw 32-byte address buffers):
|
|
19
|
+
# [ wallet_bytes, token_program_bytes, mint_bytes ]
|
|
20
|
+
# Program: PROGRAM_ID (the Associated Token Account program)
|
|
21
|
+
#
|
|
22
|
+
# Reference: https://github.com/solana-program/associated-token-account
|
|
23
|
+
module AssociatedTokenAccount
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
# SPL Associated Token Account program.
|
|
27
|
+
PROGRAM_ID = T.let(
|
|
28
|
+
Addresses::Address.new('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'),
|
|
29
|
+
Addresses::Address
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Original SPL Token program (spl-token).
|
|
33
|
+
TOKEN_PROGRAM_ID = T.let(
|
|
34
|
+
Addresses::Address.new('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
|
|
35
|
+
Addresses::Address
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Token Extensions program (spl-token-2022).
|
|
39
|
+
TOKEN_2022_PROGRAM_ID = T.let(
|
|
40
|
+
Addresses::Address.new('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'),
|
|
41
|
+
Addresses::Address
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Solana System Program.
|
|
45
|
+
SYSTEM_PROGRAM_ID = T.let(
|
|
46
|
+
Addresses::Address.new('11111111111111111111111111111111'),
|
|
47
|
+
Addresses::Address
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
# Derives the canonical Associated Token Account address (a PDA) for a
|
|
53
|
+
# given wallet and mint.
|
|
54
|
+
#
|
|
55
|
+
# @param wallet [Addresses::Address] the account that will own the ATA
|
|
56
|
+
# @param mint [Addresses::Address] the token mint
|
|
57
|
+
# @param token_program_id [Addresses::Address] TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID
|
|
58
|
+
# @return [Addresses::ProgramDerivedAddress] the ATA address + bump seed
|
|
59
|
+
sig do
|
|
60
|
+
params(
|
|
61
|
+
wallet: Addresses::Address,
|
|
62
|
+
mint: Addresses::Address,
|
|
63
|
+
token_program_id: Addresses::Address
|
|
64
|
+
).returns(Addresses::ProgramDerivedAddress)
|
|
65
|
+
end
|
|
66
|
+
def get_associated_token_address(wallet:, mint:, token_program_id: TOKEN_PROGRAM_ID)
|
|
67
|
+
wallet_bytes = Addresses.decode_address(wallet)
|
|
68
|
+
token_program_bytes = Addresses.decode_address(token_program_id)
|
|
69
|
+
mint_bytes = Addresses.decode_address(mint)
|
|
70
|
+
|
|
71
|
+
Addresses.get_program_derived_address(
|
|
72
|
+
program_address: PROGRAM_ID,
|
|
73
|
+
seeds: [wallet_bytes, token_program_bytes, mint_bytes]
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Builds an instruction that creates an Associated Token Account.
|
|
78
|
+
#
|
|
79
|
+
# Account layout expected by the on-chain program (in order):
|
|
80
|
+
# 0. payer — writable, signer (funds the rent)
|
|
81
|
+
# 1. associated_token_account — writable (the new ATA; derived via PDA)
|
|
82
|
+
# 2. wallet — readonly (the ATA's owner)
|
|
83
|
+
# 3. mint — readonly
|
|
84
|
+
# 4. system_program — readonly
|
|
85
|
+
# 5. token_program — readonly
|
|
86
|
+
#
|
|
87
|
+
# Instruction data:
|
|
88
|
+
# nil / empty → "Create" (fails if the ATA already exists)
|
|
89
|
+
# "\x01" → "CreateIdempotent" (no-op if already initialised)
|
|
90
|
+
#
|
|
91
|
+
# @param payer [Addresses::Address] pays for rent
|
|
92
|
+
# @param wallet [Addresses::Address] will own the ATA
|
|
93
|
+
# @param mint [Addresses::Address] token mint
|
|
94
|
+
# @param token_program_id [Addresses::Address] which token program to use
|
|
95
|
+
# @param idempotent [Boolean] use the idempotent variant
|
|
96
|
+
# @return [Instructions::Instruction]
|
|
97
|
+
sig do
|
|
98
|
+
params(
|
|
99
|
+
payer: Addresses::Address,
|
|
100
|
+
wallet: Addresses::Address,
|
|
101
|
+
mint: Addresses::Address,
|
|
102
|
+
token_program_id: Addresses::Address,
|
|
103
|
+
idempotent: T::Boolean
|
|
104
|
+
).returns(Instructions::Instruction)
|
|
105
|
+
end
|
|
106
|
+
def create_instruction(
|
|
107
|
+
payer:,
|
|
108
|
+
wallet:,
|
|
109
|
+
mint:,
|
|
110
|
+
token_program_id: TOKEN_PROGRAM_ID,
|
|
111
|
+
idempotent: false
|
|
112
|
+
)
|
|
113
|
+
ata = get_associated_token_address(wallet: wallet, mint: mint, token_program_id: token_program_id)
|
|
114
|
+
|
|
115
|
+
# Discriminator: none for "Create", 0x01 for "CreateIdempotent"
|
|
116
|
+
data = idempotent ? "\x01".b : nil
|
|
117
|
+
|
|
118
|
+
Instructions::Instruction.new(
|
|
119
|
+
program_address: PROGRAM_ID,
|
|
120
|
+
accounts: [
|
|
121
|
+
Instructions.writable_signer_account(payer), # 0 payer
|
|
122
|
+
Instructions.writable_account(ata.address), # 1 ATA to create
|
|
123
|
+
Instructions.readonly_account(wallet), # 2 owner (readonly)
|
|
124
|
+
Instructions.readonly_account(mint), # 3 mint
|
|
125
|
+
Instructions.readonly_account(SYSTEM_PROGRAM_ID), # 4 System Program
|
|
126
|
+
Instructions.readonly_account(token_program_id), # 5 Token Program
|
|
127
|
+
],
|
|
128
|
+
data: data
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../addresses/address'
|
|
5
|
+
require_relative '../instructions/instruction'
|
|
6
|
+
require_relative '../instructions/accounts'
|
|
7
|
+
|
|
8
|
+
module Solana::Ruby::Kit
|
|
9
|
+
module Programs
|
|
10
|
+
# Ruby interface for the Solana System Program (11111111111111111111111111111111).
|
|
11
|
+
#
|
|
12
|
+
# The System Program is responsible for creating accounts, transferring SOL,
|
|
13
|
+
# and other low-level operations. Instruction data uses little-endian encoding:
|
|
14
|
+
# a u32 discriminator identifying the instruction variant, followed by any
|
|
15
|
+
# instruction-specific fields.
|
|
16
|
+
#
|
|
17
|
+
# Reference: https://github.com/solana-program/system
|
|
18
|
+
module SystemProgram
|
|
19
|
+
extend T::Sig
|
|
20
|
+
|
|
21
|
+
# The System Program address (all zeros, encoded in base58).
|
|
22
|
+
PROGRAM_ID = T.let(
|
|
23
|
+
Addresses::Address.new('11111111111111111111111111111111'),
|
|
24
|
+
Addresses::Address
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Instruction discriminators (u32 little-endian).
|
|
28
|
+
DISCRIMINATOR_TRANSFER = T.let(2, Integer)
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Builds a System Program Transfer instruction that moves +lamports+ from
|
|
33
|
+
# +sender+ to +recipient+.
|
|
34
|
+
#
|
|
35
|
+
# Instruction data layout (12 bytes):
|
|
36
|
+
# [0..3] u32 LE — discriminator (2)
|
|
37
|
+
# [4..11] u64 LE — lamports
|
|
38
|
+
#
|
|
39
|
+
# Accounts:
|
|
40
|
+
# 0. sender — writable, signer (source of SOL)
|
|
41
|
+
# 1. recipient — writable (destination)
|
|
42
|
+
#
|
|
43
|
+
# @param sender [Addresses::Address]
|
|
44
|
+
# @param recipient [Addresses::Address]
|
|
45
|
+
# @param lamports [Integer] amount to transfer (1 SOL = 1_000_000_000 lamports)
|
|
46
|
+
# @return [Instructions::Instruction]
|
|
47
|
+
sig do
|
|
48
|
+
params(
|
|
49
|
+
sender: Addresses::Address,
|
|
50
|
+
recipient: Addresses::Address,
|
|
51
|
+
lamports: Integer
|
|
52
|
+
).returns(Instructions::Instruction)
|
|
53
|
+
end
|
|
54
|
+
def transfer_instruction(sender:, recipient:, lamports:)
|
|
55
|
+
# Pack as: u32 LE discriminator || u64 LE lamports
|
|
56
|
+
data = [DISCRIMINATOR_TRANSFER, lamports].pack('VQ<').b
|
|
57
|
+
|
|
58
|
+
Instructions::Instruction.new(
|
|
59
|
+
program_address: PROGRAM_ID,
|
|
60
|
+
accounts: [
|
|
61
|
+
Instructions.writable_signer_account(sender), # 0 sender
|
|
62
|
+
Instructions.writable_account(recipient), # 1 recipient
|
|
63
|
+
],
|
|
64
|
+
data: data
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
# Mirrors @solana/programs — helpers for inspecting custom program errors
|
|
5
5
|
# returned in Solana transaction failures.
|
|
6
|
+
require_relative 'programs/associated_token_account'
|
|
7
|
+
require_relative 'programs/system_program'
|
|
6
8
|
#
|
|
7
9
|
# A program error in a transaction result looks like:
|
|
8
10
|
# { "InstructionError" => [0, { "Custom" => 1234 }] }
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require 'base64'
|
|
5
5
|
require_relative '../../keys/signatures'
|
|
6
|
+
require_relative '../../transactions/compiler'
|
|
6
7
|
|
|
7
8
|
module Solana::Ruby::Kit
|
|
8
9
|
module Rpc
|
|
@@ -43,7 +44,7 @@ module Solana::Ruby::Kit
|
|
|
43
44
|
transaction
|
|
44
45
|
else
|
|
45
46
|
# Assume it responds to .message_bytes (Transactions::Transaction)
|
|
46
|
-
Base64.strict_encode64(transaction
|
|
47
|
+
Base64.strict_encode64(Transactions.wire_encode_transaction(transaction))
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
config = { 'encoding' => 'base64' }
|