solace 0.1.4 → 0.1.6

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/solace/composers/associated_token_account_program_create_account_composer.rb +11 -10
  3. data/lib/solace/composers/associated_token_account_program_create_idempotent_account_composer.rb +2 -1
  4. data/lib/solace/composers/base.rb +1 -1
  5. data/lib/solace/composers/spl_token_program_close_account_composer.rb +3 -3
  6. data/lib/solace/composers/spl_token_program_initialize_mint_composer.rb +5 -5
  7. data/lib/solace/composers/spl_token_program_mint_to_composer.rb +4 -4
  8. data/lib/solace/composers/spl_token_program_transfer_checked_composer.rb +6 -6
  9. data/lib/solace/composers/spl_token_program_transfer_composer.rb +4 -4
  10. data/lib/solace/composers/system_program_create_account_composer.rb +5 -5
  11. data/lib/solace/composers/system_program_transfer_composer.rb +3 -3
  12. data/lib/solace/composers/token_2022_program_close_account_composer.rb +75 -0
  13. data/lib/solace/composers/token_2022_program_initialize_mint_composer.rb +93 -0
  14. data/lib/solace/composers/token_2022_program_mint_to_composer.rb +84 -0
  15. data/lib/solace/composers/token_2022_program_transfer_checked_composer.rb +109 -0
  16. data/lib/solace/composers/token_2022_program_transfer_composer.rb +86 -0
  17. data/lib/solace/connection.rb +29 -9
  18. data/lib/solace/constants.rb +8 -2
  19. data/lib/solace/errors/confirmation_timeout.rb +4 -4
  20. data/lib/solace/errors/rpc_error.rb +4 -4
  21. data/lib/solace/instructions/associated_token_account/create_account_instruction.rb +2 -1
  22. data/lib/solace/instructions/spl_token/close_account_instruction.rb +2 -2
  23. data/lib/solace/instructions/spl_token/initialize_account_instruction.rb +2 -2
  24. data/lib/solace/instructions/spl_token/initialize_mint_instruction.rb +2 -2
  25. data/lib/solace/instructions/spl_token/mint_to_instruction.rb +2 -2
  26. data/lib/solace/instructions/spl_token/transfer_checked_instruction.rb +3 -3
  27. data/lib/solace/instructions/spl_token/transfer_instruction.rb +2 -2
  28. data/lib/solace/instructions/system_program/create_account_instruction.rb +2 -2
  29. data/lib/solace/instructions/system_program/transfer_instruction.rb +2 -2
  30. data/lib/solace/instructions/token_2022/close_account_instruction.rb +50 -0
  31. data/lib/solace/instructions/token_2022/initialize_account_instruction.rb +62 -0
  32. data/lib/solace/instructions/token_2022/initialize_mint_instruction.rb +75 -0
  33. data/lib/solace/instructions/token_2022/mint_to_instruction.rb +59 -0
  34. data/lib/solace/instructions/token_2022/transfer_checked_instruction.rb +68 -0
  35. data/lib/solace/instructions/token_2022/transfer_instruction.rb +71 -0
  36. data/lib/solace/message.rb +5 -5
  37. data/lib/solace/programs/associated_token_account.rb +35 -17
  38. data/lib/solace/programs/spl_token.rb +18 -254
  39. data/lib/solace/programs/token_2022.rb +70 -0
  40. data/lib/solace/programs/token_program_interface.rb +312 -0
  41. data/lib/solace/serializers/address_lookup_table_deserializer.rb +2 -2
  42. data/lib/solace/serializers/base_deserializer.rb +1 -1
  43. data/lib/solace/serializers/instruction_deserializer.rb +2 -2
  44. data/lib/solace/serializers/message_deserializer.rb +3 -3
  45. data/lib/solace/serializers/transaction_deserializer.rb +1 -1
  46. data/lib/solace/tokens/token.rb +1 -1
  47. data/lib/solace/tokens.rb +2 -2
  48. data/lib/solace/transaction.rb +1 -1
  49. data/lib/solace/transaction_composer.rb +5 -5
  50. data/lib/solace/utils/account_context.rb +9 -9
  51. data/lib/solace/utils/codecs.rb +309 -49
  52. data/lib/solace/utils/rpc_client.rb +9 -9
  53. data/lib/solace/version.rb +1 -1
  54. data/lib/solace.rb +2 -0
  55. metadata +124 -9
  56. data/CHANGELOG +0 -226
  57. data/LICENSE +0 -21
  58. data/README.md +0 -518
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Programs
5
+ # Mixin describing the common surface of a token-program client.
6
+ #
7
+ # The legacy SPL Token program and Token-2022 expose a wire-compatible
8
+ # base instruction set (Transfer, TransferChecked, CloseAccount, MintTo,
9
+ # InitializeMint). This module captures the *shape* of a client that
10
+ # speaks that surface — the public methods +create_mint+, +mint_to+,
11
+ # +transfer+, +transfer_checked+ and their +compose_*+ pairs — without
12
+ # binding to either program.
13
+ #
14
+ # State (connection, program_id) lives on {Programs::Base}; this module
15
+ # is purely behavior. Including classes must implement four private
16
+ # readers that name the composer class for each operation:
17
+ #
18
+ # - {#initialize_mint_composer_class}
19
+ # - {#mint_to_composer_class}
20
+ # - {#transfer_composer_class}
21
+ # - {#transfer_checked_composer_class}
22
+ #
23
+ # Each composer returned by those readers is itself bound to a single
24
+ # on-chain program, which is how this mixin keeps the boundary clean:
25
+ # +SplToken+ and +Token2022+ share *methods* but never share a composer.
26
+ #
27
+ # @see Solace::Programs::SplToken
28
+ # @see Solace::Programs::Token2022
29
+ # @since 0.1.5
30
+ # rubocop:disable Metrics/ModuleLength
31
+ module TokenProgramInterface
32
+ # Creates a new mint, signs it, and (optionally) sends it.
33
+ #
34
+ # @param payer [#to_s, PublicKey] The keypair that will pay for fees and rent.
35
+ # @param sign [Boolean] Whether to sign the transaction.
36
+ # @param execute [Boolean] Whether to execute the transaction.
37
+ # @param composer_opts [Hash] Options for {#compose_create_mint}.
38
+ # @return [Transaction] The created or sent transaction.
39
+ def create_mint(
40
+ payer:,
41
+ sign: true,
42
+ execute: true,
43
+ **composer_opts
44
+ )
45
+ composer = compose_create_mint(**composer_opts)
46
+
47
+ yield composer if block_given?
48
+
49
+ tx = composer
50
+ .set_fee_payer(payer)
51
+ .compose_transaction
52
+
53
+ if sign
54
+ tx.sign(
55
+ payer,
56
+ composer_opts[:funder],
57
+ composer_opts[:mint_account]
58
+ )
59
+
60
+ connection.send_transaction(tx.serialize) if execute
61
+ end
62
+
63
+ tx
64
+ end
65
+
66
+ # Prepares a new mint transaction.
67
+ #
68
+ # @param funder [#to_s, PublicKey] The keypair that will pay for rent of the new mint account.
69
+ # @param decimals [Integer] The number of decimal places for the token.
70
+ # @param mint_authority [#to_s, PublicKey] The base58 public key for the mint authority.
71
+ # @param freeze_authority [#to_s, PublicKey] (Optional) The base58 public key for the freeze authority.
72
+ # @param mint_account [#to_s, PublicKey] (Optional) The keypair for the new mint.
73
+ # @return [TransactionComposer] A composer with required instructions.
74
+ # rubocop:disable Metrics/MethodLength
75
+ def compose_create_mint(
76
+ funder:,
77
+ decimals:,
78
+ mint_authority:,
79
+ freeze_authority: nil,
80
+ mint_account: Solace::Keypair.generate
81
+ )
82
+ # Mint accounts need 82 bytes of space, and we need to fund it with enough lamports to be rent-exempt
83
+ rent_lamports = connection.get_minimum_lamports_for_rent_exemption(82)
84
+
85
+ # Build the account for the mint
86
+ create_account_ix = Composers::SystemProgramCreateAccountComposer.new(
87
+ from: funder,
88
+ new_account: mint_account,
89
+ owner: program_id,
90
+ lamports: rent_lamports,
91
+ space: 82
92
+ )
93
+
94
+ # Build the initialize mint composer (per-program class supplied by includer)
95
+ initialize_mint_ix = initialize_mint_composer_class.new(
96
+ decimals: decimals,
97
+ mint_account: mint_account,
98
+ mint_authority: mint_authority,
99
+ freeze_authority: freeze_authority
100
+ )
101
+
102
+ TransactionComposer
103
+ .new(connection: connection)
104
+ .add_instruction(create_account_ix)
105
+ .add_instruction(initialize_mint_ix)
106
+ end
107
+ # rubocop:enable Metrics/MethodLength
108
+
109
+ # Mints tokens to a token account.
110
+ #
111
+ # @param payer [#to_s, PublicKey] The keypair that will pay for fees and rent.
112
+ # @param sign [Boolean] Whether to sign the transaction.
113
+ # @param execute [Boolean] Whether to execute the transaction.
114
+ # @param composer_opts [Hash] Options for {#compose_mint_to}.
115
+ # @return [Transaction] The created or sent transaction.
116
+ def mint_to(
117
+ payer:,
118
+ sign: true,
119
+ execute: true,
120
+ **composer_opts
121
+ )
122
+ composer = compose_mint_to(**composer_opts)
123
+
124
+ yield composer if block_given?
125
+
126
+ tx = composer
127
+ .set_fee_payer(payer)
128
+ .compose_transaction
129
+
130
+ if sign
131
+ tx.sign(
132
+ payer,
133
+ composer_opts[:mint_authority]
134
+ )
135
+
136
+ connection.send_transaction(tx.serialize) if execute
137
+ end
138
+
139
+ tx
140
+ end
141
+
142
+ # Prepares a mint_to instruction.
143
+ #
144
+ # @param amount [Integer] The amount of tokens to mint.
145
+ # @param mint [#to_s, PublicKey] The mint of the token.
146
+ # @param destination [#to_s, PublicKey] The destination token account.
147
+ # @param mint_authority [#to_s, PublicKey] The mint authority.
148
+ # @return [TransactionComposer] A composer with the mint_to instruction.
149
+ def compose_mint_to(
150
+ mint:,
151
+ amount:,
152
+ destination:,
153
+ mint_authority:
154
+ )
155
+ ix = mint_to_composer_class.new(
156
+ amount: amount,
157
+ mint: mint,
158
+ destination: destination,
159
+ mint_authority: mint_authority
160
+ )
161
+
162
+ TransactionComposer
163
+ .new(connection: connection)
164
+ .add_instruction(ix)
165
+ end
166
+
167
+ # Transfers tokens from one account to another.
168
+ #
169
+ # @param payer [#to_s, PublicKey] The keypair that will pay for fees and rent.
170
+ # @param sign [Boolean] Whether to sign the transaction.
171
+ # @param execute [Boolean] Whether to execute the transaction.
172
+ # @param composer_opts [Hash] Options for {#compose_transfer}.
173
+ # @return [Transaction] The created or sent transaction.
174
+ def transfer(
175
+ payer:,
176
+ sign: true,
177
+ execute: true,
178
+ **composer_opts
179
+ )
180
+ composer = compose_transfer(**composer_opts)
181
+
182
+ yield composer if block_given?
183
+
184
+ tx = composer
185
+ .set_fee_payer(payer)
186
+ .compose_transaction
187
+
188
+ if sign
189
+ tx.sign(
190
+ payer,
191
+ composer_opts[:owner]
192
+ )
193
+
194
+ connection.send_transaction(tx.serialize) if execute
195
+ end
196
+ tx
197
+ end
198
+
199
+ # Prepares a transfer instruction.
200
+ #
201
+ # @param source [#to_s, PublicKey] The source token account address.
202
+ # @param destination [#to_s, PublicKey] The destination token account address.
203
+ # @param amount [Integer] The number of tokens to transfer.
204
+ # @param owner [#to_s, PublicKey] The owner of the source account.
205
+ # @return [TransactionComposer] A composer with the transfer instruction.
206
+ def compose_transfer(
207
+ amount:,
208
+ source:,
209
+ destination:,
210
+ owner:
211
+ )
212
+ ix = transfer_composer_class.new(
213
+ amount: amount,
214
+ owner: owner,
215
+ source: source,
216
+ destination: destination
217
+ )
218
+
219
+ TransactionComposer
220
+ .new(connection: connection)
221
+ .add_instruction(ix)
222
+ end
223
+
224
+ # Transfers tokens with decimal precision and validation checks.
225
+ #
226
+ # @param payer [#to_s, PublicKey] The keypair that will pay for fees and rent.
227
+ # @param sign [Boolean] Whether to sign the transaction.
228
+ # @param execute [Boolean] Whether to execute the transaction.
229
+ # @param composer_opts [Hash] Options for {#compose_transfer_checked}.
230
+ # @return [Transaction] The created or sent transaction.
231
+ def transfer_checked(
232
+ payer:,
233
+ sign: true,
234
+ execute: true,
235
+ **composer_opts
236
+ )
237
+ composer = compose_transfer_checked(**composer_opts)
238
+
239
+ yield composer if block_given?
240
+
241
+ tx = composer
242
+ .set_fee_payer(payer)
243
+ .compose_transaction
244
+
245
+ if sign
246
+ tx.sign(
247
+ payer,
248
+ composer_opts[:authority]
249
+ )
250
+
251
+ connection.send_transaction(tx.serialize) if execute
252
+ end
253
+
254
+ tx
255
+ end
256
+
257
+ # Prepares a transfer_checked instruction.
258
+ #
259
+ # @param amount [Integer] The number of tokens to transfer.
260
+ # @param decimals [Integer] The number of decimals for the token.
261
+ # @param from [#to_s, PublicKey] The source token account address.
262
+ # @param to [#to_s, PublicKey] The destination token account address.
263
+ # @param mint [#to_s, PublicKey] The mint address.
264
+ # @param authority [#to_s, PublicKey] The owner of the source account.
265
+ # @return [TransactionComposer] A composer with the transfer_checked instruction.
266
+ def compose_transfer_checked(
267
+ to:,
268
+ from:,
269
+ mint:,
270
+ authority:,
271
+ amount:,
272
+ decimals:
273
+ )
274
+ ix = transfer_checked_composer_class.new(
275
+ to: to,
276
+ from: from,
277
+ mint: mint,
278
+ authority: authority,
279
+ amount: amount,
280
+ decimals: decimals
281
+ )
282
+
283
+ TransactionComposer
284
+ .new(connection: connection)
285
+ .add_instruction(ix)
286
+ end
287
+
288
+ private
289
+
290
+ # @return [Class] Composer class used by {#compose_create_mint}.
291
+ def initialize_mint_composer_class
292
+ raise NotImplementedError, "#{self.class} must implement #initialize_mint_composer_class"
293
+ end
294
+
295
+ # @return [Class] Composer class used by {#compose_mint_to}.
296
+ def mint_to_composer_class
297
+ raise NotImplementedError, "#{self.class} must implement #mint_to_composer_class"
298
+ end
299
+
300
+ # @return [Class] Composer class used by {#compose_transfer}.
301
+ def transfer_composer_class
302
+ raise NotImplementedError, "#{self.class} must implement #transfer_composer_class"
303
+ end
304
+
305
+ # @return [Class] Composer class used by {#compose_transfer_checked}.
306
+ def transfer_checked_composer_class
307
+ raise NotImplementedError, "#{self.class} must implement #transfer_checked_composer_class"
308
+ end
309
+ end
310
+ # rubocop:enable Metrics/ModuleLength
311
+ end
312
+ end
@@ -40,7 +40,7 @@ module Solace
40
40
  #
41
41
  # @return [Array<Integer>] The writable indexes
42
42
  def next_extract_writable_indexes
43
- length, = Codecs.decode_compact_u16(io)
43
+ length, = Codecs.decode_compact_u16(io)
44
44
  record.writable_indexes = io.read(length).unpack('C*')
45
45
  end
46
46
 
@@ -52,7 +52,7 @@ module Solace
52
52
  #
53
53
  # @return [Array<Integer>] The readonly indexes
54
54
  def next_extract_readonly_indexes
55
- length, = Codecs.decode_compact_u16(io)
55
+ length, = Codecs.decode_compact_u16(io)
56
56
  record.readonly_indexes = io.read(length).unpack('C*')
57
57
  end
58
58
  end
@@ -36,7 +36,7 @@ module Solace
36
36
  # @return [BaseDeserializer] The new deserializer object
37
37
  def initialize(io)
38
38
  super()
39
- @io = io
39
+ @io = io
40
40
  @record = self.class.record_class.new
41
41
  end
42
42
 
@@ -40,7 +40,7 @@ module Solace
40
40
  #
41
41
  # @return [Array] The accounts
42
42
  def next_extract_accounts
43
- length, = Codecs.decode_compact_u16(io)
43
+ length, = Codecs.decode_compact_u16(io)
44
44
  record.accounts = io.read(length).unpack('C*')
45
45
  end
46
46
 
@@ -52,7 +52,7 @@ module Solace
52
52
  #
53
53
  # @return [Array] The instruction data
54
54
  def next_extract_data
55
- length, = Codecs.decode_compact_u16(io)
55
+ length, = Codecs.decode_compact_u16(io)
56
56
  record.data = io.read(length).unpack('C*')
57
57
  end
58
58
  end
@@ -64,7 +64,7 @@ module Solace
64
64
  #
65
65
  # @return [Array<String>] The account keys of the message
66
66
  def next_extract_accounts
67
- count, = Codecs.decode_compact_u16(io)
67
+ count, = Codecs.decode_compact_u16(io)
68
68
  record.accounts = count.times.map do
69
69
  Codecs.bytes_to_base58 io.read(32).bytes
70
70
  end
@@ -88,7 +88,7 @@ module Solace
88
88
  #
89
89
  # @return [Array<Solace::Instruction>] The instructions of the message
90
90
  def next_extract_instructions
91
- count, = Codecs.decode_compact_u16(io)
91
+ count, = Codecs.decode_compact_u16(io)
92
92
  record.instructions = count.times.map do
93
93
  Solace::Instruction.deserialize(io)
94
94
  end
@@ -104,7 +104,7 @@ module Solace
104
104
  def next_extract_address_lookup_table
105
105
  return unless record.versioned?
106
106
 
107
- count, = Codecs.decode_compact_u16(io)
107
+ count, = Codecs.decode_compact_u16(io)
108
108
  record.address_lookup_tables = count.times.map do
109
109
  Solace::AddressLookupTable.deserialize(io)
110
110
  end
@@ -29,7 +29,7 @@ module Solace
29
29
  #
30
30
  # @return [Array] Array of base58 encoded signatures
31
31
  def next_extract_signatures
32
- count, = Codecs.decode_compact_u16(io)
32
+ count, = Codecs.decode_compact_u16(io)
33
33
  record.signatures = count.times.map { io.read(64) }
34
34
  end
35
35
 
@@ -18,7 +18,7 @@ module Solace
18
18
  # @param metadata [Hash] A hash containing the token's metadata attributes
19
19
  # @return [self] The initialized Token object
20
20
  def initialize(symbol, metadata)
21
- @symbol = symbol
21
+ @symbol = symbol
22
22
  @metadata = metadata.transform_keys(&:to_sym)
23
23
  end
24
24
 
data/lib/solace/tokens.rb CHANGED
@@ -29,7 +29,7 @@ module Solace
29
29
  # @param network [String, Symbol] Network name (e.g., 'mainnet', 'testnet')
30
30
  # @return [void]
31
31
  def self.load(path:, network:)
32
- data = YAML.load_file(path)
32
+ data = YAML.load_file(path)
33
33
  tokens = data.fetch(network.to_s) do
34
34
  raise ArgumentError, "Network '#{network}' not found in config"
35
35
  end
@@ -40,7 +40,7 @@ module Solace
40
40
  @registry = {}
41
41
 
42
42
  tokens.each do |symbol, attrs|
43
- token = Solace::Tokens::Token.new(symbol, attrs)
43
+ token = Solace::Tokens::Token.new(symbol, attrs)
44
44
  const_set(symbol, token)
45
45
  @registry[symbol.to_s] = token
46
46
  end
@@ -66,7 +66,7 @@ module Solace
66
66
  )
67
67
  super()
68
68
  @signatures = signatures
69
- @message = message
69
+ @message = message
70
70
  end
71
71
 
72
72
  # Returns the first signature of the transaction (signature of the transaction fee payer)
@@ -77,9 +77,9 @@ module Solace
77
77
  #
78
78
  # @param connection [Solace::Connection] The connection to the Solana cluster
79
79
  def initialize(connection:)
80
- @connection = connection
80
+ @connection = connection
81
81
  @instruction_composers = []
82
- @context = Utils::AccountContext.new
82
+ @context = Utils::AccountContext.new
83
83
  end
84
84
 
85
85
  # Add an instruction composer to the transaction
@@ -161,9 +161,9 @@ module Solace
161
161
  context.compile
162
162
 
163
163
  message = Solace::Message.new(
164
- header: context.header,
165
- accounts: context.accounts,
166
- instructions: build_instructions,
164
+ header: context.header,
165
+ accounts: context.accounts,
166
+ instructions: build_instructions,
167
167
  recent_blockhash: connection.get_latest_blockhash[0]
168
168
  )
169
169
 
@@ -35,8 +35,8 @@ module Solace
35
35
  #
36
36
  # @return [Hash] The default account data with lowest level of permissions
37
37
  DEFAULT_ACCOUNT = {
38
- signer: false,
39
- writable: false,
38
+ signer: false,
39
+ writable: false,
40
40
  fee_payer: false
41
41
  }.freeze
42
42
 
@@ -60,8 +60,8 @@ module Solace
60
60
 
61
61
  # Initialize the account context
62
62
  def initialize
63
- @header = []
64
- @accounts = []
63
+ @header = []
64
+ @accounts = []
65
65
  @pubkey_account_map = {}
66
66
  end
67
67
 
@@ -175,7 +175,7 @@ module Solace
175
175
  #
176
176
  # @return [Hash] The compiled accounts and header
177
177
  def compile
178
- self.header = calculate_header
178
+ self.header = calculate_header
179
179
  self.accounts = order_accounts
180
180
  self
181
181
  end
@@ -206,9 +206,9 @@ module Solace
206
206
  def merge_account(pubkey, signer:, writable:, fee_payer: false)
207
207
  pubkey_str = pubkey.is_a?(String) ? pubkey : pubkey.address
208
208
 
209
- @pubkey_account_map[pubkey_str] ||= DEFAULT_ACCOUNT.dup
210
- @pubkey_account_map[pubkey_str][:signer] ||= signer
211
- @pubkey_account_map[pubkey_str][:writable] ||= writable
209
+ @pubkey_account_map[pubkey_str] ||= DEFAULT_ACCOUNT.dup
210
+ @pubkey_account_map[pubkey_str][:signer] ||= signer
211
+ @pubkey_account_map[pubkey_str][:writable] ||= writable
212
212
  @pubkey_account_map[pubkey_str][:fee_payer] ||= fee_payer
213
213
 
214
214
  self
@@ -242,7 +242,7 @@ module Solace
242
242
  @pubkey_account_map.keys.each_with_object([0, 0, 0]) do |pubkey, acc|
243
243
  acc[0] += 1 if signer?(pubkey)
244
244
 
245
- if readonly_signer?(pubkey) then acc[1] += 1
245
+ if readonly_signer?(pubkey) then acc[1] += 1
246
246
  elsif readonly_nonsigner?(pubkey) then acc[2] += 1
247
247
  end
248
248
  end