solana-ruby-kit 7.0.0 → 7.1.1
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/LICENSE +21 -0
- data/README.md +761 -0
- data/lib/solana/ruby/kit/errors.rb +24 -1
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_executor.rb +53 -12
- data/lib/solana/ruby/kit/instruction_plans/transaction_plan_result.rb +33 -17
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +15 -7
- data/lib/solana/ruby/kit/offchain_messages/message_v1.rb +115 -0
- data/lib/solana/ruby/kit/offchain_messages.rb +30 -3
- data/lib/solana/ruby/kit/rpc/api/get_inflation_reward.rb +4 -2
- data/lib/solana/ruby/kit/rpc/api/get_transactions_for_address.rb +208 -0
- data/lib/solana/ruby/kit/rpc/client.rb +2 -0
- data/lib/solana/ruby/kit/subscribable/bridge_store_to_async_iterable.rb +172 -0
- data/lib/solana/ruby/kit/subscribable.rb +1 -0
- data/lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb +22 -5
- data/lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb +5 -3
- data/lib/solana/ruby/kit/version.rb +1 -1
- data/solana-ruby-kit.gemspec +24 -12
- metadata +59 -48
data/README.md
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
# solana-ruby-kit
|
|
2
|
+
|
|
3
|
+
A Ruby port of [@anza-xyz/kit](https://github.com/anza-xyz/kit) — the official Solana TypeScript SDK — translated into idiomatic Ruby with [Sorbet](https://sorbet.org) static types.
|
|
4
|
+
|
|
5
|
+
Every module maps 1-to-1 to a TypeScript package. All methods are synchronous (Ruby's RbNaCl is synchronous; TypeScript's Web Crypto API is not). This port now tracks the anza-xyz/kit version.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby >= 3.2
|
|
10
|
+
- libsodium (required by `rbnacl`)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# macOS
|
|
14
|
+
brew install libsodium
|
|
15
|
+
|
|
16
|
+
# Debian / Ubuntu
|
|
17
|
+
apt-get install libsodium-dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Gemfile
|
|
24
|
+
gem 'solana-ruby-kit'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
Full worked examples live on the [wiki](https://github.com/pzupan/solana-ruby-kit/wiki):
|
|
34
|
+
|
|
35
|
+
- [Quick Start](https://github.com/pzupan/solana-ruby-kit/wiki/Quick-Start)
|
|
36
|
+
- [Create a Wallet](https://github.com/pzupan/solana-ruby-kit/wiki/Create-a-Wallet)
|
|
37
|
+
- [Transfer SOL](https://github.com/pzupan/solana-ruby-kit/wiki/Transfer-SOL)
|
|
38
|
+
- [Create an Associated Token Account](https://github.com/pzupan/solana-ruby-kit/wiki/Create-an-Associated-Token-Account)
|
|
39
|
+
- [Stake SOL with a Validator](https://github.com/pzupan/solana-ruby-kit/wiki/Stake-SOL-with-a-Validator)
|
|
40
|
+
- [Verify a Wallet-Signed Transaction in Rails](https://github.com/pzupan/solana-ruby-kit/wiki/Verify-a-Wallet-Signed-Transaction-in-Rails)
|
|
41
|
+
- [Build a Transaction for Browser Signing](https://github.com/pzupan/solana-ruby-kit/wiki/Build-a-Transaction-for-Browser-Signing)
|
|
42
|
+
|
|
43
|
+
The sections below cover configuration and the per-module API reference.
|
|
44
|
+
|
|
45
|
+
## Rails
|
|
46
|
+
|
|
47
|
+
The gem includes a Railtie that auto-configures when Rails is present. Add it to your `Gemfile` as usual, then run the install generator:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rails generate solana:ruby:kit:install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This creates `config/initializers/ruby_kit.rb`:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
Solana::Ruby::Kit.configure do |config|
|
|
57
|
+
config.rpc_url = 'https://api.mainnet-beta.solana.com'
|
|
58
|
+
config.ws_url = 'wss://api.mainnet-beta.solana.com'
|
|
59
|
+
config.commitment = :confirmed
|
|
60
|
+
config.timeout = 30
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or configure via `config/application.rb`:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
config.ruby_kit.rpc_url = ENV['SOLANA_RPC_URL']
|
|
68
|
+
config.ruby_kit.commitment = :finalized
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Get a pre-configured client anywhere in your app:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
rpc = Solana::Ruby::Kit.rpc_client
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
| Option | Default | Description |
|
|
80
|
+
|--------|---------|-------------|
|
|
81
|
+
| `rpc_url` | `https://api.mainnet-beta.solana.com` | JSON-RPC endpoint |
|
|
82
|
+
| `ws_url` | `nil` | WebSocket endpoint for subscriptions |
|
|
83
|
+
| `commitment` | `:confirmed` | Default commitment level |
|
|
84
|
+
| `timeout` | `30` | HTTP read timeout in seconds |
|
|
85
|
+
|
|
86
|
+
## Modules
|
|
87
|
+
|
|
88
|
+
### `Solana::Ruby::Kit::Addresses` — `@solana/addresses`
|
|
89
|
+
|
|
90
|
+
Validate and work with base58-encoded Solana addresses.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
Addr = Solana::Ruby::Kit::Addresses
|
|
94
|
+
|
|
95
|
+
# Validate
|
|
96
|
+
Addr.address?('11111111111111111111111111111111') # => true
|
|
97
|
+
|
|
98
|
+
# Wrap into a typed Address value object (raises on invalid input)
|
|
99
|
+
addr = Addr.address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
|
|
100
|
+
|
|
101
|
+
# Encode / decode raw bytes
|
|
102
|
+
bytes = Addr.decode_address(addr) # => 32-byte binary String
|
|
103
|
+
str = Addr.encode_address(bytes) # => base58 String
|
|
104
|
+
|
|
105
|
+
# Program Derived Addresses (PDAs)
|
|
106
|
+
program = Addr.address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')
|
|
107
|
+
pda = Addr.get_program_derived_address(
|
|
108
|
+
program_address: program,
|
|
109
|
+
seeds: ['my-seed', [1, 2, 3]]
|
|
110
|
+
)
|
|
111
|
+
puts pda.address # => Address
|
|
112
|
+
puts pda.bump # => Integer (0-255)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `Solana::Ruby::Kit::Keys` — `@solana/keys`
|
|
116
|
+
|
|
117
|
+
Ed25519 key generation, signing, and verification via libsodium.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
Keys = Solana::Ruby::Kit::Keys
|
|
121
|
+
|
|
122
|
+
# Generate
|
|
123
|
+
kp = Keys.generate_key_pair
|
|
124
|
+
kp.signing_key # => RbNaCl::SigningKey
|
|
125
|
+
kp.verify_key # => RbNaCl::VerifyKey
|
|
126
|
+
|
|
127
|
+
# From 64 raw bytes (seed || public key)
|
|
128
|
+
kp = Keys.create_key_pair_from_bytes(File.binread('wallet.bin'))
|
|
129
|
+
|
|
130
|
+
# From 32-byte private seed only
|
|
131
|
+
kp = Keys.create_key_pair_from_private_key_bytes(seed_bytes)
|
|
132
|
+
|
|
133
|
+
# Sign / verify
|
|
134
|
+
sig = Keys.sign_bytes(kp.signing_key, data)
|
|
135
|
+
ok = Keys.verify_signature(kp.verify_key, sig, data)
|
|
136
|
+
|
|
137
|
+
# Base58-encode a signature
|
|
138
|
+
Keys.encode_signature(sig) # => Signature string
|
|
139
|
+
Keys.decode_signature(str) # => SignatureBytes
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `Solana::Ruby::Kit::Signers` — `@solana/signers`
|
|
143
|
+
|
|
144
|
+
High-level signer abstraction that wraps a key pair and exposes an `address`.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
Signers = Solana::Ruby::Kit::Signers
|
|
148
|
+
|
|
149
|
+
# Random new signer
|
|
150
|
+
signer = Signers.generate_key_pair_signer
|
|
151
|
+
signer.address # => Addresses::Address
|
|
152
|
+
signer.to_s # => base58 string
|
|
153
|
+
|
|
154
|
+
# From existing key pair
|
|
155
|
+
signer = Signers.create_signer_from_key_pair(kp)
|
|
156
|
+
|
|
157
|
+
# From raw bytes
|
|
158
|
+
signer = Signers.create_key_pair_signer_from_bytes(bytes_64)
|
|
159
|
+
signer = Signers.create_key_pair_signer_from_private_key_bytes(seed_32)
|
|
160
|
+
|
|
161
|
+
# Sign arbitrary data
|
|
162
|
+
sig = signer.sign(message_bytes)
|
|
163
|
+
|
|
164
|
+
# Sign a batch of messages for multiple signers
|
|
165
|
+
map = Signers.sign_message_bytes_with_signers([signer1, signer2], bytes)
|
|
166
|
+
# => { "addr1" => SignatureBytes, "addr2" => SignatureBytes }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `Solana::Ruby::Kit::TransactionMessages` — `@solana/transaction-messages`
|
|
170
|
+
|
|
171
|
+
Immutable transaction message builder. Every method returns a new struct; originals are unmodified.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
TxMsg = Solana::Ruby::Kit::TransactionMessages
|
|
175
|
+
|
|
176
|
+
msg = TxMsg.create_transaction_message(version: 0)
|
|
177
|
+
|
|
178
|
+
# Set fee payer
|
|
179
|
+
msg = TxMsg.set_fee_payer(signer.address, msg)
|
|
180
|
+
|
|
181
|
+
# Set blockhash lifetime
|
|
182
|
+
constraint = TxMsg::BlockhashLifetimeConstraint.new(
|
|
183
|
+
blockhash: '4vJ9...', last_valid_block_height: 123_456
|
|
184
|
+
)
|
|
185
|
+
msg = TxMsg.set_blockhash_lifetime(constraint, msg)
|
|
186
|
+
|
|
187
|
+
# Append / prepend instructions
|
|
188
|
+
msg = TxMsg.append_instructions(msg, [instruction])
|
|
189
|
+
msg = TxMsg.prepend_instructions(msg, [priority_fee_ix])
|
|
190
|
+
|
|
191
|
+
# Compute unit limit (SetComputeUnitLimit instruction from the Compute Budget program)
|
|
192
|
+
msg = TxMsg.set_transaction_message_compute_unit_limit(200_000, msg)
|
|
193
|
+
TxMsg.get_transaction_message_compute_unit_limit(msg) # => 200_000
|
|
194
|
+
|
|
195
|
+
# Loaded accounts data size limit
|
|
196
|
+
msg = TxMsg.set_transaction_message_loaded_accounts_data_size_limit(64_000, msg)
|
|
197
|
+
TxMsg.get_transaction_message_loaded_accounts_data_size_limit(msg) # => 64_000
|
|
198
|
+
|
|
199
|
+
# Durable nonce lifetime
|
|
200
|
+
nonce_constraint = TxMsg::DurableNonceLifetimeConstraint.new(
|
|
201
|
+
nonce: 'abc...',
|
|
202
|
+
nonce_account_address: nonce_addr
|
|
203
|
+
)
|
|
204
|
+
msg = TxMsg.set_durable_nonce_lifetime(nonce_constraint, msg)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `Solana::Ruby::Kit::Transactions` — `@solana/transactions`
|
|
208
|
+
|
|
209
|
+
Compile, sign, and inspect transactions.
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
Txns = Solana::Ruby::Kit::Transactions
|
|
213
|
+
|
|
214
|
+
# Compile a TransactionMessage into wire bytes + an empty signatures map.
|
|
215
|
+
# message_bytes are the bytes that each required signer must sign.
|
|
216
|
+
# A lifetime constraint (blockhash or durable nonce) is optional at compile
|
|
217
|
+
# time — omitting it writes 32 zero bytes into the blockhash field, which
|
|
218
|
+
# must be replaced before the transaction is valid for submission.
|
|
219
|
+
transaction = Txns.compile_transaction_message(message)
|
|
220
|
+
|
|
221
|
+
# Partially sign (one or more keys, not necessarily all signers)
|
|
222
|
+
tx = Txns.partially_sign_transaction([kp.signing_key], transaction)
|
|
223
|
+
|
|
224
|
+
# Fully sign (raises unless all signers have signed)
|
|
225
|
+
signed_tx = Txns.sign_transaction([kp.signing_key], transaction)
|
|
226
|
+
|
|
227
|
+
# Encode the fully signed transaction for submission via sendTransaction.
|
|
228
|
+
# Prepends compact-u16 signature count + 64-byte signatures to message bytes.
|
|
229
|
+
wire_bytes = Txns.wire_encode_transaction(signed_tx)
|
|
230
|
+
wire_base64 = Base64.strict_encode64(wire_bytes)
|
|
231
|
+
|
|
232
|
+
# Get the transaction signature (fee payer's signature, base58)
|
|
233
|
+
sig = Txns.get_signature_from_transaction(signed_tx)
|
|
234
|
+
|
|
235
|
+
# Check completeness and size
|
|
236
|
+
Txns.fully_signed_transaction?(tx) # => true / false
|
|
237
|
+
Txns.assert_fully_signed_transaction!(tx)
|
|
238
|
+
|
|
239
|
+
Txns.within_size_limit?(tx) # => true if wire size <= 1232 bytes
|
|
240
|
+
Txns.assert_within_size_limit!(tx)
|
|
241
|
+
|
|
242
|
+
# Sendable = fully signed AND within size limit
|
|
243
|
+
Txns.sendable_transaction?(signed_tx) # => true / false
|
|
244
|
+
Txns.assert_sendable_transaction!(signed_tx)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `Solana::Ruby::Kit::Rpc` — `@solana/rpc`
|
|
248
|
+
|
|
249
|
+
Synchronous JSON-RPC client backed by `Net::HTTP`.
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
rpc = Solana::Ruby::Kit::Rpc::Client.new(
|
|
253
|
+
Solana::Ruby::Kit::RpcTypes.devnet,
|
|
254
|
+
timeout: 10,
|
|
255
|
+
open_timeout: 5
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
rpc.get_slot # => Integer
|
|
259
|
+
rpc.get_block_height # => Integer
|
|
260
|
+
rpc.get_balance(address) # resp.value => lamports
|
|
261
|
+
rpc.get_latest_blockhash # resp.value.blockhash, .last_valid_block_height
|
|
262
|
+
rpc.get_account_info(address, encoding: 'base64')
|
|
263
|
+
rpc.get_multiple_accounts([addr1, addr2])
|
|
264
|
+
rpc.get_program_accounts(program_address)
|
|
265
|
+
rpc.get_signature_statuses([sig_str])
|
|
266
|
+
rpc.is_blockhash_valid(blockhash, commitment: :confirmed)
|
|
267
|
+
rpc.get_minimum_balance_for_rent_exemption(data_length)
|
|
268
|
+
rpc.get_transaction(signature, encoding: 'base64')
|
|
269
|
+
rpc.get_token_account_balance(token_account)
|
|
270
|
+
rpc.get_token_accounts_by_owner(owner, mint: mint_address)
|
|
271
|
+
rpc.get_epoch_info
|
|
272
|
+
rpc.get_epoch_schedule
|
|
273
|
+
rpc.get_block_time(slot)
|
|
274
|
+
rpc.get_inflation_reward([address])
|
|
275
|
+
rpc.get_signatures_for_address(address)
|
|
276
|
+
rpc.get_vote_accounts
|
|
277
|
+
rpc.simulate_transaction(encoded_tx)
|
|
278
|
+
rpc.send_transaction(encoded_tx)
|
|
279
|
+
rpc.request_airdrop(address, lamports) # devnet / testnet only
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Errors:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
rescue Solana::Ruby::Kit::Rpc::RpcError => e
|
|
286
|
+
puts e.code # JSON-RPC error code
|
|
287
|
+
puts e.message # JSON-RPC error message
|
|
288
|
+
rescue Solana::Ruby::Kit::Rpc::HttpTransportError => e
|
|
289
|
+
puts e.status_code # HTTP status code
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### `Solana::Ruby::Kit::RpcTypes` — `@solana/rpc-types`
|
|
293
|
+
|
|
294
|
+
Cluster URL helpers and typed wrappers.
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
RpcTypes = Solana::Ruby::Kit::RpcTypes
|
|
298
|
+
|
|
299
|
+
RpcTypes.mainnet # default mainnet URL
|
|
300
|
+
RpcTypes.mainnet('https://my-rpc.com') # custom mainnet URL
|
|
301
|
+
RpcTypes.devnet # devnet
|
|
302
|
+
RpcTypes.testnet # testnet
|
|
303
|
+
RpcTypes.cluster_url('http://localhost:8899') # custom / localnet
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### `Solana::Ruby::Kit::Options` — `@solana/options`
|
|
307
|
+
|
|
308
|
+
Rust-style `Option<T>` for Solana's on-chain option codec pattern.
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
Opts = Solana::Ruby::Kit::Options
|
|
312
|
+
|
|
313
|
+
some = Opts.some(42) # => Some(42)
|
|
314
|
+
none = Opts.none # => None
|
|
315
|
+
|
|
316
|
+
Opts.some?(some) # => true
|
|
317
|
+
Opts.none?(none) # => true
|
|
318
|
+
Opts.option?(some) # => true
|
|
319
|
+
|
|
320
|
+
Opts.unwrap_option(some) # => 42
|
|
321
|
+
Opts.unwrap_option(none) # => nil
|
|
322
|
+
Opts.unwrap_option(none, -> { 0 }) # => 0
|
|
323
|
+
|
|
324
|
+
Opts.wrap_nullable(nil) # => None
|
|
325
|
+
Opts.wrap_nullable(42) # => Some(42)
|
|
326
|
+
|
|
327
|
+
# Recursively unwrap nested structures
|
|
328
|
+
Opts.unwrap_option_recursively({ a: some, b: [none, some] })
|
|
329
|
+
# => { a: 42, b: [nil, 42] }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### `Solana::Ruby::Kit::Functional` — `@solana/functional`
|
|
333
|
+
|
|
334
|
+
Functional pipeline composition.
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
Kit = Solana::Ruby::Kit
|
|
338
|
+
|
|
339
|
+
result = Kit::Functional.pipe(
|
|
340
|
+
Kit::TransactionMessages.create_transaction_message(version: 0),
|
|
341
|
+
->(tx) { Kit::TransactionMessages.set_fee_payer(fee_payer, tx) },
|
|
342
|
+
->(tx) { Kit::TransactionMessages.set_blockhash_lifetime(constraint, tx) },
|
|
343
|
+
->(tx) { Kit::TransactionMessages.append_instructions(tx, [ix]) }
|
|
344
|
+
)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### `Solana::Ruby::Kit::Codecs` — `@solana/codecs`
|
|
348
|
+
|
|
349
|
+
Binary encoder/decoder framework for Solana on-chain data.
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
Codecs = Solana::Ruby::Kit::Codecs
|
|
353
|
+
|
|
354
|
+
# Numbers
|
|
355
|
+
u8 = Codecs.u8
|
|
356
|
+
u16 = Codecs.u16_le # little-endian (default for Solana)
|
|
357
|
+
u32 = Codecs.u32_le
|
|
358
|
+
u64 = Codecs.u64_le
|
|
359
|
+
i8 = Codecs.i8
|
|
360
|
+
f32 = Codecs.f32_le
|
|
361
|
+
|
|
362
|
+
u16.encode(1000) # => "\xe8\x03"
|
|
363
|
+
u16.decode("\xe8\x03") # => 1000
|
|
364
|
+
|
|
365
|
+
# Strings
|
|
366
|
+
utf8 = Codecs.utf8
|
|
367
|
+
bytes = Codecs.bytes_codec
|
|
368
|
+
|
|
369
|
+
# Data structures
|
|
370
|
+
struct_codec = Codecs.struct_codec([
|
|
371
|
+
['amount', u64],
|
|
372
|
+
['mint', bytes]
|
|
373
|
+
])
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `Solana::Ruby::Kit::RpcSubscriptions` — `@solana/rpc-subscriptions`
|
|
377
|
+
|
|
378
|
+
WebSocket-based subscription client.
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
ws = Solana::Ruby::Kit::RpcSubscriptions::Client.new(
|
|
382
|
+
'wss://api.devnet.solana.com'
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
sub = ws.account_subscribe(address, commitment: :confirmed)
|
|
386
|
+
sub.on_message { |notification| puts notification }
|
|
387
|
+
sub.on_error { |err| puts err }
|
|
388
|
+
|
|
389
|
+
ws.account_unsubscribe(sub.id)
|
|
390
|
+
ws.close
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### `Solana::Ruby::Kit::Sysvars` — `@solana/sysvars`
|
|
394
|
+
|
|
395
|
+
Well-known sysvar addresses and decoded account data.
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
Sysvars = Solana::Ruby::Kit::Sysvars
|
|
399
|
+
|
|
400
|
+
Sysvars::Addresses::CLOCK_ADDRESS # => Address
|
|
401
|
+
Sysvars::Addresses::RENT_ADDRESS
|
|
402
|
+
Sysvars::Addresses::EPOCH_SCHEDULE_ADDRESS
|
|
403
|
+
|
|
404
|
+
# Fetch and decode via an RPC client
|
|
405
|
+
clock = Sysvars.fetch_sysvar_clock(rpc)
|
|
406
|
+
clock.slot # => Integer
|
|
407
|
+
clock.epoch # => Integer
|
|
408
|
+
clock.unix_timestamp # => Integer
|
|
409
|
+
|
|
410
|
+
rent = Sysvars.fetch_sysvar_rent(rpc)
|
|
411
|
+
rent.lamports_per_byte_year # => Integer
|
|
412
|
+
rent.exemption_threshold # => Float
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### `Solana::Ruby::Kit::Programs` — `@solana/programs`
|
|
416
|
+
|
|
417
|
+
Program error helpers and well-known program interfaces.
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
Programs = Solana::Ruby::Kit::Programs
|
|
421
|
+
|
|
422
|
+
# Inspect custom program errors in transaction simulation results
|
|
423
|
+
Programs.program_error?(err) # => true / false
|
|
424
|
+
Programs.program_error?(err, expected_code: 1) # match a specific code
|
|
425
|
+
Programs.get_program_error_code(err) # => Integer or nil
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### `Programs::StakeProgram`
|
|
429
|
+
|
|
430
|
+
Create and delegate stake accounts.
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
Stake = Solana::Ruby::Kit::Programs::StakeProgram
|
|
434
|
+
|
|
435
|
+
# Well-known addresses
|
|
436
|
+
Stake::PROGRAM_ID # Stake11111111111111111111111111111111111111
|
|
437
|
+
Stake::STAKE_CONFIG_ID # StakeConfig11111111111111111111111111111111
|
|
438
|
+
Stake::STAKE_ACCOUNT_SPACE # => 200 (bytes required for a stake account)
|
|
439
|
+
|
|
440
|
+
# Build two instructions that allocate and initialise a new stake account.
|
|
441
|
+
# The caller appends both to a transaction message.
|
|
442
|
+
create_ixs = Stake.create_account_instructions(
|
|
443
|
+
from: fee_payer_address, # funding wallet (writable, signer)
|
|
444
|
+
stake_account: stake_keypair.address, # new stake account address (writable, signer)
|
|
445
|
+
authorized: owner_address, # becomes both staker and withdrawer
|
|
446
|
+
lamports: 2_282_880 # enough for rent + some stake
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Build one instruction that delegates an initialised stake account.
|
|
450
|
+
delegate_ix = Stake.delegate_instruction(
|
|
451
|
+
stake_account: stake_keypair.address,
|
|
452
|
+
vote_account: validator_vote_address,
|
|
453
|
+
authorized: owner_address # must sign the transaction
|
|
454
|
+
)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### `Programs::AssociatedTokenAccount`
|
|
458
|
+
|
|
459
|
+
Create SPL token accounts at their canonical (Associated Token Account) address.
|
|
460
|
+
|
|
461
|
+
```ruby
|
|
462
|
+
ATA = Solana::Ruby::Kit::Programs::AssociatedTokenAccount
|
|
463
|
+
|
|
464
|
+
# Well-known program IDs
|
|
465
|
+
ATA::PROGRAM_ID # ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
|
|
466
|
+
ATA::TOKEN_PROGRAM_ID # TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
|
|
467
|
+
ATA::TOKEN_2022_PROGRAM_ID
|
|
468
|
+
ATA::SYSTEM_PROGRAM_ID
|
|
469
|
+
|
|
470
|
+
# Derive the ATA address (no RPC call required)
|
|
471
|
+
pda = ATA.get_associated_token_address(
|
|
472
|
+
wallet: owner_address,
|
|
473
|
+
mint: mint_address,
|
|
474
|
+
token_program_id: ATA::TOKEN_PROGRAM_ID # default; omit for SPL Token
|
|
475
|
+
)
|
|
476
|
+
pda.address # => Addresses::Address (the ATA)
|
|
477
|
+
pda.bump # => Integer
|
|
478
|
+
|
|
479
|
+
# Build the createAssociatedTokenAccount instruction
|
|
480
|
+
ix = ATA.create_instruction(
|
|
481
|
+
payer: fee_payer_address, # pays rent
|
|
482
|
+
wallet: owner_address, # will own the ATA
|
|
483
|
+
mint: mint_address,
|
|
484
|
+
token_program_id: ATA::TOKEN_PROGRAM_ID,
|
|
485
|
+
idempotent: true # use CreateIdempotent — safe to call if ATA exists
|
|
486
|
+
)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### `Solana::Ruby::Kit::OffchainMessages` — `@solana/signers`, `@solana/offchain-messages`
|
|
490
|
+
|
|
491
|
+
Sign and verify off-chain messages (Phantom wallet standard).
|
|
492
|
+
|
|
493
|
+
```ruby
|
|
494
|
+
OffChain = Solana::Ruby::Kit::OffchainMessages
|
|
495
|
+
|
|
496
|
+
msg = OffChain::Message.new(
|
|
497
|
+
version: 1, # 0 = legacy printable ASCII, 1 = extended UTF-8
|
|
498
|
+
domain: 'example.com',
|
|
499
|
+
message: 'Hello, Solana!'
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
encoded = OffChain.encode_offchain_message(msg)
|
|
503
|
+
decoded = OffChain.decode_offchain_message(encoded)
|
|
504
|
+
|
|
505
|
+
signature = OffChain.sign_offchain_message(signer, msg)
|
|
506
|
+
OffChain.verify_offchain_message_signature(verify_key_bytes, signature, msg) # => true
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### Checking that a signer signed what you asked
|
|
510
|
+
|
|
511
|
+
A wallet returns the message bytes it signed alongside its signature. Verifying that
|
|
512
|
+
signature only proves the signer produced it over *those* bytes — not that those bytes
|
|
513
|
+
are the message you asked for. Compare the two before you verify the signature, so a
|
|
514
|
+
signer that signed the wrong thing is reported as a content mismatch rather than as a
|
|
515
|
+
confusing cryptographic failure.
|
|
516
|
+
|
|
517
|
+
```ruby
|
|
518
|
+
Addresses = Solana::Ruby::Kit::Addresses
|
|
519
|
+
|
|
520
|
+
expected = OffChain::MessageV1.new(
|
|
521
|
+
content: 'Transfer 1 SOL to Alice',
|
|
522
|
+
required_signatories: [OffChain::Signatory.new(address: Addresses::Address.new(my_address))]
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Raises SolanaError if the content or the required signatories differ.
|
|
526
|
+
# Required signatories are compared ignoring order; content is compared exactly.
|
|
527
|
+
OffChain.assert_offchain_message_v1_equal(received, expected)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The error context reports content lengths in UTF-8 bytes rather than the content itself,
|
|
531
|
+
so message text never reaches your logs or error reporting.
|
|
532
|
+
|
|
533
|
+
> **Note:** `MessageV1` mirrors the newer `@solana/offchain-messages` v1 shape (UTF-8
|
|
534
|
+
> `content` plus `required_signatories`) and is a distinct type from `Message` above,
|
|
535
|
+
> which models the older `@solana/signers` domain form. The v1 wire codec is not yet
|
|
536
|
+
> translated, so `received` must come from your own decoding for now.
|
|
537
|
+
|
|
538
|
+
### `Solana::Ruby::Kit::ResourceLimitEstimation` — `@solana/kit`
|
|
539
|
+
|
|
540
|
+
Estimate and set compute unit limits and loaded accounts data size limits by simulating
|
|
541
|
+
the transaction before sending.
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
RLE = Solana::Ruby::Kit::ResourceLimitEstimation
|
|
545
|
+
|
|
546
|
+
# 1. Fill provisory (0) limits as placeholders during message construction.
|
|
547
|
+
# This reserves space in the transaction for the limit instructions so the
|
|
548
|
+
# size estimate used by compile_transaction_message is accurate.
|
|
549
|
+
msg = RLE.fill_transaction_message_provisory_resource_limits(msg)
|
|
550
|
+
|
|
551
|
+
# 2. After construction, estimate actual resource usage via simulation and
|
|
552
|
+
# stamp the real values onto the message.
|
|
553
|
+
estimator = RLE.estimate_resource_limits_factory(rpc: rpc)
|
|
554
|
+
# estimate returns { compute_unit_limit: Integer, loaded_accounts_data_size_limit?: Integer }
|
|
555
|
+
estimate = estimator.call(msg)
|
|
556
|
+
|
|
557
|
+
# 3. Or combine steps 2 + set in one call.
|
|
558
|
+
setter = RLE.estimate_and_set_resource_limits_factory(estimator)
|
|
559
|
+
msg = setter.call(msg)
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### `Solana::Ruby::Kit::TransactionConfirmation` — `@solana/transaction-confirmation`
|
|
563
|
+
|
|
564
|
+
Poll for transaction confirmation with timeout.
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
Confirm = Solana::Ruby::Kit::TransactionConfirmation
|
|
568
|
+
|
|
569
|
+
Confirm.wait_for_confirmation(
|
|
570
|
+
rpc,
|
|
571
|
+
sig,
|
|
572
|
+
commitment: :confirmed,
|
|
573
|
+
timeout_secs: 60
|
|
574
|
+
)
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### `Solana::Ruby::Kit::InstructionPlans` — `@solana/instruction-plans`
|
|
578
|
+
|
|
579
|
+
Plan and execute instruction sequences that may span multiple transactions.
|
|
580
|
+
The planner packs instructions into messages respecting the 1232-byte transaction
|
|
581
|
+
size limit; the executor walks the resulting plan tree and sends each transaction.
|
|
582
|
+
|
|
583
|
+
```ruby
|
|
584
|
+
require 'base64'
|
|
585
|
+
Plans = Solana::Ruby::Kit::InstructionPlans
|
|
586
|
+
Kit = Solana::Ruby::Kit
|
|
587
|
+
|
|
588
|
+
# ── 1. Build an instruction plan ─────────────────────────────────────────────
|
|
589
|
+
# Instructions are auto-wrapped in SingleInstructionPlan.
|
|
590
|
+
# Use parallel_instruction_plan for instructions that can run concurrently.
|
|
591
|
+
plan = Plans.sequential_instruction_plan([ix1, ix2, ix3])
|
|
592
|
+
|
|
593
|
+
# ── 2. Create a planner ───────────────────────────────────────────────────────
|
|
594
|
+
# create_transaction_message is called whenever a new (empty) transaction is
|
|
595
|
+
# needed. It must return a message with a fee payer and blockhash already set.
|
|
596
|
+
#
|
|
597
|
+
# max_instructions_per_transaction caps how many top-level instructions the
|
|
598
|
+
# planner packs into a single message. Must be a positive integer no greater
|
|
599
|
+
# than 64 (the transaction format's hard limit); defaults to 16, leaving
|
|
600
|
+
# headroom for inner (CPI) instructions the planner can't see ahead of time.
|
|
601
|
+
planner = Plans.create_transaction_planner(
|
|
602
|
+
create_transaction_message: -> {
|
|
603
|
+
Kit::Functional.pipe(
|
|
604
|
+
Kit::TransactionMessages.create_transaction_message(version: :legacy),
|
|
605
|
+
->(tx) { Kit::TransactionMessages.set_fee_payer(signer.address, tx) },
|
|
606
|
+
->(tx) { Kit::TransactionMessages.set_blockhash_lifetime(constraint, tx) }
|
|
607
|
+
)
|
|
608
|
+
},
|
|
609
|
+
max_instructions_per_transaction: 12 # optional; omit for the default of 16
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# plan! distributes instructions across as few transactions as possible.
|
|
613
|
+
# The cap can also be overridden per call:
|
|
614
|
+
transaction_plan = planner.call(plan, max_instructions_per_transaction: 8)
|
|
615
|
+
|
|
616
|
+
# ── 3. Create an executor and run it ─────────────────────────────────────────
|
|
617
|
+
# execute_transaction_message receives a fresh, mutable context Hash and each
|
|
618
|
+
# fully-packed TransactionMessage, and returns the context a successful result
|
|
619
|
+
# should carry. The two serve different outcomes: what you *store* on the context
|
|
620
|
+
# reaches a failed or canceled result, what you *return* reaches a successful one
|
|
621
|
+
# (merged over what was stored, the returned value winning).
|
|
622
|
+
# If it raises, the executor cancels all remaining messages and re-raises — and the
|
|
623
|
+
# context as it stood at that moment is preserved on the failed result.
|
|
624
|
+
executor = Plans.create_transaction_plan_executor(
|
|
625
|
+
execute_transaction_message: ->(context, message) {
|
|
626
|
+
transaction = Kit::Transactions.compile_transaction_message(message)
|
|
627
|
+
signed = Kit::Transactions.sign_transaction([signer.key_pair.signing_key], transaction)
|
|
628
|
+
context[:transaction] = signed # recorded now, so it survives a failure below
|
|
629
|
+
wire = Base64.strict_encode64(Kit::Transactions.wire_encode_transaction(signed))
|
|
630
|
+
rpc.send_transaction(wire)
|
|
631
|
+
{ transaction: signed }
|
|
632
|
+
}
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
result = executor.call(transaction_plan)
|
|
636
|
+
# result is a TransactionPlanResult tree mirroring the transaction_plan structure.
|
|
637
|
+
# Each leaf is a SingleTransactionPlanResult with status :successful, :failed, or :canceled.
|
|
638
|
+
# Every status carries a #context; a :successful one also exposes #transaction.
|
|
639
|
+
|
|
640
|
+
# A one-argument lambda returning { transaction:, context: } — the shape this method
|
|
641
|
+
# required before v7.1.0 — is still accepted and adapted onto the flow above.
|
|
642
|
+
|
|
643
|
+
# ── 4. Plan types ─────────────────────────────────────────────────────────────
|
|
644
|
+
Plans.single_instruction_plan(ix) # wrap one instruction
|
|
645
|
+
Plans.sequential_instruction_plan([ix1, ix2]) # ordered, divisible
|
|
646
|
+
Plans.non_divisible_sequential_instruction_plan([ix1, ix2]) # must be atomic
|
|
647
|
+
Plans.parallel_instruction_plan([ix1, ix2]) # any order / same tx
|
|
648
|
+
|
|
649
|
+
# MessagePacker plans for instructions of variable size (e.g. large data writes)
|
|
650
|
+
Plans.get_linear_message_packer_instruction_plan(
|
|
651
|
+
total_length: data.bytesize,
|
|
652
|
+
get_instruction: ->(offset, length) { build_write_ix(offset, data[offset, length]) }
|
|
653
|
+
)
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### `Solana::Ruby::Kit::WalletStandard` — `@solana/wallet-standard`
|
|
657
|
+
|
|
658
|
+
Server-side handling of the [Wallet Standard](https://github.com/wallet-standard/wallet-standard)
|
|
659
|
+
`signTransaction` interface. A browser wallet (Phantom, Backpack, Solflare, …) signs a
|
|
660
|
+
transaction and returns wire bytes; Rails decodes those bytes and verifies every Ed25519
|
|
661
|
+
signature without broadcasting — because Solana addresses **are** Ed25519 public keys, no
|
|
662
|
+
additional key lookup is required.
|
|
663
|
+
|
|
664
|
+
Full worked examples: [Verify a Wallet-Signed Transaction in Rails](https://github.com/pzupan/solana-ruby-kit/wiki/Verify-a-Wallet-Signed-Transaction-in-Rails) and [Build a Transaction for Browser Signing](https://github.com/pzupan/solana-ruby-kit/wiki/Build-a-Transaction-for-Browser-Signing).
|
|
665
|
+
|
|
666
|
+
#### Wallet Standard feature constants
|
|
667
|
+
|
|
668
|
+
Use these when building frontend metadata or documenting required wallet capabilities:
|
|
669
|
+
|
|
670
|
+
```ruby
|
|
671
|
+
WS = Solana::Ruby::Kit::WalletStandard
|
|
672
|
+
|
|
673
|
+
WS::SIGN_TRANSACTION # => 'solana:signTransaction'
|
|
674
|
+
WS::SIGN_AND_SEND_TRANSACTION # => 'solana:signAndSendTransaction'
|
|
675
|
+
WS::SIGN_MESSAGE # => 'solana:signMessage'
|
|
676
|
+
WS::CONNECT # => 'standard:connect'
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### `Solana::Ruby::Kit::TransactionIntrospection` — `@solana/transaction-introspection`
|
|
680
|
+
|
|
681
|
+
Decode a confirmed `getTransaction` response and walk its outer and inner (CPI)
|
|
682
|
+
instructions — useful for indexers, explorers, or auditing what a transaction
|
|
683
|
+
actually did on-chain.
|
|
684
|
+
|
|
685
|
+
```ruby
|
|
686
|
+
TI = Solana::Ruby::Kit::TransactionIntrospection
|
|
687
|
+
rpc = Solana::Ruby::Kit.rpc_client
|
|
688
|
+
|
|
689
|
+
rpc_tx = rpc.get_transaction(
|
|
690
|
+
signature,
|
|
691
|
+
encoding: 'base64',
|
|
692
|
+
max_supported_transaction_version: 0
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Decodes 'base64', 'base58', or 'json' getTransaction responses.
|
|
696
|
+
# `transaction` is only present for 'base64'/'base58' (a re-encodable
|
|
697
|
+
# Transactions::Transaction); 'json' responses carry no wire bytes to round-trip.
|
|
698
|
+
decoded = TI.decode_transaction_from_rpc_response(rpc_tx)
|
|
699
|
+
|
|
700
|
+
# Outer instructions only, with account indices resolved to full AccountMetas.
|
|
701
|
+
outer = TI.get_instructions_from_compiled_transaction_message(
|
|
702
|
+
decoded.compiled_message,
|
|
703
|
+
decoded.loaded_addresses
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Every instruction — outer and inner (CPI) — in the order an explorer
|
|
707
|
+
# displays them, each tagged with a `trace` describing its position.
|
|
708
|
+
instructions = TI.walk_instructions(
|
|
709
|
+
compiled_message: decoded.compiled_message,
|
|
710
|
+
loaded_addresses: decoded.loaded_addresses,
|
|
711
|
+
meta: rpc_tx['meta']
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
instructions.each do |traced|
|
|
715
|
+
ix = traced.instruction
|
|
716
|
+
puts "#{traced.trace[:kind]} ix -> #{ix.program_address}"
|
|
717
|
+
end
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
> **Note** `'jsonParsed'` responses are not supported — their instructions are
|
|
721
|
+
> pre-parsed by the server and lack raw bytes, so they can't be resolved here;
|
|
722
|
+
> fetch with `'json'` or `'base64'` instead. Wire-format (`'base64'`/`'base58'`)
|
|
723
|
+
> decoding only handles legacy and v0 transactions — Ruby's transaction compiler
|
|
724
|
+
> doesn't produce v1 transactions yet either, so there is nothing to decompile
|
|
725
|
+
> for that version.
|
|
726
|
+
|
|
727
|
+
## Error handling
|
|
728
|
+
|
|
729
|
+
All errors inherit from `Solana::Ruby::Kit::SolanaError`.
|
|
730
|
+
|
|
731
|
+
```ruby
|
|
732
|
+
rescue Solana::Ruby::Kit::SolanaError => e
|
|
733
|
+
puts e.code # => :SOLANA_ERROR__ADDRESSES__INVALID_BASE58_ENCODED_ADDRESS
|
|
734
|
+
puts e.message # => human-readable description
|
|
735
|
+
puts e.context # => Hash of structured context values
|
|
736
|
+
end
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Error codes match the TypeScript `@solana/errors` package constants.
|
|
740
|
+
|
|
741
|
+
## Type checking with Sorbet
|
|
742
|
+
|
|
743
|
+
Every public method has a `sig` block. To enable static type checking in your project:
|
|
744
|
+
|
|
745
|
+
```bash
|
|
746
|
+
bundle exec srb init
|
|
747
|
+
bundle exec srb tc
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
## Development
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
bundle install
|
|
754
|
+
bundle exec rspec # run tests
|
|
755
|
+
bundle exec srb tc # type-check
|
|
756
|
+
bundle exec tapioca gems # regenerate gem RBI files (first time or after gem updates)
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
## License
|
|
760
|
+
|
|
761
|
+
MIT
|