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.
@@ -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/plans'
7
-
8
- module Solana::Ruby::Kit
9
- module InstructionPlans
10
- extend T::Sig
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.message_bytes)
47
+ Base64.strict_encode64(Transactions.wire_encode_transaction(transaction))
47
48
  end
48
49
 
49
50
  config = { 'encoding' => 'base64' }