solace 0.0.6 → 0.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6a955af361ac7765d33c34560a09704730c8a394cd74e0bc17196d8cf3cb8eb
4
- data.tar.gz: 3c86565b3f728bdd4b116dff1abc63125d187b2cc6d8b5fcb44c8444c2d15def
3
+ metadata.gz: 18749af83cf5133c767914f6321a473a710caa5b5c3c613529af9547d6fcd079
4
+ data.tar.gz: df4de15d443d6033ea92998f7af6450a0bbeae62b5eb389d86fa6ff839ee6f16
5
5
  SHA512:
6
- metadata.gz: 489042853cdc2ca68d2e35abab66c26bb1363e9ae31fa9816203b6c267d9083361a006cc73610467581ed179999317f0fefc0b09460497d1ac6f10dcb273b5c2
7
- data.tar.gz: bc66a1d998abf224a30365e44ea026b034437cbdc3cf09092bf45147510bf010d8d97178047ee485f088255d86a928a58a752694aa813595e5622d6bd66d1310
6
+ metadata.gz: 8e0aad6a855c4721363f353deeb46ecb68429995db8366f1f475ac98b25fe4caa2616b26f584316e0511e3f8ffb8e0fe7a22d97fea92e1904c6e70834d750477
7
+ data.tar.gz: 47b02239af795269f2e287fe598961b265283a303ac4c37fff0f4f29b6758a60511794dd85a85809edd390580b0aee9590c49a3420c1e0b2610bed4903d05017
data/CHANGELOG CHANGED
@@ -16,6 +16,32 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
16
16
  ### Fixed
