solace 0.0.10 → 0.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +124 -24
  3. data/README.md +10 -8
  4. data/lib/solace/composers/base.rb +35 -0
  5. data/lib/solace/composers/spl_token_program_initialize_mint_composer.rb +95 -0
  6. data/lib/solace/composers/spl_token_program_mint_to_composer.rb +86 -0
  7. data/lib/solace/composers/spl_token_program_transfer_composer.rb +90 -0
  8. data/lib/solace/composers/system_program_create_account_composer.rb +98 -0
  9. data/lib/solace/connection.rb +92 -15
  10. data/lib/solace/errors/confirmation_timeout.rb +18 -4
  11. data/lib/solace/errors/http_error.rb +16 -1
  12. data/lib/solace/errors/parse_error.rb +15 -1
  13. data/lib/solace/errors/rpc_error.rb +17 -1
  14. data/lib/solace/errors.rb +8 -3
  15. data/lib/solace/instructions/associated_token_account/create_associated_token_account_instruction.rb +9 -2
  16. data/lib/solace/instructions/spl_token/initialize_mint_instruction.rb +0 -1
  17. data/lib/solace/instructions/spl_token/transfer_instruction.rb +21 -0
  18. data/lib/solace/instructions/system_program/create_account_instruction.rb +30 -0
  19. data/lib/solace/instructions/system_program/transfer_instruction.rb +11 -0
  20. data/lib/solace/programs/associated_token_account.rb +57 -30
  21. data/lib/solace/programs/base.rb +23 -0
  22. data/lib/solace/programs/spl_token.rb +197 -125
  23. data/lib/solace/serializers/base_serializer.rb +29 -1
  24. data/lib/solace/tokens/token.rb +53 -0
  25. data/lib/solace/tokens.rb +86 -0
  26. data/lib/solace/transaction.rb +24 -21
  27. data/lib/solace/transaction_composer.rb +77 -3
  28. data/lib/solace/utils/account_context.rb +1 -1
  29. data/lib/solace/utils/codecs.rb +17 -0
  30. data/lib/solace/utils/pda.rb +13 -5
  31. data/lib/solace/version.rb +3 -2
  32. data/lib/solace.rb +38 -11
  33. metadata +21 -1
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Composers
5
+ # Composer for creating an account using the system program.
6
+ #
7
+ # This composer resolves and orders the required accounts for a `CreateAccount` instruction,
8
+ # sets up their access permissions, and delegates construction to the appropriate
9
+ # instruction builder (`Instructions::SystemProgram::CreateAccountInstruction`).
10
+ #
11
+ # It is used for creating new accounts on the Solana blockchain.
12
+ #
13
+ # Required accounts:
14
+ # - **From**: funding account (writable, signer)
15
+ # - **Owner**: owner program id (readonly, non-signer)
16
+ # - **New Account**: new account to create (writable, signer)
17
+ #
18
+ # @example Compose and build a create account instruction
19
+ # composer = SystemProgramCreateAccountComposer.new(
20
+ # from: payer_address,
21
+ # new_account: new_account_address,
22
+ # owner: owner_address,
23
+ # lamports: 1000,
24
+ # space: 1024
25
+ # )
26
+ #
27
+ # @see Instructions::SystemProgram::CreateAccountInstruction
28
+ # @since 0.1.0
29
+ class SystemProgramCreateAccountComposer < Base
30
+ # Extracts the to address from the params
31
+ #
32
+ # @return [String] The from address
33
+ def from
34
+ params[:from].to_s
35
+ end
36
+
37
+ # Extracts the new account address from the params
38
+ #
39
+ # @return [String] The new account address
40
+ def new_account
41
+ params[:new_account].to_s
42
+ end
43
+
44
+ # Returns the system program id
45
+ #
46
+ # @return [String] The system program id
47
+ def system_program
48
+ Solace::Constants::SYSTEM_PROGRAM_ID.to_s
49
+ end
50
+
51
+ # Extracts the owner address from the params
52
+ #
53
+ # @return [String] The owner address
54
+ def owner
55
+ params[:owner].to_s
56
+ end
57
+
58
+ # Returns the lamports to transfer
59
+ #
60
+ # @return [Integer] The lamports to transfer
61
+ def lamports
62
+ params[:lamports]
63
+ end
64
+
65
+ # Returns the space to allocate
66
+ #
67
+ # @return [Integer] The space to allocate
68
+ def space
69
+ params[:space]
70
+ end
71
+
72
+ # Setup accounts required for create account instruction
73
+ # Called automatically during initialization
74
+ #
75
+ # @return [void]
76
+ def setup_accounts
77
+ account_context.add_writable_signer(from)
78
+ account_context.add_writable_signer(new_account)
79
+ account_context.add_readonly_nonsigner(system_program)
80
+ end
81
+
82
+ # Build instruction with resolved account indices
83
+ #
84
+ # @param account_context [Utils::AccountContext] The account context
85
+ # @return [Solace::Instruction]
86
+ def build_instruction(account_context)
87
+ Solace::Instructions::SystemProgram::CreateAccountInstruction.build(
88
+ space: space,
89
+ lamports: lamports,
90
+ owner: owner,
91
+ from_index: account_context.index_of(from),
92
+ new_account_index: account_context.index_of(new_account),
93
+ system_program_index: account_context.index_of(system_program)
94
+ )
95
+ end
96
+ end
97
+ end
98
+ end
@@ -42,6 +42,12 @@ module Solace
42
42
  # The default options for RPC requests
43
43
  attr_reader :default_options
44
44
 
45
+ # Last fetched blockhash
46
+ attr_reader :last_fetched_blockhash
47
+
48
+ # Last fetched blockhash timestamp
49
+ attr_reader :last_fetched_block_height
50
+
45
51
  # Initialize the connection with a default or custom RPC URL
46
52
  #
47
53
  # @param rpc_url [String] The URL of the Solana RPC node
@@ -49,11 +55,13 @@ module Solace
49
55
  # @return [Solace::Connection] The connection object
50
56
  # @param [Integer] http_open_timeout The timeout for opening an HTTP connection
51
57
  # @param [Integer] http_read_timeout The timeout for reading an HTTP response
58
+ # @param [String] encoding The encoding for the RPC requests
52
59
  def initialize(
53
60
  rpc_url = 'http://localhost:8899',
54
61
  commitment: 'processed',
55
62
  http_open_timeout: 30,
56
- http_read_timeout: 60
63
+ http_read_timeout: 60,
64
+ encoding: 'base64'
57
65
  )
58
66
  # Initialize the RPC client
59
67
  @rpc_client = Utils::RPCClient.new(
@@ -65,10 +73,31 @@ module Solace
65
73
  # Set default options for rpc requests
66
74
  @default_options = {
67
75
  commitment: commitment,
68
- encoding: 'base64'
76
+ encoding: encoding
69
77
  }
70
78
  end
71
79
 
80
+ # Gets the version of the Solana node
81
+ #
82
+ # @return [Hash] The version information
83
+ def get_version
84
+ @rpc_client.rpc_request('getVersion')['result']
85
+ end
86
+
87
+ # Get the health status of the Solana node
88
+ #
89
+ # @return [String] The health status
90
+ def get_health
91
+ @rpc_client.rpc_request('getHealth')['result']
92
+ end
93
+
94
+ # Get the genesis hash of the Solana node
95
+ #
96
+ # @return [String] The genesis hash
97
+ def get_genesis_hash
98
+ @rpc_client.rpc_request('getGenesisHash')['result']
99
+ end
100
+
72
101
  # Request an airdrop of lamports to a given address
73
102
  #
74
103
  # @param pubkey [String] The public key of the account to receive the airdrop
@@ -97,12 +126,34 @@ module Solace
97
126
 
98
127
  # Get the latest blockhash from the Solana node
99
128
  #
129
+ # Stores the last fetched blockhash and last valid block height in the connection, as
130
+ # different clients may need to access these values.
131
+ #
132
+ # @example
133
+ # # Initialize the connection
134
+ # connection = Solace::Connection.new('http://localhost:8899', commitment: 'confirmed')
135
+ #
136
+ # # Get the latest blockhash
137
+ # blockhash, last_valid_block_height = connection.get_latest_blockhash
138
+ #
139
+ # puts "Latest blockhash: #{blockhash}"
140
+ # puts "Last valid block height: #{last_valid_block_height}"
141
+ #
142
+ # # Access the last fetched blockhash and height from the connection
143
+ # puts "Stored blockhash: #{connection.last_fetched_blockhash}"
144
+ # puts "Stored last valid block height: #{connection.last_fetched_block_height}"
145
+ #
100
146
  # @return [Array<String, Integer>] The latest blockhash and lastValidBlockHeight
101
147
  def get_latest_blockhash
102
- @rpc_client
103
- .rpc_request('getLatestBlockhash', [build_get_latest_blockhash_options])
104
- .dig('result', 'value')
105
- .values_at('blockhash', 'lastValidBlockHeight')
148
+ result = @rpc_client
149
+ .rpc_request('getLatestBlockhash', [build_get_latest_blockhash_options])
150
+ .dig('result', 'value')
151
+
152
+ # Set the last fetched blockhash and last valid block height in the connection
153
+ @last_fetched_blockhash, @last_fetched_block_height = result.values_at('blockhash', 'lastValidBlockHeight')
154
+
155
+ # Return the blockhash and last valid block height
156
+ [@last_fetched_blockhash, @last_fetched_block_height]
106
157
  end
107
158
 
108
159
  # Get the minimum required lamports for rent exemption
@@ -121,6 +172,14 @@ module Solace
121
172
  @rpc_client.rpc_request('getAccountInfo', [pubkey, default_options]).dig('result', 'value')
122
173
  end
123
174
 
175
+ # Get multiple accounts information from the Solana node
176
+ #
177
+ # @param pubkeys [Array<String>] The public keys of the accounts
178
+ # @return [Array<Object>] The accounts information
179
+ def get_multiple_accounts(pubkeys)
180
+ @rpc_client.rpc_request('getMultipleAccounts', [pubkeys, default_options]).dig('result', 'value')
181
+ end
182
+
124
183
  # Get the balance of a specific account
125
184
  #
126
185
  # @param pubkey [String] The public key of the account
@@ -137,10 +196,19 @@ module Solace
137
196
  @rpc_client.rpc_request('getTokenAccountBalance', [token_account, default_options]).dig('result', 'value')
138
197
  end
139
198
 
199
+ # Gets the token accounts by owner
200
+ #
201
+ # @param owner [String] The public key of the owner
202
+ # @return [Array<Hash>] The token accounts owned by the owner for the specified mint
203
+ def get_token_accounts_by_owner(owner)
204
+ params = [owner, { programId: Constants::TOKEN_PROGRAM_ID }, default_options]
205
+ @rpc_client.rpc_request('getTokenAccountsByOwner', params).dig('result', 'value')
206
+ end
207
+
140
208
  # Get the transaction by signature
141
209
  #
142
210
  # @param signature [String] The signature of the transaction
143
- # @return [Solace::Transaction] The transaction object
211
+ # @return [Transaction] The transaction object
144
212
  # @param [Hash{Symbol => Object}] options
145
213
  def get_transaction(signature, options = { maxSupportedTransactionVersion: 0 })
146
214
  @rpc_client.rpc_request('getTransaction', [signature, default_options.merge(options)])['result']
@@ -173,9 +241,9 @@ module Solace
173
241
  get_signature_statuses([signature])
174
242
  end
175
243
 
176
- # Builds send_tranaction options
244
+ # Builds send_transaction options
177
245
  #
178
- # @params [Hash] The overrides for the options
246
+ # @param overrides[Hash] The overrides for the options
179
247
  # @return [Hash] The options for the send_transaction call
180
248
  def build_send_transaction_options(overrides)
181
249
  {
@@ -188,13 +256,22 @@ module Solace
188
256
 
189
257
  # Send a transaction to the Solana node
190
258
  #
191
- # @param transaction [Solace::Transaction] The transaction to send
259
+ # @param transaction [Transaction] The transaction to send
192
260
  # @param [Hash{Symbol => Object}] overrides
193
261
  # @return [String] The signature of the transaction
194
262
  def send_transaction(transaction, overrides = {})
195
263
  @rpc_client.rpc_request('sendTransaction', [transaction, build_send_transaction_options(overrides)])
196
264
  end
197
265
 
266
+ # Simulate a transaction on the Solana node
267
+ #
268
+ # @param transaction [Transaction] The transaction to simulate
269
+ # @param [Hash{Symbol => Object}] overrides
270
+ # @return [Object] The result of the simulation
271
+ def simulate_transaction(transaction, overrides = {})
272
+ @rpc_client.rpc_request('simulateTransaction', [transaction, build_send_transaction_options(overrides)])
273
+ end
274
+
198
275
  # Wait until the yielded signature reaches the desired commitment or timeout.
199
276
  #
200
277
  # @param commitment [String] One of "processed", "confirmed", "finalized"
@@ -214,7 +291,7 @@ module Solace
214
291
  deadline = monotonic_deadline(timeout)
215
292
 
216
293
  # Wait for confirmation
217
- until dealine_passed?(deadline)
294
+ until deadline_passed?(deadline)
218
295
  return signature if commitment_reached?(signature, commitment)
219
296
 
220
297
  sleep interval
@@ -244,15 +321,15 @@ module Solace
244
321
 
245
322
  # Checks if a timeout deadline has been reached
246
323
  #
247
- # @params deadline [Integer] The deadline for the timeout
248
- # @return [boolean] whether the dealine has passed
249
- def dealine_passed?(deadline)
324
+ # @param deadline [Integer] The deadline for the timeout
325
+ # @return [Boolean] whether the deadline has passed
326
+ def deadline_passed?(deadline)
250
327
  Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
251
328
  end
252
329
 
253
330
  # Sets a deadline given a timeout in seconds
254
331
  #
255
- # @params seconds [Integer] The seconds for the deadline
332
+ # @param seconds [Integer] The seconds for the deadline
256
333
  # @return [Integer] The deadline in seconds
257
334
  def monotonic_deadline(seconds)
258
335
  Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
@@ -2,7 +2,21 @@
2
2
 
3
3
  module Solace
4
4
  module Errors
5
- # Waiting for confirmation exceeded timeout
5
+ # Raised when a transaction confirmation times out.
6
+ #
7
+ # This error is raised when waiting for a transaction to be confirmed by the
8
+ # network exceeds the specified timeout period. This can happen when the
9
+ # network is congested, the transaction fee is too low, or the transaction
10
+ # was not successfully processed by the validators.
11
+ #
12
+ # @example Handling confirmation timeout
13
+ # begin
14
+ # connection.wait_for_confirmed_signature(signature, timeout: 30)
15
+ # rescue Solace::Errors::ConfirmationTimeout => e
16
+ # puts "Transaction confirmation timed out: #{e.message}"
17
+ # end
18
+ #
19
+ # @since 0.0.1
6
20
  class ConfirmationTimeout < StandardError
7
21
  attr_reader :signature, :commitment, :timeout
8
22
 
@@ -19,9 +33,9 @@ module Solace
19
33
 
20
34
  # Formats a confirmation timeout error
21
35
  #
22
- # @params [String] signature The signature of the transaction
23
- # @params [String] commitment The commitment level not reached
24
- # @params [Integer] timeout The time out reached
36
+ # @param signature [String] The signature of the transaction
37
+ # @param commitment [String] The commitment level not reached
38
+ # @param timeout [Integer] The time out reached
25
39
  # @return [Solace::Errors::ConfirmationTimeout] The formatted error
26
40
  def self.format(signature, commitment, timeout)
27
41
  new(
@@ -2,7 +2,22 @@
2
2
 
3
3
  module Solace
4
4
  module Errors
5
- # Non-2xx HTTP or low-level network issues
5
+ # Raised when an HTTP request to the RPC node fails.
6
+ #
7
+ # This error is raised for network-level failures when communicating with the
8
+ # Solana RPC node, including connection timeouts, DNS resolution failures,
9
+ # and HTTP protocol errors. This is distinct from RPC-level errors, which are
10
+ # raised as {Solace::Errors::RPCError}.
11
+ #
12
+ # @example Handling HTTP errors
13
+ # begin
14
+ # connection.get_account_info(address)
15
+ # rescue Solace::Errors::HTTPError => e
16
+ # puts "Network error: #{e.message}"
17
+ # end
18
+ #
19
+ # @see Solace::Errors::RPCError
20
+ # @since 0.0.1
6
21
  class HTTPError < StandardError
7
22
  attr_reader :code, :body
8
23
 
@@ -2,7 +2,21 @@
2
2
 
3
3
  module Solace
4
4
  module Errors
5
- # JSON parsing failed
5
+ # Raised when parsing or deserializing data fails.
6
+ #
7
+ # This error is raised when the gem encounters data that cannot be properly
8
+ # parsed or deserialized, such as malformed JSON responses from the RPC node,
9
+ # invalid binary data, or unexpected data structures. This typically indicates
10
+ # either a bug in the gem or an incompatibility with the RPC node's response format.
11
+ #
12
+ # @example Handling parse errors
13
+ # begin
14
+ # transaction = Solace::Transaction.deserialize(binary_data)
15
+ # rescue Solace::Errors::ParseError => e
16
+ # puts "Failed to parse transaction: #{e.message}"
17
+ # end
18
+ #
19
+ # @since 0.0.1
6
20
  class ParseError < StandardError
7
21
  attr_reader :body
8
22
 
@@ -2,7 +2,23 @@
2
2
 
3
3
  module Solace
4
4
  module Errors
5
- # JSON-RPC returned an "error" object
5
+ # Raised when the RPC node returns an error response.
6
+ #
7
+ # This error is raised when the Solana RPC node successfully processes the HTTP
8
+ # request but returns an error in the JSON-RPC response. This includes errors
9
+ # like invalid parameters, insufficient funds, blockhash not found, and other
10
+ # RPC method-specific errors. The error message and code from the RPC response
11
+ # are included in the exception.
12
+ #
13
+ # @example Handling RPC errors
14
+ # begin
15
+ # connection.send_transaction(transaction)
16
+ # rescue Solace::Errors::RPCError => e
17
+ # puts "RPC error (code #{e.code}): #{e.message}"
18
+ # end
19
+ #
20
+ # @see Solace::Errors::HTTPError
21
+ # @since 0.0.1
6
22
  class RPCError < StandardError
7
23
  attr_reader :rpc_code, :rpc_message, :rpc_data
8
24
 
data/lib/solace/errors.rb CHANGED
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solace
4
- # Error handling module
4
+ # The Errors module contains custom exception classes for the Solace gem.
5
5
  #
6
- # This module provides error classes for handling different types of errors that may occur during
7
- # Solana RPC requests and processing transactions.
6
+ # These exceptions provide specific error handling for various failure scenarios
7
+ # when interacting with the Solana blockchain, including:
8
+ # - {Solace::Errors::HTTPError} - HTTP communication failures
9
+ # - {Solace::Errors::RPCError} - RPC method errors returned by the node
10
+ # - {Solace::Errors::ParseError} - Data parsing and deserialization errors
11
+ # - {Solace::Errors::ConfirmationTimeout} - Transaction confirmation timeouts
8
12
  #
13
+ # @see Solace::Connection
9
14
  # @since 0.0.8
10
15
  module Errors
11
16
  # JSON-RPC Errors
@@ -2,6 +2,15 @@
2
2
 
3
3
  module Solace
4
4
  module Instructions
5
+ # The AssociatedTokenAccount module contains instruction builders for the
6
+ # Associated Token Account Program.
7
+ #
8
+ # The Associated Token Account (ATA) Program provides a deterministic way to
9
+ # derive token account addresses for a given wallet and mint. This ensures that
10
+ # each wallet has a single, predictable token account for each token type.
11
+ #
12
+ # @see https://spl.solana.com/associated-token-account
13
+ # @since 0.0.2
5
14
  module AssociatedTokenAccount
6
15
  # Instruction for creating an Associated Token Account.
7
16
  #
@@ -18,8 +27,6 @@ module Solace
18
27
  # token_program_index: 5,
19
28
  # program_index: 6
20
29
  # )
21
- #
22
- # @since 0.0.2
23
30
  class CreateAssociatedTokenAccountInstruction
24
31
  # !@const INSTRUCTION_INDEX
25
32
  # Instruction index for CreateAssociatedTokenAccount
@@ -18,7 +18,6 @@ module Solace
18
18
  # program_index: 3
19
19
  # )
20
20
  #
21
- # @see Solace::Instructions::SystemProgram::CreateAccountInstruction
22
21
  # @since 0.0.2
23
22
  class InitializeMintInstruction
24
23
  # Instruction index for Initialize Mint
@@ -2,6 +2,27 @@
2
2
 
3
3
  module Solace
4
4
  module Instructions
5
+ # The SplToken module contains instruction builders for the SPL Token Program.
6
+ #
7
+ # The SPL Token Program is Solana's standard for fungible and non-fungible tokens.
8
+ # It provides instructions for creating token mints, creating token accounts,
9
+ # minting tokens, transferring tokens, and managing token authorities.
10
+ #
11
+ # This module contains classes that build the low-level instruction data required
12
+ # to interact with the SPL Token Program. Each class corresponds to a specific
13
+ # instruction in the program.
14
+ #
15
+ # @example Building a transfer instruction
16
+ # instruction = Solace::Instructions::SplToken::TransferInstruction.build(
17
+ # source_index: 0,
18
+ # destination_index: 1,
19
+ # owner_index: 2,
20
+ # program_index: 3,
21
+ # amount: 1_000_000
22
+ # )
23
+ #
24
+ # @see https://spl.solana.com/token
25
+ # @since 0.0.2
5
26
  module SplToken
6
27
  # Instruction for transferring SPL tokens.
7
28
  #
@@ -1,6 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solace
4
+ # The Instructions module contains low-level instruction builders for Solana programs.
5
+ #
6
+ # Instructions are the fundamental building blocks of Solana transactions. Each
7
+ # instruction represents a single operation to be executed by an on-chain program.
8
+ # The classes in this module build the binary instruction data and specify the
9
+ # accounts required for each operation.
10
+ #
11
+ # Instructions are organized by program:
12
+ # - {Solace::Instructions::SystemProgram} - System Program instructions
13
+ # - {Solace::Instructions::SplToken} - SPL Token Program instructions
14
+ # - {Solace::Instructions::AssociatedTokenAccount} - Associated Token Account Program instructions
15
+ #
16
+ # Being a low-level primitive, you must build instructions manually.
17
+ #
18
+ # @example Building a system transfer instruction
19
+ #
20
+ # # Assuming the transaction's accounts are ordered as follows:
21
+ # accounts = %w[from_address to_address system_program_id]
22
+ #
23
+ # # Build the instruction by specifying the account indices directly
24
+ # instruction = Solace::Instructions::SystemProgram::TransferInstruction.build(
25
+ # from_index: 0,
26
+ # to_index: 1,
27
+ # program_index: 2,
28
+ # lamports: 1_000_000
29
+ # )
30
+ #
31
+ # @see Solace::Instruction
32
+ # @see Solace::Composers
33
+ # @since 0.0.1
4
34
  module Instructions
5
35
  module SystemProgram
6
36
  # Instruction for creating a new account.
@@ -2,6 +2,17 @@
2
2
 
3
3
  module Solace
4
4
  module Instructions
5
+ # The SystemProgram module contains instruction builders for the System Program.
6
+ #
7
+ # The System Program is Solana's native program for fundamental operations like
8
+ # creating accounts, transferring SOL, and allocating account data. It is the
9
+ # only program that can create new accounts and assign them to other programs.
10
+ #
11
+ # This module contains classes that build the low-level instruction data required
12
+ # to interact with the System Program.
13
+ #
14
+ # @see https://docs.solana.com/developing/runtime-facilities/programs#system-program
15
+ # @since 0.0.1
5
16
  module SystemProgram
6
17
  # Instruction for transferring SOL.
7
18
  #