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,211 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'transaction'
|
|
5
|
+
require_relative '../addresses/address'
|
|
6
|
+
require_relative '../transaction_messages/transaction_message'
|
|
7
|
+
require_relative '../instructions/roles'
|
|
8
|
+
require_relative '../errors'
|
|
9
|
+
|
|
10
|
+
module Solana::Ruby::Kit
|
|
11
|
+
module Transactions
|
|
12
|
+
# Maximum wire-encoded transaction size in bytes (Solana protocol limit).
|
|
13
|
+
TRANSACTION_SIZE_LIMIT = T.let(1232, Integer)
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Returns the compiled byte-size of a transaction message.
|
|
18
|
+
# Mirrors `getTransactionMessageSize()` from @solana/transactions.
|
|
19
|
+
# The message must have a fee payer and a blockhash lifetime set.
|
|
20
|
+
sig { params(message: TransactionMessages::TransactionMessage).returns(Integer) }
|
|
21
|
+
def get_transaction_message_size(message)
|
|
22
|
+
compile_transaction_message(message).message_bytes.bytesize
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# compile_transaction_message
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Compiles a TransactionMessage into a Transaction ready for signing.
|
|
29
|
+
#
|
|
30
|
+
# Only legacy (version: :legacy) transactions are supported; the serialised
|
|
31
|
+
# format follows the Solana on-wire message layout:
|
|
32
|
+
#
|
|
33
|
+
# [header: 3 bytes]
|
|
34
|
+
# [compact-u16 account count] [32-byte addresses …]
|
|
35
|
+
# [recent blockhash: 32 bytes]
|
|
36
|
+
# [compact-u16 instruction count]
|
|
37
|
+
# [for each instruction:
|
|
38
|
+
# [program_id_index: u8]
|
|
39
|
+
# [compact-u16 account count] [account indices: u8 …]
|
|
40
|
+
# [compact-u16 data length] [data bytes]
|
|
41
|
+
# ]
|
|
42
|
+
#
|
|
43
|
+
# The returned Transaction contains:
|
|
44
|
+
# - message_bytes — the serialised message (the bytes that are signed)
|
|
45
|
+
# - signatures — an ordered hash of signer_address → nil (unfilled)
|
|
46
|
+
#
|
|
47
|
+
# Use wire_encode_transaction to prepend the signatures section before
|
|
48
|
+
# submitting to the RPC node via sendTransaction.
|
|
49
|
+
sig { params(message: TransactionMessages::TransactionMessage).returns(Transaction) }
|
|
50
|
+
def compile_transaction_message(message)
|
|
51
|
+
Kernel.raise SolanaError.new(:SOLANA_ERROR__TRANSACTION__FEE_PAYER_MISSING) if message.fee_payer.nil?
|
|
52
|
+
fee_payer = T.must(message.fee_payer)
|
|
53
|
+
|
|
54
|
+
# ── 1. Collect accounts and merge roles ────────────────────────────────
|
|
55
|
+
# Insertion-ordered hash: address_str → merged AccountRole integer.
|
|
56
|
+
account_roles = T.let({}, T::Hash[String, Integer])
|
|
57
|
+
|
|
58
|
+
# Fee payer is always the first writable signer.
|
|
59
|
+
account_roles[fee_payer.value] = Instructions::AccountRole::WRITABLE_SIGNER
|
|
60
|
+
|
|
61
|
+
message.instructions.each do |ix|
|
|
62
|
+
# The instruction's program address is a readonly non-signer participant.
|
|
63
|
+
prog = ix.program_address.value
|
|
64
|
+
account_roles[prog] ||= Instructions::AccountRole::READONLY
|
|
65
|
+
|
|
66
|
+
(ix.accounts || []).each do |meta|
|
|
67
|
+
addr = meta.address.value
|
|
68
|
+
existing = account_roles[addr] || Instructions::AccountRole::READONLY
|
|
69
|
+
account_roles[addr] = Instructions::AccountRole.merge(existing, meta.role)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ── 2. Partition into the four groups and sort within each ─────────────
|
|
74
|
+
fp = fee_payer.value
|
|
75
|
+
|
|
76
|
+
writable_signers = T.let([], T::Array[String])
|
|
77
|
+
readonly_signers = T.let([], T::Array[String])
|
|
78
|
+
writable_non_signers = T.let([], T::Array[String])
|
|
79
|
+
readonly_non_signers = T.let([], T::Array[String])
|
|
80
|
+
|
|
81
|
+
account_roles.each do |addr, role|
|
|
82
|
+
next if addr == fp # fee payer is handled separately
|
|
83
|
+
|
|
84
|
+
is_signer = Instructions::AccountRole.signer_role?(role)
|
|
85
|
+
is_writable = Instructions::AccountRole.writable_role?(role)
|
|
86
|
+
|
|
87
|
+
if is_signer && is_writable
|
|
88
|
+
writable_signers << addr
|
|
89
|
+
elsif is_signer
|
|
90
|
+
readonly_signers << addr
|
|
91
|
+
elsif is_writable
|
|
92
|
+
writable_non_signers << addr
|
|
93
|
+
else
|
|
94
|
+
readonly_non_signers << addr
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
writable_signers.sort!
|
|
99
|
+
readonly_signers.sort!
|
|
100
|
+
writable_non_signers.sort!
|
|
101
|
+
readonly_non_signers.sort!
|
|
102
|
+
|
|
103
|
+
# Fee payer first, then writable signers, readonly signers, non-signers.
|
|
104
|
+
ordered = [fp] + writable_signers + readonly_signers +
|
|
105
|
+
writable_non_signers + readonly_non_signers
|
|
106
|
+
|
|
107
|
+
# ── 3. Build index lookup ──────────────────────────────────────────────
|
|
108
|
+
account_index = T.let({}, T::Hash[String, Integer])
|
|
109
|
+
ordered.each_with_index { |addr, i| account_index[addr] = i }
|
|
110
|
+
|
|
111
|
+
# ── 4. Message header (3 bytes) ────────────────────────────────────────
|
|
112
|
+
num_required_sigs = 1 + writable_signers.size + readonly_signers.size
|
|
113
|
+
num_readonly_signed = readonly_signers.size
|
|
114
|
+
num_readonly_unsigned = readonly_non_signers.size
|
|
115
|
+
|
|
116
|
+
header = [num_required_sigs, num_readonly_signed, num_readonly_unsigned].pack('CCC').b
|
|
117
|
+
|
|
118
|
+
# ── 5. Account addresses section ───────────────────────────────────────
|
|
119
|
+
accounts_section = encode_compact_u16(ordered.size)
|
|
120
|
+
ordered.each do |addr_str|
|
|
121
|
+
accounts_section = accounts_section + Addresses.decode_address(Addresses::Address.new(addr_str))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# ── 6. Recent blockhash (32 bytes) ─────────────────────────────────────
|
|
125
|
+
# Mirrors upstream compile-transaction.ts #581:
|
|
126
|
+
# - BlockhashLifetime → use the blockhash bytes
|
|
127
|
+
# - DurableNonceLifetime → use the nonce value bytes (stored in the same field)
|
|
128
|
+
# - No lifetime → 32 zero bytes (placeholder; must be replaced before signing)
|
|
129
|
+
constraint = message.lifetime_constraint
|
|
130
|
+
blockhash_bytes =
|
|
131
|
+
if constraint.is_a?(TransactionMessages::BlockhashLifetimeConstraint)
|
|
132
|
+
Addresses.decode_address(Addresses::Address.new(constraint.blockhash))
|
|
133
|
+
elsif constraint.is_a?(TransactionMessages::DurableNonceLifetimeConstraint)
|
|
134
|
+
Addresses.decode_address(Addresses::Address.new(constraint.nonce))
|
|
135
|
+
else
|
|
136
|
+
("\x00" * 32).b
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# ── 7. Instructions section ────────────────────────────────────────────
|
|
140
|
+
ixs_section = encode_compact_u16(message.instructions.size)
|
|
141
|
+
|
|
142
|
+
message.instructions.each do |ix|
|
|
143
|
+
prog_idx = T.must(account_index[ix.program_address.value])
|
|
144
|
+
ix_accounts = ix.accounts || []
|
|
145
|
+
ix_indices = ix_accounts.map { |m| T.must(account_index[m.address.value]) }
|
|
146
|
+
data_bytes = (ix.data || '').b
|
|
147
|
+
|
|
148
|
+
ixs_section = ixs_section +
|
|
149
|
+
[prog_idx].pack('C').b +
|
|
150
|
+
encode_compact_u16(ix_indices.size) +
|
|
151
|
+
ix_indices.pack('C*').b +
|
|
152
|
+
encode_compact_u16(data_bytes.bytesize) +
|
|
153
|
+
data_bytes
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
message_bytes = (header + accounts_section + blockhash_bytes + ixs_section).b
|
|
157
|
+
|
|
158
|
+
# ── 8. Signatures map (one nil slot per required signer) ───────────────
|
|
159
|
+
signer_addresses = [fp] + writable_signers + readonly_signers
|
|
160
|
+
signatures = T.let({}, T::Hash[String, T.nilable(String)])
|
|
161
|
+
signer_addresses.each { |addr| signatures[addr] = nil }
|
|
162
|
+
|
|
163
|
+
Transaction.new(message_bytes: message_bytes, signatures: signatures)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# wire_encode_transaction
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Encodes a Transaction (signed or partially signed) into the full Solana
|
|
170
|
+
# on-wire format that the RPC node's sendTransaction method expects:
|
|
171
|
+
#
|
|
172
|
+
# [compact-u16 signature count]
|
|
173
|
+
# [64-byte signature or 64 zero bytes if nil] × count
|
|
174
|
+
# [message bytes]
|
|
175
|
+
#
|
|
176
|
+
# The result is a binary String; base64-encode it before sending via HTTP.
|
|
177
|
+
sig { params(transaction: Transaction).returns(String) }
|
|
178
|
+
def wire_encode_transaction(transaction)
|
|
179
|
+
sigs = transaction.signatures
|
|
180
|
+
header = encode_compact_u16(sigs.size)
|
|
181
|
+
sig_bytes = sigs.values.map { |s| (s || ("\x00" * 64)).b }.join
|
|
182
|
+
|
|
183
|
+
(header + sig_bytes + transaction.message_bytes).b
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Private helpers
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
# Encodes a non-negative integer using Solana's compact-u16 format.
|
|
191
|
+
# 0–127 → 1 byte
|
|
192
|
+
# 128–16383 → 2 bytes
|
|
193
|
+
# 16384+ → 3 bytes (max value 0x7fff used in practice)
|
|
194
|
+
sig { params(value: Integer).returns(String) }
|
|
195
|
+
def encode_compact_u16(value)
|
|
196
|
+
result = T.let([], T::Array[Integer])
|
|
197
|
+
remaining = value
|
|
198
|
+
|
|
199
|
+
Kernel.loop do
|
|
200
|
+
byte = remaining & 0x7f
|
|
201
|
+
remaining = remaining >> 7
|
|
202
|
+
byte |= 0x80 if remaining > 0
|
|
203
|
+
result << byte
|
|
204
|
+
break if remaining == 0
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
result.pack('C*').b
|
|
208
|
+
end
|
|
209
|
+
private_class_method :encode_compact_u16
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -67,6 +67,42 @@ module Solana::Ruby::Kit
|
|
|
67
67
|
)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Returns true if the wire-encoded transaction fits within TRANSACTION_SIZE_LIMIT.
|
|
71
|
+
# Mirrors `isTransactionWithinSizeLimit(transaction)`.
|
|
72
|
+
sig { params(transaction: Transaction).returns(T::Boolean) }
|
|
73
|
+
def within_size_limit?(transaction)
|
|
74
|
+
wire_encode_transaction(transaction).bytesize <= TRANSACTION_SIZE_LIMIT
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Raises SolanaError if the wire-encoded transaction exceeds TRANSACTION_SIZE_LIMIT.
|
|
78
|
+
# Mirrors `assertIsTransactionWithinSizeLimit(transaction)`.
|
|
79
|
+
sig { params(transaction: Transaction).void }
|
|
80
|
+
def assert_within_size_limit!(transaction)
|
|
81
|
+
actual = wire_encode_transaction(transaction).bytesize
|
|
82
|
+
return if actual <= TRANSACTION_SIZE_LIMIT
|
|
83
|
+
|
|
84
|
+
Kernel.raise SolanaError.new(
|
|
85
|
+
SolanaError::TRANSACTIONS__EXCEEDS_SIZE_LIMIT,
|
|
86
|
+
{ actual_size: actual, limit: TRANSACTION_SIZE_LIMIT }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns true if the transaction is both fully signed and within the size limit.
|
|
91
|
+
# Mirrors `isSendableTransaction(transaction)`.
|
|
92
|
+
# sendableTransaction = FullySignedTransaction & TransactionWithinSizeLimit
|
|
93
|
+
sig { params(transaction: Transaction).returns(T::Boolean) }
|
|
94
|
+
def sendable_transaction?(transaction)
|
|
95
|
+
fully_signed_transaction?(transaction) && within_size_limit?(transaction)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Raises SolanaError unless the transaction is fully signed and within size limit.
|
|
99
|
+
# Mirrors `assertIsSendableTransaction(transaction)`.
|
|
100
|
+
sig { params(transaction: Transaction).void }
|
|
101
|
+
def assert_sendable_transaction!(transaction)
|
|
102
|
+
assert_fully_signed_transaction!(transaction)
|
|
103
|
+
assert_within_size_limit!(transaction)
|
|
104
|
+
end
|
|
105
|
+
|
|
70
106
|
# Signs a transaction with one or more RbNaCl::SigningKey objects.
|
|
71
107
|
# Only keys whose address appears in `transaction.signatures` are applied.
|
|
72
108
|
# Raises SolanaError if a key is not expected to sign this transaction.
|
data/solana-ruby-kit.gemspec
CHANGED
|
@@ -5,7 +5,7 @@ require_relative 'lib/solana/ruby/kit/version'
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = 'solana-ruby-kit'
|
|
7
7
|
spec.version = Solana::Ruby::Kit::VERSION
|
|
8
|
-
spec.authors = ['Paul Zupan']
|
|
8
|
+
spec.authors = ['Paul Zupan, Idhra Inc.']
|
|
9
9
|
spec.summary = 'Ruby port of the Anza TypeScript SDK (@anza-xyz/kit)'
|
|
10
10
|
spec.license = 'MIT'
|
|
11
11
|
spec.required_ruby_version = '>= 3.2.0'
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solana-ruby-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Paul Zupan
|
|
7
|
+
- Paul Zupan, Idhra Inc.
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-04-09 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: openssl
|
|
@@ -208,7 +208,12 @@ files:
|
|
|
208
208
|
- lib/solana/ruby/kit/fast_stable_stringify.rb
|
|
209
209
|
- lib/solana/ruby/kit/functional.rb
|
|
210
210
|
- lib/solana/ruby/kit/instruction_plans.rb
|
|
211
|
+
- lib/solana/ruby/kit/instruction_plans/instruction_plan.rb
|
|
211
212
|
- lib/solana/ruby/kit/instruction_plans/plans.rb
|
|
213
|
+
- lib/solana/ruby/kit/instruction_plans/transaction_plan.rb
|
|
214
|
+
- lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb
|
|
215
|
+
- lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb
|
|
216
|
+
- lib/solana/ruby/kit/instruction_plans/transaction_planner.rb
|
|
212
217
|
- lib/solana/ruby/kit/instructions.rb
|
|
213
218
|
- lib/solana/ruby/kit/instructions/accounts.rb
|
|
214
219
|
- lib/solana/ruby/kit/instructions/instruction.rb
|
|
@@ -225,6 +230,8 @@ files:
|
|
|
225
230
|
- lib/solana/ruby/kit/options/option.rb
|
|
226
231
|
- lib/solana/ruby/kit/plugin_core.rb
|
|
227
232
|
- lib/solana/ruby/kit/programs.rb
|
|
233
|
+
- lib/solana/ruby/kit/programs/associated_token_account.rb
|
|
234
|
+
- lib/solana/ruby/kit/programs/system_program.rb
|
|
228
235
|
- lib/solana/ruby/kit/promises.rb
|
|
229
236
|
- lib/solana/ruby/kit/railtie.rb
|
|
230
237
|
- lib/solana/ruby/kit/rpc.rb
|
|
@@ -285,6 +292,7 @@ files:
|
|
|
285
292
|
- lib/solana/ruby/kit/transaction_messages.rb
|
|
286
293
|
- lib/solana/ruby/kit/transaction_messages/transaction_message.rb
|
|
287
294
|
- lib/solana/ruby/kit/transactions.rb
|
|
295
|
+
- lib/solana/ruby/kit/transactions/compiler.rb
|
|
288
296
|
- lib/solana/ruby/kit/transactions/transaction.rb
|
|
289
297
|
- lib/solana/ruby/kit/version.rb
|
|
290
298
|
- solana-ruby-kit.gemspec
|