17
17
  ```
18
18
 
19
+ ## [0.0.8] - 2025-08-11
20
+
21
+ ### Added
22
+ 1. Added `load` method to `Solace::Constants` to load constants from a YAML file. This method allows for loading constants from a YAML file (i.e. custom program addresses and mint accounts).
23
+ 2. Added `to_s` and `address` method to `Solace::Keypair` and `Solace::PublicKey` to return the public key as a Base58 string.
24
+ 3. Added `get_signature_status` and `get_signature_statuses` to `Solace::Connection`.
25
+ 4. Added `RPCClient` to `Solace::Utils` to handle HTTP requests to a Solana RPC node.
26
+ 5. Added `Errors` module to `Solace::Utils` to handle errors from the HTTP requests made to the Solana RPC node.
27
+
28
+ ### Changed
29
+ 1. All methods that take a `Solace::Keypair` or `Solace::Pubkey` where an address is needed now also accept a plain string address. This prevents the need of creating instances of the classes when all that is needed is the address. This is with the exception of the low-level instruction builders, which only expect the correct data and indicies with no required casting.
30
+ 2. Changed `wait_for_confirmed_signature` method to accept a `timeout`, `interval`, and `commitment` keyword arguments (breaking change on commitment).
31
+
32
+ ### Fixed
33
+ 1. Fixed `get_or_create_address` method in `Solace::Programs::AssociatedTokenAccount` to return the address of the associated token account if it already exists by checking if there is any data at the address.
34
+
35
+ ## [0.0.7] - 2025-08-09
36
+
37
+ ### Added
38
+ 1. Added `AssociatedTokenAccountProgramCreateAccountComposer` with tests.
39
+
40
+ ### Changed
41
+ 1. Updated `AssociatedTokenAccount` to use `AssociatedTokenAccountProgramCreateAccountComposer` and sign the transaction in the `create_associated_token_account` method instead of the `prepare_create_associated_token_account` method.
42
+
43
+ ### Fixed
44
+
19
45
  ## [0.0.6] - 2025-08-07
20
46
 
21
47
  ### Added
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Composers
5
+ # Composer for creating an associated token account program create account instruction
6
+ #
7
+ # This composer resolves and orders the required accounts for a `CreateAssociatedTokenAccount` instruction,
8
+ # sets up their access permissions, and delegates construction to the appropriate
9
+ # instruction builder (`Instructions::AssociatedTokenAccount::CreateAssociatedTokenAccountInstruction`).
10
+ #
11
+ # Required accounts:
12
+ # - **Funder**: the account that will pay for fees and rent.
13
+ # - **Owner**: the account that will own the new ATA.
14
+ # - **ATA**: the address of the new ATA.
15
+ # - **Mint**: the mint address of the token.
16
+ # - **System Program**: the system program id.
17
+ # - **Token Program**: the token program id.
18
+ # - **Associated Token Account Program**: the associated token account program id.
19
+ #
20
+ # @example Compose and build a create account instruction
21
+ # composer = AssociatedTokenAccountProgramCreateAccountComposer.new(
22
+ # funder: funder_address,
23
+ # owner: owner_address,
24
+ # ata_address: ata_address,
25
+ # mint: mint_address
26
+ # )
27
+ #
28
+ # @see Instructions::AssociatedTokenAccount::CreateAssociatedTokenAccountInstruction
29
+ # @since 0.0.7
30
+ class AssociatedTokenAccountProgramCreateAccountComposer < Base
31
+ # Extracts the owner address from the params
32
+ #
33
+ # @return [String] The owner address
34
+ def owner
35
+ params[:owner].to_s
36
+ end
37
+
38
+ # Extracts the mint address from the params
39
+ #
40
+ # @return [String] The mint address
41
+ def mint
42
+ params[:mint].to_s
43
+ end
44
+
45
+ # Extracts the ata_address from the params
46
+ #
47
+ # @return [String] The ata_address
48
+ def ata_address
49
+ params[:ata_address].to_s
50
+ end
51
+
52
+ # Extracts the funder address from the params
53
+ #
54
+ # @return [String] The funder address
55
+ def funder
56
+ params[:funder].to_s
57
+ end
58
+
59
+ # Extracts the system program id from the constants
60
+ #
61
+ # @return [String] The system program id
62
+ def system_program_id
63
+ Constants::SYSTEM_PROGRAM_ID.to_s
64
+ end
65
+
66
+ # Extracts the token program id from the constants
67
+ #
68
+ # @return [String] The token program id
69
+ def token_program_id
70
+ Constants::TOKEN_PROGRAM_ID.to_s
71
+ end
72
+
73
+ # Extracts the associated token account program id from the constants
74
+ #
75
+ # @return [String] The associated token account program id
76
+ def associated_token_account_program_id
77
+ Constants::ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID.to_s
78
+ end
79
+
80
+ # Setup accounts required for associated token account program create account instruction
81
+ # Called automatically during initialization
82
+ #
83
+ # @return [void]
84
+ def setup_accounts # rubocop:disable Metrics/AbcSize
85
+ account_context.add_writable_signer(funder)
86
+ account_context.add_writable_nonsigner(ata_address)
87
+ account_context.add_readonly_nonsigner(owner)
88
+ account_context.add_readonly_nonsigner(mint)
89
+ account_context.add_readonly_nonsigner(system_program_id)
90
+ account_context.add_readonly_nonsigner(token_program_id)
91
+ account_context.add_readonly_nonsigner(associated_token_account_program_id)
92
+ end
93
+
94
+ # Builds the instruction for the associated token account program create account instruction
95
+ #
96
+ # @param account_context [Utils::AccountContext] The account context
97
+ # @return [Solace::Instruction] The instruction
98
+ def build_instruction(account_context)
99
+ Instructions::AssociatedTokenAccount::CreateAssociatedTokenAccountInstruction.build(
100
+ funder_index: account_context.index_of(funder),
101
+ owner_index: account_context.index_of(owner),
102
+ mint_index: account_context.index_of(mint),
103
+ associated_token_account_index: account_context.index_of(ata_address),
104
+ system_program_index: account_context.index_of(system_program_id),
105
+ token_program_index: account_context.index_of(token_program_id),
106
+ program_index: account_context.index_of(associated_token_account_program_id)
107
+ )
108
+ end
109
+ end
110
+ end
111
+ end
@@ -34,14 +34,14 @@ module Solace
34
34
  #
35
35
  # @return [String] The to address
36
36
  def to
37
- params[:to].is_a?(String) ? params[:to] : params[:to].address
37
+ params[:to].to_s
38
38
  end
39
39
 
40
40
  # Extracts the from address from the params
41
41
  #
42
42
  # @return [String] The from address
43
43
  def from
44
- params[:from].is_a?(String) ? params[:from] : params[:from].address
44
+ params[:from].to_s
45
45
  end
46
46
 
47
47
  # Extracts the authority address from the params
@@ -50,21 +50,21 @@ module Solace
50
50
  #
51
51
  # @return [String] The authority address
52
52
  def authority
53
- params[:authority].is_a?(String) ? params[:authority] : params[:authority].address
53
+ params[:authority].to_s
54
54
  end
55
55
 
56
56
  # Extracts the mint address from the params
57
57
  #
58
58
  # @return [String] The mint address
59
59
  def mint
60
- params[:mint].is_a?(String) ? params[:mint] : params[:mint].address
60
+ params[:mint].to_s
61
61
  end
62
62
 
63
63
  # Returns the spl token program id
64
64
  #
65
65
  # @return [String] The spl token program id
66
66
  def spl_token_program
67
- Constants::TOKEN_PROGRAM_ID
67
+ Constants::TOKEN_PROGRAM_ID.to_s
68
68
  end
69
69
 
70
70
  # Returns the lamports to transfer
@@ -29,21 +29,21 @@ module Solace
29
29
  #
30
30
  # @return [String] The to address
31
31
  def to
32
- params[:to].is_a?(String) ? params[:to] : params[:to].address
32
+ params[:to].to_s
33
33
  end
34
34
 
35
35
  # Extracts the from address from the params
36
36
  #
37
37
  # @return [String] The from address
38
38
  def from
39
- params[:from].is_a?(String) ? params[:from] : params[:from].address
39
+ params[:from].to_s
40
40
  end
41
41
 
42
42
  # Returns the system program id
43
43
  #
44
44
  # @return [String] The system program id
45
45
  def system_program
46
- Solace::Constants::SYSTEM_PROGRAM_ID
46
+ Solace::Constants::SYSTEM_PROGRAM_ID.to_s
47
47
  end
48
48
 
49
49
  # Returns the lamports to transfer
@@ -4,6 +4,9 @@ require 'net/http'
4
4
  require 'json'
5
5
  require 'uri'
6
6
 
7
+ require 'solace/errors'
8
+ require 'solace/utils/rpc_client'
9
+
7
10
  module Solace
8
11
  # Connection to a Solana RPC node
9
12
  #
@@ -23,13 +26,17 @@ module Solace
23
26
  # # Wait for the transaction to be finalized
24
27
  # connection.wait_for_confirmed_signature('finalized') { result['result'] }
25
28
  #
29
+ # @raise [
30
+ # Solace::Errors::HTTPError,
31
+ # Solace::Errors::ParseError,
32
+ # Solace::Errors::RPCError,
33
+ # Solace::Errors::ConfirmationTimeout
34
+ # ]
26
35
  # @since 0.0.1
27
- #
28
- # rubocop:disable Metrics/ClassLength
29
36
  class Connection
30
37
  # @!attribute [r] rpc_url
31
38
  # The URL of the Solana RPC node
32
- attr_reader :rpc_url
39
+ attr_reader :rpc_client
33
40
 
34
41
  # @!attribute [r] default_options
35
42
  # The default options for RPC requests
@@ -40,11 +47,22 @@ module Solace
40
47
  # @param rpc_url [String] The URL of the Solana RPC node
41
48
  # @param commitment [String] The commitment level for RPC requests
42
49
  # @return [Solace::Connection] The connection object
43
- def initialize(rpc_url = 'http://localhost:8899', commitment: 'confirmed')
44
- @request_id = nil
45
- @rpc_url = rpc_url
50
+ # @param [Integer] http_open_timeout The timeout for opening an HTTP connection
51
+ # @param [Integer] http_read_timeout The timeout for reading an HTTP response
52
+ def initialize(
53
+ rpc_url = 'http://localhost:8899',
54
+ commitment: 'confirmed',
55
+ http_open_timeout: 30,
56
+ http_read_timeout: 60
57
+ )
58
+ # Initialize the RPC client
59
+ @rpc_client = Utils::RPCClient.new(
60
+ rpc_url,
61
+ open_timeout: http_open_timeout,
62
+ read_timeout: http_read_timeout
63
+ )
46
64
 
47
- # Set default options
65
+ # Set default options for rpc requests
48
66
  @default_options = {
49
67
  commitment: commitment,
50
68
  encoding: 'base64'
@@ -56,7 +74,12 @@ module Solace
56
74
  # @param method [String] the JSON-RPC method name
57
75
  # @param params [Array] the parameters for the RPC method
58
76
  # @return [Hash] the parsed JSON response
59
- # @raise [RuntimeError] if the response is not successful
77
+ # @raise [
78
+ # Solace::Errors::HTTPError,
79
+ # Solace::Errors::ParseError,
80
+ # Solace::Errors::RPCError,
81
+ # Solace::Errors::ConfirmationTimeout
82
+ # ]
60
83
  def rpc_request(method, params = [])
61
84
  request = build_rpc_request(method, params)
62
85
  response = perform_http_request(request)
@@ -70,7 +93,7 @@ module Solace
70
93
  # @param [Hash{Symbol => Object}] options The options for the request
71
94
  # @return [String] The transaction signature of the airdrop
72
95
  def request_airdrop(pubkey, lamports, options = {})
73
- rpc_request(
96
+ @rpc_client.rpc_request(
74
97
  'requestAirdrop',
75
98
  [
76
99
  pubkey,
@@ -84,7 +107,7 @@ module Solace
84
107
  #
85
108
  # @return [String] The latest blockhash
86
109
  def get_latest_blockhash
87
- rpc_request('getLatestBlockhash')['result']['value']['blockhash']
110
+ @rpc_client.rpc_request('getLatestBlockhash').dig('result', 'value', 'blockhash')
88
111
  end
89
112
 
90
113
  # Get the minimum required lamports for rent exemption
@@ -92,7 +115,7 @@ module Solace
92
115
  # @param space [Integer] Number of bytes to allocate for the account
93
116
  # @return [Integer] The minimum required lamports
94
117
  def get_minimum_lamports_for_rent_exemption(space)
95
- rpc_request('getMinimumBalanceForRentExemption', [space])['result']
118
+ @rpc_client.rpc_request('getMinimumBalanceForRentExemption', [space])['result']
96
119
  end
97
120
 
98
121
  # Get the account information from the Solana node
@@ -100,17 +123,7 @@ module Solace
100
123
  # @param pubkey [String] The public key of the account
101
124
  # @return [Object] The account information
102
125
  def get_account_info(pubkey)
103
- response = rpc_request(
104
- 'getAccountInfo',
105
- [
106
- pubkey,
107
- default_options
108
- ]
109
- )['result']
110
-
111
- return if response.nil?
112
-
113
- response['value']
126
+ @rpc_client.rpc_request('getAccountInfo', [pubkey, default_options]).dig('result', 'value')
114
127
  end
115
128
 
116
129
  # Get the balance of a specific account
@@ -118,13 +131,7 @@ module Solace
118
131
  # @param pubkey [String] The public key of the account
119
132
  # @return [Integer] The balance of the account
120
133
  def get_balance(pubkey)
121
- rpc_request(
122
- 'getBalance',
123
- [
124
- pubkey,
125
- default_options
126
- ]
127
- )['result']['value']
134
+ @rpc_client.rpc_request('getBalance', [pubkey, default_options]).dig('result', 'value')
128
135
  end
129
136
 
130
137
  # Get the balance of a token account
@@ -132,13 +139,7 @@ module Solace
132
139
  # @param token_account [String] The public key of the token account
133
140
  # @return [Hash] Token account balance information with amount and decimals
134
141
  def get_token_account_balance(token_account)
135
- rpc_request(
136
- 'getTokenAccountBalance',
137
- [
138
- token_account,
139
- default_options
140
- ]
141
- )['result']['value']
142
+ @rpc_client.rpc_request('getTokenAccountBalance', [token_account, default_options]).dig('result', 'value')
142
143
  end
143
144
 
144
145
  # Get the transaction by signature
@@ -147,27 +148,24 @@ module Solace
147
148
  # @return [Solace::Transaction] The transaction object
148
149
  # @param [Hash{Symbol => Object}] options
149
150
  def get_transaction(signature, options = { maxSupportedTransactionVersion: 0 })
150
- rpc_request(
151
- 'getTransaction',
152
- [
153
- signature,
154
- default_options.merge(options)
155
- ]
156
- )['result']
151
+ @rpc_client.rpc_request('getTransaction', [signature, default_options.merge(options)])['result']
157
152
  end
158
153
 
159
154
  # Get the signature status
160
155
  #
161
156
  # @param signatures [Array] The signatures of the transactions
162
157
  # @return [Object] The signature status
163
- def get_signature_status(signatures)
164
- rpc_request(
165
- 'getSignatureStatuses',
166
- [
167
- signatures,
168
- default_options.merge({ 'searchTransactionHistory' => true })
169
- ]
170
- )['result']
158
+ def get_signature_statuses(signatures)
159
+ @rpc_client.rpc_request('getSignatureStatuses',
160
+ [signatures, default_options.merge({ 'searchTransactionHistory' => true })])['result']
161
+ end
162
+
163
+ # Get the signature status
164
+ #
165
+ # @param signature [String] The signature of the transaction
166
+ # @return [Object] The signature status
167
+ def get_signature_status(signature)
168
+ get_signature_statuses([signature])
171
169
  end
172
170
 
173
171
  # Send a transaction to the Solana node
@@ -176,69 +174,70 @@ module Solace
176
174
  # @return [String] The signature of the transaction
177
175
  # @param [Hash{Symbol => Object}] options
178
176
  def send_transaction(transaction, options = {})
179
- rpc_request(
180
- 'sendTransaction',
181
- [
182
- transaction,
183
- default_options.merge(options)
184
- ]
185
- )
177
+ @rpc_client.rpc_request('sendTransaction', [transaction, default_options.merge(options)])
186
178
  end
187
179
 
188
- # Wait for a confirmed signature from the transaction
180
+ # Wait until the yielded signature reaches the desired commitment or timeout.
189
181
  #
190
- # @param commitment [String] The commitment level to wait for
191
- # @return [Boolean] True if the transaction was confirmed, false otherwise
192
- def wait_for_confirmed_signature(commitment = 'confirmed')
182
+ # @param commitment [String] One of "processed", "confirmed", "finalized"
183
+ # @param timeout [Numeric] seconds to wait before raising
184
+ # @param interval [Numeric] polling interval in seconds
185
+ # @yieldreturn [String, Hash] a signature string or a JSON-RPC hash with "result"
186
+ # @return [String] the signature when the commitment is reached
187
+ # @raise [ArgumentError, Errors::ConfirmationTimeout]
188
+ def wait_for_confirmed_signature(
189
+ commitment = 'confirmed',
190
+ timeout: 60,
191
+ interval: 0.1
192
+ )
193
193
  raise ArgumentError, 'Block required' unless block_given?
194
194
 
195
- # Get the signature from the block
196
- signature = yield
197
-
198
- interval = 0.1
195
+ signature = extract_signature_from(yield)
196
+ deadline = monotonic_deadline(timeout)
199
197
 
200
198
  # Wait for confirmation
201
- loop do
202
- status = get_signature_status([signature]).dig('value', 0)
203
-
204
- break if status && status['confirmationStatus'] == commitment
199
+ until dealine_passed?(deadline)
200
+ return signature if commitment_reached?(signature, commitment)
205
201
 
206
202
  sleep interval
207
203
  end
208
204
 
209
- signature
205
+ raise Errors::ConfirmationTimeout.format(signature, commitment, timeout)
210
206
  end
211
207
 
212
208
  private
213
209
 
214
- def build_rpc_request(method, params)
215
- uri = URI(rpc_url)
216
- req = Net::HTTP::Post.new(uri)
217
- req['Accept'] = 'application/json'
218
- req['Content-Type'] = 'application/json'
219
- @request_id = SecureRandom.uuid
220
-
221
- req.body = {
222
- jsonrpc: '2.0',
223
- id: @request_id,
224
- method: method,
225
- params: params
226
- }.to_json
227
-
228
- [uri, req]
210
+ # Confirms the commitment is reached
211
+ #
212
+ # @param signature [String] The signature of the transaction
213
+ # @param commitment [String] The commitment level not reached
214
+ # @return [Boolean] Whether the commitment is reached
215
+ def commitment_reached?(signature, commitment)
216
+ get_signature_status(signature).dig('value', 0, 'confirmationStatus') == commitment
229
217
  end
230
218
 
231
- def perform_http_request((uri, req))
232
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
233
- http.request(req)
234
- end
219
+ # Extracts signature from given value
220
+ #
221
+ # @param value [String, Object] The result of the yielded block
222
+ # @return [String] The signature
223
+ def extract_signature_from(value)
224
+ value.is_a?(String) ? value : value['result']
235
225
  end
236
226
 
237
- def handle_rpc_response(response)
238
- raise "RPC error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
227
+ # Checks if a timeout deadline has been reached
228
+ #
229
+ # @params deadline [Integer] The deadline for the timeout
230
+ # @return [boolean] whether the dealine has passed
231
+ def dealine_passed?(deadline)
232
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
233
+ end
239
234
 
240
- JSON.parse(response.body)
235
+ # Sets a deadline given a timeout in seconds
236
+ #
237
+ # @params seconds [Integer] The seconds for the deadline
238
+ # @return [Integer] The deadline in seconds
239
+ def monotonic_deadline(seconds)
240
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
241
241
  end
242
242
  end
243
- # rubocop:enable Metrics/ClassLength
244
243
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Constants module
4
- #
5
- # Contains constants used across the library.
6
- #
7
- # @return [Module] Constants module
8
3
  module Solace
4
+ # Constants module
5
+ #
6
+ # Contains constants used across the library.
7
+ #
8
+ # @return [Module] Constants module
9
9
  module Constants
10
10
  # @!attribute SYSTEM_PROGRAM_ID
11
11
  # The public key of the System Program (native SOL transfers, account creation, etc)
@@ -41,5 +41,63 @@ module Solace
41
41
  # The public key of the Address Lookup Table Program
42
42
  # This is the same across all Solana clusters
43
43
  ADDRESS_LOOKUP_TABLE_PROGRAM_ID = 'AddressLookupTab1e1111111111111111111111111'
44
+
45
+ # Loads the constants declared in a YAML file
46
+ #
47
+ # Developers require adding program addresses and mint accounts that will not
48
+ # be added directly to Solace. This method allows for loading those constants
49
+ # from a YAML file and extends the Constants module with them.
50
+ #
51
+ # The YAML file should be a hash of key-value pairs, where the key is the constant
52
+ # name and the value is the constant value.
53
+ #
54
+ # @example
55
+ # # constants.yml
56
+ # devnet:
57
+ # my_program_id: some_devnet_program_id
58
+ # squads_program_id: some_devnet_program_id
59
+ # usdc_mint_account: some_devnet_program_id
60
+ # usdt_mint_account: some_devnet_program_id
61
+ #
62
+ # mainnet:
63
+ # my_program_id: some_mainnet_program_id
64
+ # squads_program_id: some_mainnet_program_id
65
+ # usdc_mint_account: some_mainnet_program_id
66
+ # usdt_mint_account: some_mainnet_program_id
67
+ #
68
+ # @example
69
+ # Solace::Constants.load(
70
+ # path: '/home/user/my-project/config/constants.yml',
71
+ # namespace: 'devnet',
72
+ # protect_overrides: false
73
+ # )
74
+ #
75
+ # Solace::Constants::MY_PROGRAM_ID
76
+ # Solace::Constants::SQUADS_PROGRAM_ID
77
+ # Solace::Constants::USDC_MINT_ACCOUNT
78
+ # Solace::Constants::USDT_MINT_ACCOUNT
79
+ #
80
+ # @param path [String] The path to the YAML file
81
+ # @param namespace [String] The namespace to load the constants from
82
+ # @param protect_overrides [Boolean] Whether to protect constants that are already defined
83
+ # @return [void]
84
+ # @raise [ArgumentError] If protect_overrides is on and a constant is already defined
85
+ def self.load(
86
+ path:,
87
+ namespace: 'default',
88
+ protect_overrides: true
89
+ )
90
+ content = YAML.load_file(path)
91
+
92
+ content[namespace].each do |key, value|
93
+ if const_defined?(key.upcase)
94
+ raise ArgumentError, "Constant #{key} is already defined" if protect_overrides
95
+
96
+ remove_const(key.upcase)
97
+ end
98
+
99
+ const_set(key.upcase, value)
100
+ end
101
+ end
44
102
  end
45
103
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Errors
5
+ # Waiting for confirmation exceeded timeout
6
+ class ConfirmationTimeout < StandardError
7
+ attr_reader :signature, :commitment, :timeout
8
+
9
+ # @param [String] message The error message
10
+ # @param [String] signature The signature of the transaction
11
+ # @param [String] commitment The commitment level not reached
12
+ # @param [Integer] timeout The time out reached
13
+ def initialize(message, signature:, commitment:, timeout:)
14
+ super(message)
15
+ @signature = signature
16
+ @commitment = commitment
17
+ @timeout = timeout
18
+ end
19
+
20
+ # Formats a confirmation timeout error
21
+ #
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
25
+ # @return [Solace::Errors::ConfirmationTimeout] The formatted error
26
+ def self.format(signature, commitment, timeout)
27
+ new(
28
+ "Timed out waiting for signature #{signature} at commitment=#{commitment} after #{timeout}s",
29
+ signature: signature,
30
+ commitment: commitment,
31
+ timeout: timeout
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Errors
5
+ # Non-2xx HTTP or low-level network issues
6
+ class HTTPError < StandardError
7
+ attr_reader :code, :body
8
+
9
+ # @param [String] message The error message
10
+ # @param [Integer] code The HTTP status code
11
+ # @param [String] body The HTTP response body
12
+ def initialize(message, code:, body: nil)
13
+ super(message)
14
+ @code = code
15
+ @body = body
16
+ end
17
+
18
+ # Formats a response to an error
19
+ #
20
+ # @param response [Net::HTTPResponse] The HTTP response
21
+ # @return [Solace::Errors::HTTPError] The formatted error
22
+ def self.format_response(response)
23
+ new("HTTP error: #{response.message}", code: response.code.to_i, body: response.body)
24
+ end
25
+
26
+ # Formats transport errors
27
+ #
28
+ # @param error [SocketError, IOError] The transport error
29
+ # @return [Solace::Errors::HTTPError] The formatted error
30
+ def self.format_transport_error(error)
31
+ new("HTTP transport error: #{error.message}", code: 0)
32
+ end
33
+
34
+ # Formats timeout errors
35
+ #
36
+ # @param error [Net::OpenTimeout, Net::ReadTimeout] The timeout error
37
+ # @return [Solace::Errors::HTTPError] The formatted error
38
+ def self.format_timeout_error(error)
39
+ new("HTTP timeout: #{error.class}", code: 408)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Errors
5
+ # JSON parsing failed
6
+ class ParseError < StandardError
7
+ attr_reader :body
8
+
9
+ # @param [String] message The error message
10
+ # @param [Object] body The response body
11
+ def initialize(message, body:)
12
+ super(message)
13
+ @body = body
14
+ end
15
+
16
+ # Formats a response to an error
17
+ #
18
+ # @param error [JSON::ParserError] The JSON-RPC error
19
+ # @param [Object] response The response from the RPC
20
+ # @return [Solace::Errors::ParseError] The formatted error
21
+ def self.format_response(error, response)
22
+ new("Invalid JSON from RPC: #{error.message}", body: response.body)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Errors
5
+ # JSON-RPC returned an "error" object
6
+ class RPCError < StandardError
7
+ attr_reader :rpc_code, :rpc_message, :rpc_data
8
+
9
+ # @param [String] message The error message
10
+ # @param [Integer] rpc_code The JSON-RPC error code
11
+ # @param [String] rpc_message The JSON-RPC error message
12
+ # @param [Object] rpc_data The JSON-RPC error data
13
+ def initialize(message, rpc_code:, rpc_message:, rpc_data: nil)
14
+ super(message)
15
+ @rpc_code = rpc_code
16
+ @rpc_message = rpc_message
17
+ @rpc_data = rpc_data
18
+ end
19
+
20
+ # Formats a response to an error
21
+ #
22
+ # @param response [Hash] The JSON-RPC response
23
+ # @return [Solace::Errors::RPCError] The formatted error
24
+ def self.format_response(response)
25
+ new(
26
+ "RPC error #{response['error']['code']}: #{response['error']['message']}",
27
+ rpc_data: response['error']['data'],
28
+ rpc_code: response['error']['code'],
29
+ rpc_message: response['error']['message']
30
+ )
31
+ end
32
+
33
+ # @return [Hash] The error as a hash
34
+ def to_h = { code: rpc_code, message: rpc_message, data: rpc_data }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ # Error handling module
5
+ #
6
+ # This module provides error classes for handling different types of errors that may occur during
7
+ # Solana RPC requests and processing transactions.
8
+ #
9
+ # @since 0.0.8
10
+ module Errors
11
+ # JSON-RPC Errors
12
+ require 'solace/errors/rpc_error'
13
+ require 'solace/errors/http_error'
14
+ require 'solace/errors/parse_error'
15
+ require 'solace/errors/confirmation_timeout'
16
+ end
17
+ end
@@ -132,13 +132,31 @@ module Solace
132
132
  # Returns the public key address as a Base58 string
133
133
  #
134
134
  # @example
135
- # pubkey_str = instance.address
135
+ # pubkey_str = instance.to_base58
136
136
  #
137
137
  # @return [String] Base58 encoded public key
138
- def address
138
+ def to_base58
139
139
  public_key.to_base58
140
140
  end
141
141
 
142
+ # Returns the public key address as a Base58 string
143
+ #
144
+ # @example
145
+ # pubkey_str = instance.to_s
146
+ #
147
+ # @return [String] Base58 encoded public key
148
+ # @since 0.0.8
149
+ alias to_s to_base58
150
+
151
+ # Returns the public key address as a Base58 string
152
+ #
153
+ # @example
154
+ # pubkey_str = instance.to_base58
155
+ #
156
+ # @return [String] Base58 encoded public key
157
+ # @since 0.0.8
158
+ alias address to_base58
159
+
142
160
  # Signs a message (string or binary)
143
161
  #
144
162
  # @example
@@ -33,9 +33,9 @@ module Solace
33
33
  def get_address(owner:, mint:)
34
34
  Solace::Utils::PDA.find_program_address(
35
35
  [
36
- owner.address,
36
+ owner.to_s,
37
37
  Solace::Constants::TOKEN_PROGRAM_ID,
38
- mint.address
38
+ mint.to_s
39
39
  ],
40
40
  Solace::Constants::ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
41
41
  )
@@ -51,7 +51,7 @@ module Solace
51
51
 
52
52
  # Alias method for get_address
53
53
  #
54
- # @option options [Hash] A hash of options for the get_address class method
54
+ # @param options [Hash] A hash of options for the get_address class method
55
55
  # @return [Array<String, Integer>] The address of the associated token account and the bump seed
56
56
  def get_address(**options)
57
57
  self.class.get_address(**options)
@@ -67,15 +67,15 @@ module Solace
67
67
  def get_or_create_address(payer:, owner:, mint:, commitment: 'confirmed')
68
68
  ata_address, _bump = get_address(owner: owner, mint: mint)
69
69
 
70
- account_info = @connection.get_account_info(ata_address)
70
+ account_balance = @connection.get_account_info(ata_address)
71
71
 
72
- return ata_address if account_info
72
+ return ata_address unless account_balance.nil?
73
73
 
74
74
  response = create_associated_token_account(payer: payer, owner: owner, mint: mint)
75
75
 
76
76
  raise 'Failed to create associated token account' unless response['result']
77
77
 
78
- @connection.wait_for_confirmed_signature(commitment) { response['result'] }
78
+ @connection.wait_for_confirmed_signature(commitment) { response }
79
79
 
80
80
  ata_address
81
81
  end
@@ -87,6 +87,8 @@ module Solace
87
87
  def create_associated_token_account(**options)
88
88
  tx = prepare_create_associated_token_account(**options)
89
89
 
90
+ tx.sign(options[:payer])
91
+
90
92
  @connection.send_transaction(tx.serialize)
91
93
  end
92
94
 
@@ -97,7 +99,6 @@ module Solace
97
99
  # @param payer [Solace::Keypair] The keypair that will pay for fees and rent.
98
100
  # @return [Solace::Transaction] The signed transaction.
99
101
  #
100
- # rubocop:disable Metrics/MethodLength
101
102
  def prepare_create_associated_token_account(
102
103
  payer:,
103
104
  owner:,
@@ -105,39 +106,21 @@ module Solace
105
106
  )
106
107
  ata_address, = get_address(owner: owner, mint: mint)
107
108
 
108
- accounts = [
109
- payer.address,
110
- ata_address,
111
- owner.address,
112
- mint.address,
113
- Solace::Constants::SYSTEM_PROGRAM_ID,
114
- Solace::Constants::TOKEN_PROGRAM_ID,
115
- Solace::Constants::ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID
116
- ]
117
-
118
- instruction = Solace::Instructions::AssociatedTokenAccount::CreateAssociatedTokenAccountInstruction.build(
119
- funder_index: 0,
120
- associated_token_account_index: 1,
121
- owner_index: 2,
122
- mint_index: 3,
123
- system_program_index: 4,
124
- token_program_index: 5,
125
- program_index: 6
126
- )
127
-
128
- message = Solace::Message.new(
129
- header: [1, 0, 4],
130
- accounts: accounts,
131
- recent_blockhash: @connection.get_latest_blockhash,
132
- instructions: [instruction]
133
- )
134
-
135
- tx = Solace::Transaction.new(message: message)
136
- tx.sign(payer)
137
-
138
- tx
109
+ TransactionComposer.new(connection: connection).try do |tx_composer|
110
+ tx_composer.set_fee_payer(payer)
111
+
112
+ tx_composer.add_instruction(
113
+ Solace::Composers::AssociatedTokenAccountProgramCreateAccountComposer.new(
114
+ mint: mint,
115
+ owner: owner,
116
+ funder: payer,
117
+ ata_address: ata_address
118
+ )
119
+ )
120
+
121
+ tx_composer.compose_transaction
122
+ end
139
123
  end
140
- # rubocop:enable Metrics/MethodLength
141
124
  end
142
125
  end
143
126
  end
@@ -63,8 +63,8 @@ module Solace
63
63
  mint_keypair: Solace::Keypair.generate
64
64
  )
65
65
  accounts = [
66
- payer.address,
67
- mint_keypair.address,
66
+ payer.to_s,
67
+ mint_keypair.to_s,
68
68
  Solace::Constants::SYSVAR_RENT_PROGRAM_ID,
69
69
  Solace::Constants::TOKEN_PROGRAM_ID,
70
70
  Solace::Constants::SYSTEM_PROGRAM_ID
@@ -81,15 +81,15 @@ module Solace
81
81
  owner: program_id
82
82
  )
83
83
 
84
- freeze_authority_address = freeze_authority.respond_to?(:address) ? freeze_authority.address : nil
84
+ freeze_authority = freeze_authority.to_s unless freeze_authority.nil?
85
85
 
86
86
  initialize_mint_ix = Solace::Instructions::SplToken::InitializeMintInstruction.build(
87
87
  mint_account_index: 1,
88
88
  rent_sysvar_index: 2,
89
89
  program_index: 3,
90
90
  decimals: decimals,
91
- mint_authority: mint_authority.address,
92
- freeze_authority: freeze_authority_address
91
+ mint_authority: mint_authority.to_s,
92
+ freeze_authority: freeze_authority
93
93
  )
94
94
 
95
95
  message = Message.new(
@@ -134,11 +134,11 @@ module Solace
134
134
  mint_authority:
135
135
  )
136
136
  accounts = [
137
- payer.address,
138
- mint_authority.address,
139
- mint.address,
140
- destination,
141
- Solace::Constants::TOKEN_PROGRAM_ID
137
+ payer.to_s,
138
+ mint_authority.to_s,
139
+ mint.to_s,
140
+ destination.to_s,
141
+ Solace::Constants::TOKEN_PROGRAM_ID.to_s
142
142
  ]
143
143
 
144
144
  ix = Solace::Instructions::SplToken::MintToInstruction.build(
@@ -191,11 +191,11 @@ module Solace
191
191
  owner:
192
192
  )
193
193
  accounts = [
194
- payer.address,
195
- owner.address,
196
- source,
197
- destination,
198
- Solace::Constants::TOKEN_PROGRAM_ID
194
+ payer.to_s,
195
+ owner.to_s,
196
+ source.to_s,
197
+ destination.to_s,
198
+ Solace::Constants::TOKEN_PROGRAM_ID.to_s
199
199
  ]
200
200
 
201
201
  ix = Solace::Instructions::SplToken::TransferInstruction.build(
@@ -51,6 +51,7 @@ module Solace
51
51
  # pubkey_str = instance.to_base58
52
52
  #
53
53
  # @return [String]
54
+ # @since 0.0.1
54
55
  def to_base58
55
56
  Solace::Utils::Codecs.bytes_to_base58(@bytes)
56
57
  end
@@ -61,9 +62,8 @@ module Solace
61
62
  # pubkey_str = instance.to_s
62
63
  #
63
64
  # @return [String]
64
- def to_s
65
- to_base58
66
- end
65
+ # @since 0.0.8
66
+ alias to_s to_base58
67
67
 
68
68
  # Return the address of the public key
69
69
  #
@@ -71,9 +71,8 @@ module Solace
71
71
  # pubkey_str = instance.address
72
72
  #
73
73
  # @return [String]
74
- def address
75
- to_base58
76
- end
74
+ # @since 0.0.8
75
+ alias address to_base58
77
76
 
78
77
  # Compare two public keys for equality
79
78
  #
@@ -82,6 +81,7 @@ module Solace
82
81
  #
83
82
  # @param other [PublicKey]
84
83
  # @return [Boolean]
84
+ # @since 0.0.1
85
85
  def ==(other)
86
86
  other.is_a?(Solace::PublicKey) && other.bytes == bytes
87
87
  end
@@ -92,6 +92,7 @@ module Solace
92
92
  # pubkey_bytes = instance.to_bytes
93
93
  #
94
94
  # @return [Array<Integer>]
95
+ # @since 0.0.1
95
96
  def to_bytes
96
97
  @bytes.dup
97
98
  end
@@ -103,7 +104,10 @@ module Solace
103
104
  # pubkey = Solace::PublicKey.from_address(address)
104
105
  #
105
106
  # @param address [String] The base58 address of the public key
106
- # @return [PublicKey]
107
+ # @return [PublicKey] The public key instance
108
+ # @raise [ArgumentError] If the address is not a valid base58 string
109
+ #
110
+ # @since 0.0.6
107
111
  def from_address(address)
108
112
  new(Solace::Utils::Codecs.base58_to_bytes(address))
109
113
  end
@@ -38,7 +38,7 @@ module Solace
38
38
  # # Sign the transaction with all required signers
39
39
  # tx.sign(*required_signers)
40
40
  #
41
- # @since 0.0.1
41
+ # @since 0.0.6
42
42
  class TransactionComposer
43
43
  # @!attribute connection
44
44
  # The connection to the Solana cluster
@@ -76,7 +76,7 @@ module Solace
76
76
  # @param pubkey [String, Solace::PublicKey, Solace::Keypair] The fee payer pubkey
77
77
  # @return [TransactionComposer] Self for chaining
78
78
  def set_fee_payer(pubkey)
79
- context.set_fee_payer(pubkey)
79
+ context.set_fee_payer(pubkey.to_s)
80
80
  self
81
81
  end
82
82
 
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ require 'solace/errors'
8
+
9
+ module Solace
10
+ module Utils
11
+ # RPCClient provides Net::HTTP based HTTP client for sending HTTP
12
+ # requests to a Solana RPC node and parsing responses.
13
+ #
14
+ # @since 0.0.8
15
+ class RPCClient
16
+ # @!attribute [r] url
17
+ # The URL for the HTTP request
18
+ attr_reader :url
19
+
20
+ # @!attribute [r] open_timeout
21
+ # The timeout for opening an HTTP connection
22
+ attr_reader :open_timeout
23
+
24
+ # @!attribute [r] read_timeout
25
+ # The timeout for reading an HTTP response
26
+ attr_reader :read_timeout
27
+
28
+ # Initialize the connection with a default or custom RPC URL
29
+ #
30
+ # @param url [String] The URL of the Solana RPC node
31
+ # @param open_timeout [Integer] The timeout for opening an HTTP connection
32
+ # @param read_timeout [Integer] The timeout for reading an HTTP response
33
+ def initialize(
34
+ url,
35
+ open_timeout:,
36
+ read_timeout:
37
+ )
38
+ @url = url
39
+ @open_timeout = open_timeout
40
+ @read_timeout = read_timeout
41
+ end
42
+
43
+ # Sends a JSON-RPC request to the configured Solana RPC server.
44
+ #
45
+ # @param method [String] the JSON-RPC method name
46
+ # @param params [Array] the parameters for the RPC method
47
+ # @return [Hash] the parsed JSON response
48
+ # @raise [
49
+ # Solace::Errors::HTTPError,
50
+ # Solace::Errors::ParseError,
51
+ # Solace::Errors::RPCError,
52
+ # Solace::Errors::ConfirmationTimeout
53
+ # ]
54
+ def rpc_request(method, params = [])
55
+ request = build_rpc_request(method, params)
56
+ response = perform_http_request(*request)
57
+ handle_rpc_response(response)
58
+ end
59
+
60
+ private
61
+
62
+ # Builds a JSON-RPC request
63
+ #
64
+ # @param method [String] the JSON-RPC method name
65
+ # @param params [Array] the parameters for the RPC method
66
+ # @return [Array] the URI and request object
67
+ def build_rpc_request(method, params)
68
+ uri = URI(url)
69
+
70
+ req = Net::HTTP::Post.new(uri)
71
+ req['Accept'] = 'application/json'
72
+ req['Content-Type'] = 'application/json'
73
+ req.body = build_request_body(method, params)
74
+
75
+ [uri, req]
76
+ end
77
+
78
+ # Builds request body
79
+ #
80
+ # @param method [String] the JSON-RPC method name
81
+ # @param params [Array] the parameters for the RPC method
82
+ # @return [String] the request body
83
+ def build_request_body(method, params)
84
+ {
85
+ jsonrpc: '2.0',
86
+ id: SecureRandom.uuid,
87
+ method: method,
88
+ params: params
89
+ }.to_json
90
+ end
91
+
92
+ # Performs an HTTP request to the configured Solana RPC server.
93
+ #
94
+ # @param uri [URI] the URI for the HTTP request
95
+ # @param req [Net::HTTP::Post] the request object
96
+ # @return [Net::HTTPResponse] the HTTP response
97
+ # @raise [Solace::Errors::HTTPError]
98
+ def perform_http_request(uri, req)
99
+ Net::HTTP.start(
100
+ uri.hostname,
101
+ uri.port,
102
+ use_ssl: uri.scheme == 'https',
103
+ open_timeout: open_timeout,
104
+ read_timeout: read_timeout
105
+ ) do |http|
106
+ http.request(req)
107
+ end
108
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
109
+ raise Errors::HTTPError.format_timeout_error(e)
110
+ rescue SocketError, IOError => e
111
+ raise Errors::HTTPError.format_transport_error(e)
112
+ end
113
+
114
+ # Handles the response from the HTTP request
115
+ #
116
+ # @param response [Net::HTTPResponse] The HTTP response
117
+ # @return [Hash] The parsed JSON response
118
+ # @raise [Solace::Errors::HTTPError]
119
+ # @raise [Solace::Errors::ParseError]
120
+ # @raise [Solace::Errors::RPCError]
121
+ def handle_rpc_response(response)
122
+ raise Errors::HTTPError.format_response(response) unless response.is_a?(Net::HTTPSuccess)
123
+
124
+ json = JSON.parse(response.body)
125
+
126
+ raise Errors::RPCError.format_response(json) if json['error']
127
+
128
+ json
129
+ rescue JSON::ParserError => e
130
+ raise Errors::ParseError.format_response(e, response)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Solace::VERSION is the version of the Solace gem
4
4
  module Solace
5
- VERSION = '0.0.6'
5
+ VERSION = '0.0.8'
6
6
  end
data/lib/solace.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  require_relative 'solace/version'
5
5
 
6
6
  # 🛠️ Helpers
7
+ require_relative 'solace/errors'
7
8
  require_relative 'solace/constants'
8
9
  require_relative 'solace/connection'
9
10
  require_relative 'solace/utils/codecs'
@@ -25,9 +26,13 @@ require_relative 'solace/instruction'
25
26
  require_relative 'solace/address_lookup_table'
26
27
  require_relative 'solace/transaction_composer'
27
28
 
28
- # 📦 Composers (Builders)
29
+ # Base Classes (Abstract classes)
30
+ require_relative 'solace/programs/base'
31
+ require_relative 'solace/composers/base'
32
+
33
+ # 📦 Composers
29
34
  #
30
- # Glob require all instructions
35
+ # Glob require all composers
31
36
  Dir[File.join(__dir__, 'solace/composers', '**', '*.rb')].each { |file| require file }
32
37
 
33
38
  # 📦 Instructions (Builders)
@@ -36,6 +41,5 @@ Dir[File.join(__dir__, 'solace/composers', '**', '*.rb')].each { |file| require
36
41
  Dir[File.join(__dir__, 'solace/instructions', '**', '*.rb')].each { |file| require file }
37
42
 
38
43
  # 📦 Programs
39
- require_relative 'solace/programs/base'
40
44
  require_relative 'solace/programs/spl_token'
41
45
  require_relative 'solace/programs/associated_token_account'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl
@@ -93,12 +93,18 @@ files:
93
93
  - README.md
94
94
  - lib/solace.rb
95
95
  - lib/solace/address_lookup_table.rb
96
+ - lib/solace/composers/associated_token_account_program_create_account_composer.rb
96
97
  - lib/solace/composers/base.rb
97
98
  - lib/solace/composers/spl_token_program_transfer_checked_composer.rb
98
99
  - lib/solace/composers/system_program_transfer_composer.rb
99
100
  - lib/solace/concerns/binary_serializable.rb
100
101
  - lib/solace/connection.rb
101
102
  - lib/solace/constants.rb
103
+ - lib/solace/errors.rb
104
+ - lib/solace/errors/confirmation_timeout.rb
105
+ - lib/solace/errors/http_error.rb
106
+ - lib/solace/errors/parse_error.rb
107
+ - lib/solace/errors/rpc_error.rb
102
108
  - lib/solace/instruction.rb
103
109
  - lib/solace/instructions/associated_token_account/create_associated_token_account_instruction.rb
104
110
  - lib/solace/instructions/spl_token/initialize_account_instruction.rb
@@ -133,6 +139,7 @@ files:
133
139
  - lib/solace/utils/libcurve25519_dalek-macos/libcurve25519_dalek.dylib
134
140
  - lib/solace/utils/libcurve25519_dalek-windows/curve25519_dalek.dll
135
141
  - lib/solace/utils/pda.rb
142
+ - lib/solace/utils/rpc_client.rb
136
143
  - lib/solace/version.rb
137
144
  homepage: https://github.com/sebscholl/solace
138
145
  licenses: