solace 0.0.2 → 0.0.3

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: 78ff45cbaff80411dfec8433e478a0b7a97caea5fb97ae6b8159e2ea7fa409fc
4
- data.tar.gz: 13c025072948cc13dfc61d0d7c431df5ae4af90bcca76e239d1fff6cb0033d4f
3
+ metadata.gz: 73c15e6b362288fa1f3be0fd2793fcf43aff390deabe20f3264f22550cb67734
4
+ data.tar.gz: 1b036900c1306c1af4f4c0887a625d6a633a7cf4276c79c1a8b61754d7ffe593
5
5
  SHA512:
6
- metadata.gz: 6d7dfe812cbe1a44275a6a62d36555cdf666e838d1ed37777efc9072bd91605ac4e0a8fca931a29405c387944c3bcef5c949022c21af762dfba09e7e6ab2530d
7
- data.tar.gz: 181f9c5eb217ad0499b76f6e0052e3f9131d46c12c76583f8a3b1ad05d10274347158512e490656ed41c0c44a07b7645648b0e89b956de8691bf2b4916eebdd6
6
+ metadata.gz: 422a09d627bbf0e9bb59f3183f4a1ad34904a14996291555ff7c83717554c0915c4ba1d93c689b852cb9d9fab024cfd0e491c0443b332a6dc6f448028c48e000
7
+ data.tar.gz: a185276e03d031c72c7a78b11db5fd3a9b1d1fa6388758b41c20fad66241aa14b44e5188caace7d0bffaf498c03834d4fe73461754834c3a33e412a13544bf1c
data/CHANGELOG CHANGED
@@ -0,0 +1,45 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
5
+
6
+ ### Template
7
+
8
+ ```markdown
9
+ ## [VERSION] - yyyy-mm-dd
10
+
11
+ ### Added
12
+ 1.
13
+
14
+ ### Changed
15
+
16
+ ### Fixed
17
+ ```
18
+
19
+ ## [0.0.3] - 2025-07-30
20
+
21
+ ### Added
22
+ 1. Moved `TransferCheckedInstruction` and `TransferInstruction` to `Solace::Instructions::SystemProgram` namespace.
23
+ 2. Added `get_token_account_balance` to `Solace::Connection`.
24
+ 3. Added `get_or_create_address` to `Solace::Programs::AssociatedTokenAccount`.
25
+ 4. Added Composer system for handling accounts and instructions in a higher level abstraction.
26
+ 5. Added `SystemProgramTransferComposer` to `Solace::Composers` namespace.
27
+ 6. Added `SplTokenProgramTransferCheckedComposer` to `Solace::Composers` namespace.
28
+ 7. Moved TransferCheckedInstruction to proper namespace `Solace::Instructions::SplToken::TransferCheckedInstruction`.
29
+ 8. Updated `get_or_create_address` to wait for confirmation before returning the address in `Solace::Programs::AssociatedTokenAccount`.
30
+ 9. Moved default options to instance variable in `Solace::Connection`.
31
+ 10. Moved test setup to `test/bootstrap.rb` and added rake task `bootstrap` to run it.
32
+
33
+ ### Changed
34
+
35
+ THESE ARE BREAKING CHANGES FROM 0.0.2 TO 0.0.3. Solice is still in alpha and the API is subject to change.
36
+
37
+ There was a bit of a refactoring of the codebase to make it more maintainable and to make it easier to add new features. Additonally, the SDK now has `Composers` for handling accounts and instructions in a higher level abstraction. These composers are used in the tests and can be used in your own code as well.
38
+
39
+ Soon, YARD documentation will be added to the SDK and published to https://solace-rb.github.io/solace/.
40
+
41
+ Stay tuned.
42
+
43
+ ### Fixed
44
+
45
+ N/A
data/LICENSE CHANGED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sebastian Scholl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -174,7 +174,7 @@ Instruction builders are service objects that create specific instruction types.
174
174
 
175
175
  ```ruby
176
176
  # Build a SOL transfer instruction
177
- transfer_ix = Solace::Instructions::TransferInstruction.build(
177
+ transfer_ix = Solace::Instructions::SystemProgram::TransferInstruction.build(
178
178
  lamports: 1_000_000, # 0.001 SOL
179
179
  from_index: 0, # Sender account index
180
180
  to_index: 1, # Recipient account index
@@ -433,7 +433,7 @@ recipient = Solace::Keypair.generate
433
433
  connection.request_airdrop(payer.address, 1_000_000_000)
434
434
 
435
435
  # Build transfer instruction
436
- transfer_ix = Solace::Instructions::TransferInstruction.build(
436
+ transfer_ix = Solace::Instructions::SystemProgram::TransferInstruction.build(
437
437
  lamports: 100_000_000, # 0.1 SOL
438
438
  from_index: 0,
439
439
  to_index: 1,
@@ -0,0 +1,40 @@
1
+ module Solace
2
+ module Composers
3
+ class Base
4
+ # @!attribute params
5
+ # The params for the composer
6
+ #
7
+ # @return [Hash] The parameters passed to the composer
8
+ attr_reader :params
9
+
10
+ # @!attribute account_context
11
+ # The account_context for the composer
12
+ #
13
+ # @return [Utils::AccountContext] The AccountContext instance for the composer
14
+ attr_reader :account_context
15
+
16
+ # Initialize the composer
17
+ #
18
+ # @param **params [Hash] Parameters to pass to the composer constructor
19
+ def initialize(**params)
20
+ @params = params
21
+ @account_context = Utils::AccountContext.new
22
+ setup_accounts
23
+ end
24
+
25
+ # Setup accounts required for this instruction
26
+ #
27
+ # @return [void]
28
+ def setup_accounts
29
+ raise NotImplementedError, "Subclasses must implement setup_accounts method"
30
+ end
31
+
32
+ # Build instruction with resolved account indices
33
+ #
34
+ # @return [void]
35
+ def build_instruction(indices)
36
+ raise NotImplementedError, "Subclasses must implement build_instruction method"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Composers
5
+ class SplTokenProgramTransferCheckedComposer < Base
6
+ # Extracts the to address from the params
7
+ #
8
+ # @return [String] The to address
9
+ def to
10
+ params[:to].is_a?(String) ? params[:to] : params[:to].address
11
+ end
12
+
13
+ # Extracts the from address from the params
14
+ #
15
+ # @return [String] The from address
16
+ def from
17
+ params[:from].is_a?(String) ? params[:from] : params[:from].address
18
+ end
19
+
20
+ # Extracts the authority address from the params
21
+ #
22
+ # The authority is the owner of the token account
23
+ #
24
+ # @return [String] The authority address
25
+ def authority
26
+ params[:authority].is_a?(String) ? params[:authority] : params[:authority].address
27
+ end
28
+
29
+ # Extracts the mint address from the params
30
+ #
31
+ # @return [String] The mint address
32
+ def mint
33
+ params[:mint].is_a?(String) ? params[:mint] : params[:mint].address
34
+ end
35
+
36
+ # Returns the spl token program id
37
+ #
38
+ # @return [String] The spl token program id
39
+ def spl_token_program
40
+ Constants::TOKEN_PROGRAM_ID
41
+ end
42
+
43
+ # Returns the lamports to transfer
44
+ #
45
+ # @return [Integer] The lamports to transfer
46
+ def amount
47
+ params[:amount]
48
+ end
49
+
50
+ # Returns the decimals for the mint of the token
51
+ #
52
+ # @return [Integer] The decimals for the mint
53
+ def decimals
54
+ params[:decimals]
55
+ end
56
+
57
+ # Setup accounts required for transfer instruction
58
+ # Called automatically during initialization
59
+ #
60
+ # @return [void]
61
+ def setup_accounts
62
+ account_context.add_writable_signer(authority)
63
+ account_context.add_writable_nonsigner(to)
64
+ account_context.add_writable_nonsigner(from)
65
+ account_context.add_readonly_nonsigner(mint)
66
+ account_context.add_readonly_nonsigner(spl_token_program)
67
+ end
68
+
69
+ # Build instruction with resolved account indices
70
+ #
71
+ # @param account_context [Utils::AccountContext] The account context
72
+ # @return [Solace::Instruction]
73
+ def build_instruction(account_context)
74
+ Instructions::SplToken::TransferCheckedInstruction.build(
75
+ amount: amount,
76
+ decimals: decimals,
77
+ to_index: account_context.index_of(to),
78
+ from_index: account_context.index_of(from),
79
+ mint_index: account_context.index_of(mint),
80
+ authority_index: account_context.index_of(authority),
81
+ program_index: account_context.index_of(spl_token_program)
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Composers
5
+ class SystemProgramTransferComposer < Base
6
+ # Extracts the to address from the params
7
+ #
8
+ # @return [String] The to address
9
+ def to
10
+ params[:to].is_a?(String) ? params[:to] : params[:to].address
11
+ end
12
+
13
+ # Extracts the from address from the params
14
+ #
15
+ # @return [String] The from address
16
+ def from
17
+ params[:from].is_a?(String) ? params[:from] : params[:from].address
18
+ end
19
+
20
+ # Returns the system program id
21
+ #
22
+ # @return [String] The system program id
23
+ def system_program
24
+ Solace::Constants::SYSTEM_PROGRAM_ID
25
+ end
26
+
27
+ # Returns the lamports to transfer
28
+ #
29
+ # @return [Integer] The lamports to transfer
30
+ def lamports
31
+ params[:lamports]
32
+ end
33
+
34
+ # Setup accounts required for transfer instruction
35
+ # Called automatically during initialization
36
+ #
37
+ # @return [void]
38
+ def setup_accounts
39
+ account_context.add_writable_signer(from)
40
+ account_context.add_writable_nonsigner(to)
41
+ account_context.add_readonly_nonsigner(system_program)
42
+ end
43
+
44
+ # Build instruction with resolved account indices
45
+ #
46
+ # @param account_context [Utils::AccountContext] The account context
47
+ # @return [Solace::Instruction]
48
+ def build_instruction(account_context)
49
+ Instructions::SystemProgram::TransferInstruction.build(
50
+ lamports: lamports,
51
+ to_index: account_context.index_of(to),
52
+ from_index: account_context.index_of(from),
53
+ program_index: account_context.index_of(system_program)
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -6,21 +6,31 @@ require 'uri'
6
6
 
7
7
  module Solace
8
8
  class Connection
9
+ # @!attribute [r] rpc_url
10
+ # The URL of the Solana RPC node
11
+ #
12
+ # @return [String] The URL of the Solana RPC node
9
13
  attr_reader :rpc_url
10
14
 
11
- # !const default options
12
- DEFAULT_OPTIONS = {
13
- encoding: 'base64',
14
- commitment: 'confirmed'
15
- }.freeze
15
+ # @!attribute [r] default_options
16
+ # The default options for RPC requests
17
+ #
18
+ # @return [Hash] The default options for RPC requests
19
+ attr_reader :default_options
16
20
 
17
21
  # Initialize the connection with a default or custom RPC URL
18
22
  #
19
23
  # @param rpc_url [String] The URL of the Solana RPC node
20
24
  # @return [Solace::Connection] The connection object
21
- def initialize(rpc_url = 'http://localhost:8899')
22
- @rpc_url = rpc_url
25
+ def initialize(rpc_url = 'http://localhost:8899', commitment: 'confirmed')
23
26
  @request_id = nil
27
+ @rpc_url = rpc_url
28
+
29
+ # Set default options
30
+ @default_options = {
31
+ commitment: commitment,
32
+ encoding: 'base64'
33
+ }
24
34
  end
25
35
 
26
36
  # Make an RPC request to the Solana node
@@ -68,7 +78,7 @@ module Solace
68
78
  [
69
79
  pubkey,
70
80
  lamports,
71
- DEFAULT_OPTIONS.merge(options)
81
+ default_options.merge(options)
72
82
  ]
73
83
  )
74
84
  end
@@ -97,7 +107,7 @@ module Solace
97
107
  'getAccountInfo',
98
108
  [
99
109
  pubkey,
100
- DEFAULT_OPTIONS
110
+ default_options
101
111
  ]
102
112
  )['result']
103
113
 
@@ -115,7 +125,21 @@ module Solace
115
125
  'getBalance',
116
126
  [
117
127
  pubkey,
118
- DEFAULT_OPTIONS
128
+ default_options
129
+ ]
130
+ )['result']['value']
131
+ end
132
+
133
+ # Get the balance of a token account
134
+ #
135
+ # @param token_account [String] The public key of the token account
136
+ # @return [Hash] Token account balance information with amount and decimals
137
+ def get_token_account_balance(token_account)
138
+ rpc_request(
139
+ 'getTokenAccountBalance',
140
+ [
141
+ token_account,
142
+ default_options
119
143
  ]
120
144
  )['result']['value']
121
145
  end
@@ -129,7 +153,7 @@ module Solace
129
153
  'getTransaction',
130
154
  [
131
155
  signature,
132
- DEFAULT_OPTIONS.merge(options)
156
+ default_options.merge(options)
133
157
  ]
134
158
  )['result']
135
159
  end
@@ -143,7 +167,7 @@ module Solace
143
167
  'getSignatureStatuses',
144
168
  [
145
169
  signatures,
146
- DEFAULT_OPTIONS.merge({ 'searchTransactionHistory' => true })
170
+ default_options.merge({ 'searchTransactionHistory' => true })
147
171
  ]
148
172
  )['result']
149
173
  end
@@ -157,7 +181,7 @@ module Solace
157
181
  'sendTransaction',
158
182
  [
159
183
  transaction,
160
- DEFAULT_OPTIONS.merge(options)
184
+ default_options.merge(options)
161
185
  ]
162
186
  )
163
187
  end
@@ -172,15 +196,17 @@ module Solace
172
196
  # Get the signature from the block
173
197
  signature = yield
174
198
 
199
+ interval = 0.1
200
+
175
201
  # Wait for confirmation
176
202
  loop do
177
203
  status = get_signature_status([signature]).dig('value', 0)
178
-
204
+
179
205
  break if status && status['confirmationStatus'] == commitment
180
-
181
- sleep 0.5
206
+
207
+ sleep interval
182
208
  end
183
-
209
+
184
210
  signature
185
211
  end
186
212
  end
@@ -0,0 +1,21 @@
1
+ module Solace
2
+ module Instructions
3
+ class Base
4
+ class << self
5
+ # Must implement build method
6
+ #
7
+ # @return [Solace::Instruction] The instruction
8
+ def build
9
+ raise NotImplementedError, "Subclasses must implement build method"
10
+ end
11
+
12
+ # Must implement data method
13
+ #
14
+ # @return [Array<Integer>] The instruction data
15
+ def data
16
+ raise NotImplementedError, "Subclasses must implement data method"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Instructions
5
+ module SplToken
6
+ # Service object for building an SPL Token Program transfer instruction
7
+ class TransferCheckedInstruction
8
+ # SPL Token Program instruction index for Transfer Checked
9
+ INSTRUCTION_INDEX = [12].freeze
10
+
11
+ # Builds a Solace::Instruction for transferring SPL tokens
12
+ #
13
+ # SPL Token Program transfer instruction layout:
14
+ # - 1 byte: instruction index (12 for transfer checked)
15
+ # - 8 bytes: amount (u64, little-endian)
16
+ # - 8 bytes: decimals (u64, little-endian)
17
+ #
18
+ # @param amount [Integer] Amount to transfer (in tokens, according to mint's decimals)
19
+ # @param decimals [Integer] Number of decimals for the token
20
+ # @param to_index [Integer] Index of the destination token account in the transaction's accounts
21
+ # @param from_index [Integer] Index of the source token account in the transaction's accounts
22
+ # @param mint_index [Integer] Index of the mint in the transaction's accounts
23
+ # @param authority_index [Integer] Index of the authority (owner) in the transaction's accounts
24
+ # @param program_index [Integer] Index of the SPL Token Program in the transaction's accounts (default: 3)
25
+ # @return [Solace::Instruction]
26
+ def self.build(
27
+ amount:,
28
+ decimals:,
29
+ to_index:,
30
+ from_index:,
31
+ mint_index:,
32
+ authority_index:,
33
+ program_index: 3
34
+ )
35
+ Solace::Instruction.new.tap do |ix|
36
+ ix.program_index = program_index
37
+ ix.accounts = [from_index, mint_index, to_index, authority_index]
38
+ ix.data = data(amount, decimals)
39
+ end
40
+ end
41
+
42
+ # Instruction data for a token transfer instruction
43
+ #
44
+ # The BufferLayout is:
45
+ # - [Instruction Index (1 byte)]
46
+ # - [Amount (8 bytes little-endian u64)]
47
+ # - [Decimals (8 bytes little-endian u64)]
48
+ #
49
+ # @param amount [Integer] Amount to transfer
50
+ # @param decimals [Integer] Number of decimals for the token
51
+ # @return [Array] 1-byte instruction index + 8-byte amount + decimals
52
+ def self.data(amount, decimals)
53
+ INSTRUCTION_INDEX +
54
+ Solace::Utils::Codecs.encode_le_u64(amount).bytes +
55
+ [decimals]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Instructions
5
+ module SystemProgram
6
+ # Service object for building a System Program transfer instruction
7
+ class TransferInstruction < Base
8
+ # Instruction ID for System Transfer
9
+ INSTRUCTION_ID = [2, 0, 0, 0].freeze
10
+
11
+ # Builds a Solace::Instruction for transferring SOL
12
+ #
13
+ # System Program transfer instruction layout:
14
+ # - 4 bytes: instruction index (0 for transfer)
15
+ # - 8 bytes: amount (u64, little-endian)
16
+ #
17
+ # @param lamports [Integer] Amount to transfer (in lamports)
18
+ # @param to_index [Integer] Index of the recipient in the transaction's accounts
19
+ # @param from_index [Integer] Index of the sender in the transaction's accounts
20
+ # @param program_index [Integer] Index of the program in the transaction's accounts (default: 2)
21
+ # @return [Solace::Instruction]
22
+ def self.build(
23
+ lamports:,
24
+ to_index:,
25
+ from_index:,
26
+ program_index: 2
27
+ )
28
+ Instruction.new.tap do |ix|
29
+ ix.program_index = program_index
30
+ ix.accounts = [from_index, to_index]
31
+ ix.data = data(lamports)
32
+ end
33
+ end
34
+
35
+ # Instruction data for a transfer instruction
36
+ #
37
+ # The BufferLayout is:
38
+ # - [Instruction ID (4 bytes)]
39
+ # - [Amount (8 bytes little-endian u64)]
40
+ #
41
+ # @param lamports [Integer] Amount to transfer (in lamports)
42
+ # @return [Array] 4-byte instruction ID + 8-byte amount
43
+ def self.data(lamports)
44
+ INSTRUCTION_ID +
45
+ Utils::Codecs.encode_le_u64(lamports).bytes
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -36,6 +36,28 @@ module Solace
36
36
  self.class.get_address(**options)
37
37
  end
38
38
 
39
+ # Gets the address of an associated token account, creating it if it doesn't exist.
40
+ #
41
+ # @param payer [Solace::Keypair] The keypair that will pay for fees and rent (required if account needs to be created).
42
+ # @param owner [Solace::Keypair || Solace::PublicKey] The keypair of the owner.
43
+ # @param mint [Solace::Keypair || Solace::PublicKey] The keypair of the mint.
44
+ # @return [String] The address of the associated token account
45
+ def get_or_create_address(payer:, owner:, mint:, commitment: 'confirmed')
46
+ ata_address, _ = get_address(owner:, mint:)
47
+
48
+ account_info = @connection.get_account_info(ata_address)
49
+
50
+ return ata_address if account_info
51
+
52
+ response = create_associated_token_account(payer:, owner:, mint:)
53
+
54
+ raise "Failed to create associated token account" unless response['result']
55
+
56
+ @connection.wait_for_confirmed_signature(commitment) { response['result'] }
57
+
58
+ ata_address
59
+ end
60
+
39
61
  # Creates a new associated token account.
40
62
  #
41
63
  # @param options [Hash] Options for calling the prepare_create_associated_token_account method.
@@ -53,9 +75,9 @@ module Solace
53
75
  # @param payer [Solace::Keypair] The keypair that will pay for fees and rent.
54
76
  # @return [Solace::Transaction] The signed transaction.
55
77
  def prepare_create_associated_token_account(
56
- owner:,
57
- mint:,
58
- payer:
78
+ payer:,
79
+ owner:,
80
+ mint:
59
81
  )
60
82
  ata_address, _ = get_address(owner:, mint:)
61
83
 
@@ -0,0 +1,81 @@
1
+ # lib/solace/transaction_composer.rb
2
+ module Solace
3
+ # Composes multi-instruction transactions with automatic account management
4
+ class TransactionComposer
5
+
6
+ # @!attribute connection
7
+ #
8
+ # @return [Solace::Connection] The connection to the Solana cluster
9
+ attr_reader :connection
10
+
11
+ # @!attribute context
12
+ #
13
+ # @return [Utils::AccountContext] The account registry
14
+ attr_reader :context
15
+
16
+ # @!attribute instruction_composers
17
+ #
18
+ # @return [Array<Composers::Base>] The instruction composers
19
+ attr_reader :instruction_composers
20
+
21
+ # Initialize the composer
22
+ #
23
+ # @param connection [Solace::Connection] The connection to the Solana cluster
24
+ def initialize(connection:)
25
+ @connection = connection
26
+ @instruction_composers = []
27
+ @context = Utils::AccountContext.new
28
+ end
29
+
30
+ # Add an instruction composer to the transaction
31
+ #
32
+ # @param composer [Composers::Base] The instruction composer
33
+ # @return [TransactionComposer] Self for chaining
34
+ def add_instruction(composer)
35
+ merge_accounts(composer.account_context)
36
+ instruction_composers << composer
37
+ self
38
+ end
39
+
40
+ # Set the fee payer for the transaction
41
+ #
42
+ # @param pubkey [String | Solace::PublicKey | Solace::Keypair] The fee payer pubkey
43
+ # @return [TransactionComposer] Self for chaining
44
+ def set_fee_payer(pubkey)
45
+ context.set_fee_payer(pubkey)
46
+ self
47
+ end
48
+
49
+ # Compose the final transaction
50
+ #
51
+ # @return [Solace::Transaction] The composed transaction (unsigned)
52
+ def compose_transaction
53
+ context.compile
54
+
55
+ message = Solace::Message.new(
56
+ header: context.header,
57
+ accounts: context.accounts,
58
+ instructions: build_instructions,
59
+ recent_blockhash: connection.get_latest_blockhash,
60
+ )
61
+
62
+ Solace::Transaction.new(message: message)
63
+ end
64
+
65
+ private
66
+
67
+ # Build all instructions with resolved indices
68
+ #
69
+ # @return [Array<Solace::Instruction>] The built instructions
70
+ def build_instructions
71
+ instruction_composers.map { _1.build_instruction(context) }
72
+ end
73
+
74
+ # Merge all accounts from another AccountContext into this one
75
+ #
76
+ # @param account_context [AccountContext] The other context to merge from
77
+ def merge_accounts(account_context)
78
+ context.merge_from(account_context)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ module Utils
5
+ # Internal utility for managing account context in transaction building
6
+ # with automatic deduplication and sorting
7
+ class AccountContext
8
+ # @!attribute DEFAULT_ACCOUNT
9
+ # The default account data
10
+ #
11
+ # @return [Hash] The default account data with lowest level of permissions
12
+ DEFAULT_ACCOUNT = {
13
+ signer: false,
14
+ writable: false,
15
+ fee_payer: false,
16
+ }
17
+
18
+ # @!attribute header
19
+ # The header for the transaction
20
+ #
21
+ # @return [Array<Integer>] The header for the transaction
22
+ attr_accessor :header
23
+
24
+ # @!attribute accounts
25
+ # The accounts in the transaction
26
+ #
27
+ # @return [Array<String>] The accounts
28
+ attr_accessor :accounts
29
+
30
+ # @!attribute pubkey_account_map
31
+ # The map of accounts
32
+ #
33
+ # @return [Hash] The map of accounts
34
+ attr_accessor :pubkey_account_map
35
+
36
+ # Initialize the account context
37
+ def initialize
38
+ @header = []
39
+ @accounts = []
40
+ @pubkey_account_map = {}
41
+ end
42
+
43
+ # Set the fee payer account
44
+ #
45
+ # @param pubkey [Solace::Keypair | Solace::PublicKey | String] The pubkey of the fee payer account
46
+ def set_fee_payer(pubkey)
47
+ merge_account(pubkey, signer: true, writable: true, fee_payer: true)
48
+ end
49
+
50
+ # Add a signer account
51
+ #
52
+ # @param pubkey [Solace::Keypair | Solace::PublicKey | String] The pubkey of the signer account
53
+ def add_writable_signer(pubkey)
54
+ merge_account(pubkey, signer: true, writable: true)
55
+ end
56
+
57
+ # Add a writable account
58
+ #
59
+ # @param pubkey [Solace::Keypair | Solace::PublicKey | String] The pubkey of the writable account
60
+ def add_writable_nonsigner(pubkey)
61
+ merge_account(pubkey, signer: false, writable: true)
62
+ end
63
+
64
+ # Add a readonly signer account
65
+ #
66
+ # @param pubkey [Solace::Keypair | Solace::PublicKey | String] The pubkey of the readonly signer account
67
+ def add_readonly_signer(pubkey)
68
+ merge_account(pubkey, signer: true, writable: false)
69
+ end
70
+
71
+
72
+ # Add a readonly account
73
+ #
74
+ # @param pubkey [Solace::Keypair | Solace::PublicKey | String] The pubkey of the readonly account
75
+ def add_readonly_nonsigner(pubkey)
76
+ merge_account(pubkey, signer: false, writable: false)
77
+ end
78
+
79
+ # Predicate to check if an account is a fee payer
80
+ #
81
+ # @param pubkey [String] The pubkey of the account
82
+ # @return [Boolean] Whether the account is a fee payer
83
+ def fee_payer?(pubkey)
84
+ @pubkey_account_map[pubkey].try { |acc| acc[:fee_payer] }
85
+ end
86
+
87
+ # Predicate to check if an account is a signer
88
+ #
89
+ # @param pubkey [String] The pubkey of the account
90
+ # @return [Boolean] Whether the account is a signer
91
+ def signer?(pubkey)
92
+ @pubkey_account_map[pubkey].try { |acc| acc[:signer] }
93
+ end
94
+
95
+ # Predicate to check if an account is writable
96
+ #
97
+ # @param pubkey [String] The pubkey of the account
98
+ # @return [Boolean] Whether the account is writable
99
+ def writable?(pubkey)
100
+ @pubkey_account_map[pubkey].try { |acc| acc[:writable] }
101
+ end
102
+
103
+ # Predicate to check if an account is a writable signer
104
+ #
105
+ # @param pubkey [String] The pubkey of the account
106
+ # @return [Boolean] Whether the account is a writable signer
107
+ def writable_signer?(pubkey)
108
+ @pubkey_account_map[pubkey].try { |acc| acc[:signer] && acc[:writable] }
109
+ end
110
+
111
+ # Predicate to check if an account is writable and not a signer
112
+ #
113
+ # @param pubkey [String] The pubkey of the account
114
+ # @return [Boolean] Whether the account is writable and not a signer
115
+ def writable_nonsigner?(pubkey)
116
+ @pubkey_account_map[pubkey].try { |acc| !acc[:signer] && acc[:writable] }
117
+ end
118
+
119
+ # Predicate to check if an account is a readonly signer
120
+ #
121
+ # @param pubkey [String] The pubkey of the account
122
+ # @return [Boolean] Whether the account is a readonly signer
123
+ def readonly_signer?(pubkey)
124
+ @pubkey_account_map[pubkey].try { |acc| acc[:signer] && !acc[:writable] }
125
+ end
126
+
127
+ # Predicate to check if an account is readonly and not a signer
128
+ #
129
+ # @param pubkey [String] The pubkey of the account
130
+ # @return [Boolean] Whether the account is readonly and not a signer
131
+ def readonly_nonsigner?(pubkey)
132
+ @pubkey_account_map[pubkey].try { |acc| !acc[:signer] && !acc[:writable] }
133
+ end
134
+
135
+ # Merge all accounts from another AccountContext into this one
136
+ #
137
+ # @param other_context [AccountContext] The other context to merge from
138
+ def merge_from(other_context)
139
+ other_context.pubkey_account_map.each do |pubkey, data|
140
+ merge_account(
141
+ pubkey,
142
+ signer: data[:signer],
143
+ writable: data[:writable],
144
+ fee_payer: data[:fee_payer]
145
+ )
146
+ end
147
+ end
148
+
149
+ # Compile accounts into final format
150
+ #
151
+ # Gets unique accounts and sorts them in the following order:
152
+ # - Signers first (Solana requirement)
153
+ # - Then writable accounts
154
+ # - Then readonly accounts
155
+ #
156
+ # @return [Hash] The compiled accounts and header
157
+ def compile
158
+ self.header = calculate_header
159
+ self.accounts = order_accounts
160
+ self
161
+ end
162
+
163
+ # Index of a pubkey in the accounts array
164
+ #
165
+ # @param pubkey_str [String] The public key of the account
166
+ # @return [Integer] The index of the pubkey in the accounts array or -1 if not found
167
+ def index_of(pubkey_str)
168
+ indices[pubkey_str] || -1
169
+ end
170
+
171
+ # Get map of indicies for pubkeys in accounts array
172
+ #
173
+ # @return [Hash<String, Integer>] The indices of the pubkeys in the accounts array
174
+ def indices
175
+ accounts.each_with_index.to_h
176
+ end
177
+
178
+ private
179
+
180
+ # Add or merge an account into the context
181
+ #
182
+ # @param pubkey [String | Solace::PublicKey | Solace::Keypair] The public key of the account
183
+ # @param signer [Boolean] Whether the account is a signer
184
+ # @param writable [Boolean] Whether the account is writable
185
+ def merge_account(
186
+ pubkey,
187
+ signer:,
188
+ writable:,
189
+ fee_payer: false
190
+ )
191
+ pubkey_str = resolve_pubkey(pubkey)
192
+
193
+ @pubkey_account_map[pubkey_str] ||= DEFAULT_ACCOUNT.dup
194
+ @pubkey_account_map[pubkey_str][:signer] ||= signer
195
+ @pubkey_account_map[pubkey_str][:writable] ||= writable
196
+ @pubkey_account_map[pubkey_str][:fee_payer] ||= fee_payer
197
+
198
+ self
199
+ end
200
+
201
+ # Order accounts by signer, writable, readonly signer, readonly
202
+ #
203
+ # @return [Array<String>] The ordered accounts
204
+ def order_accounts
205
+ @pubkey_account_map.keys.sort_by do |pubkey|
206
+ if fee_payer?(pubkey)
207
+ 0
208
+ elsif writable_signer?(pubkey)
209
+ 1
210
+ elsif readonly_signer?(pubkey)
211
+ 2
212
+ elsif writable_nonsigner?(pubkey)
213
+ 2
214
+ elsif readonly_nonsigner?(pubkey)
215
+ 3
216
+ else
217
+ raise ArgumentError, "Unknown account type for pubkey: #{pubkey}"
218
+ end
219
+ end
220
+ end
221
+
222
+ # Calculate the header for the transaction
223
+ #
224
+ # @note The header is an array of three integers:
225
+ # - The number of signers (writable + readonly)
226
+ # - The number of readonly signers
227
+ # - The number of readonly unsigned accounts
228
+ #
229
+ # @return [Array] The header for the transaction
230
+ def calculate_header
231
+ @pubkey_account_map.keys.reduce([0, 0, 0]) do |acc, pubkey|
232
+ acc[0] += 1 if signer?(pubkey)
233
+
234
+ if readonly_signer?(pubkey)
235
+ acc[1] += 1
236
+ elsif readonly_nonsigner?(pubkey)
237
+ acc[2] += 1
238
+ end
239
+
240
+ acc
241
+ end
242
+ end
243
+
244
+ # Resolve the pubkey from a Solace::PublicKey or String
245
+ #
246
+ # @param pubkey [String|Solace::PublicKey] The pubkey to resolve
247
+ # @return [String] The resolved pubkey
248
+ def resolve_pubkey(pubkey)
249
+ pubkey.is_a?(String) ? pubkey : pubkey.address
250
+ end
251
+ end
252
+ end
253
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Solace
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
data/lib/solace.rb CHANGED
@@ -8,6 +8,7 @@ require_relative 'solace/constants'
8
8
  require_relative 'solace/connection'
9
9
  require_relative 'solace/utils/codecs'
10
10
  require_relative 'solace/utils/pda'
11
+ require_relative 'solace/utils/account_context'
11
12
  require_relative 'solace/utils/curve25519_dalek'
12
13
  require_relative 'solace/concerns/binary_serializable'
13
14
 
@@ -26,6 +27,12 @@ require_relative 'solace/transaction'
26
27
  require_relative 'solace/message'
27
28
  require_relative 'solace/instruction'
28
29
  require_relative 'solace/address_lookup_table'
30
+ require_relative 'solace/transaction_composer'
31
+
32
+ # 📦 Composers (Builders)
33
+ #
34
+ # Glob require all instructions
35
+ Dir[File.join(__dir__, 'solace/composers', '**', '*.rb')].sort.each { |file| require file }
29
36
 
30
37
  # 📦 Instructions (Builders)
31
38
  #
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.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl
@@ -83,7 +83,7 @@ description: A Ruby library for working with Solana blockchain. Provides both lo
83
83
  instruction builders and high-level program clients for interacting with Solana
84
84
  programs.
85
85
  email:
86
- - sebastian@scholl.io
86
+ - sebscholl@gmail.com
87
87
  executables: []
88
88
  extensions: []
89
89
  extra_rdoc_files: []
@@ -93,18 +93,22 @@ files:
93
93
  - README.md
94
94
  - lib/solace.rb
95
95
  - lib/solace/address_lookup_table.rb
96
+ - lib/solace/composers/base.rb
97
+ - lib/solace/composers/spl_token_program_transfer_checked_composer.rb
98
+ - lib/solace/composers/system_program_transfer_composer.rb
96
99
  - lib/solace/concerns/binary_serializable.rb
97
100
  - lib/solace/connection.rb
98
101
  - lib/solace/constants.rb
99
102
  - lib/solace/instruction.rb
100
103
  - lib/solace/instructions/associated_token_account/create_associated_token_account_instruction.rb
104
+ - lib/solace/instructions/base.rb
101
105
  - lib/solace/instructions/spl_token/initialize_account_instruction.rb
102
106
  - lib/solace/instructions/spl_token/initialize_mint_instruction.rb
103
107
  - lib/solace/instructions/spl_token/mint_to_instruction.rb
108
+ - lib/solace/instructions/spl_token/transfer_checked_instruction.rb
104
109
  - lib/solace/instructions/spl_token/transfer_instruction.rb
105
110
  - lib/solace/instructions/system_program/create_account_instruction.rb
106
- - lib/solace/instructions/transfer_checked_instruction.rb
107
- - lib/solace/instructions/transfer_instruction.rb
111
+ - lib/solace/instructions/system_program/transfer_instruction.rb
108
112
  - lib/solace/keypair.rb
109
113
  - lib/solace/message.rb
110
114
  - lib/solace/programs/associated_token_account.rb
@@ -124,6 +128,8 @@ files:
124
128
  - lib/solace/serializers/transaction_deserializer.rb
125
129
  - lib/solace/serializers/transaction_serializer.rb
126
130
  - lib/solace/transaction.rb
131
+ - lib/solace/transaction_composer.rb
132
+ - lib/solace/utils/account_context.rb
127
133
  - lib/solace/utils/codecs.rb
128
134
  - lib/solace/utils/curve25519_dalek.rb
129
135
  - lib/solace/utils/libcurve25519_dalek-linux/libcurve25519_dalek.so
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Solace
4
- module Instructions
5
- # Service object for building an SPL Token Program transfer instruction
6
- class TransferCheckedInstruction
7
- # SPL Token Program instruction index for Transfer Checked
8
- INSTRUCTION_INDEX = [12].freeze
9
-
10
- # Builds a Solace::Instruction for transferring SPL tokens
11
- #
12
- # SPL Token Program transfer instruction layout:
13
- # - 1 byte: instruction index (12 for transfer checked)
14
- # - 8 bytes: amount (u64, little-endian)
15
- # - 8 bytes: decimals (u64, little-endian)
16
- #
17
- # @param amount [Integer] Amount to transfer (in tokens, according to mint's decimals)
18
- # @param decimals [Integer] Number of decimals for the token
19
- # @param to_index [Integer] Index of the destination token account in the transaction's accounts
20
- # @param from_index [Integer] Index of the source token account in the transaction's accounts
21
- # @param mint_index [Integer] Index of the mint in the transaction's accounts
22
- # @param authority_index [Integer] Index of the authority (owner) in the transaction's accounts
23
- # @param program_index [Integer] Index of the SPL Token Program in the transaction's accounts (default: 3)
24
- # @return [Solace::Instruction]
25
- def self.build(
26
- amount:,
27
- decimals:,
28
- to_index:,
29
- from_index:,
30
- mint_index:,
31
- authority_index:,
32
- program_index: 3
33
- )
34
- Solace::Instruction.new.tap do |ix|
35
- ix.program_index = program_index
36
- ix.accounts = [from_index, mint_index, to_index, authority_index]
37
- ix.data = data(amount, decimals)
38
- end
39
- end
40
-
41
- # Instruction data for a token transfer instruction
42
- #
43
- # The BufferLayout is:
44
- # - [Instruction Index (1 byte)]
45
- # - [Amount (8 bytes little-endian u64)]
46
- # - [Decimals (8 bytes little-endian u64)]
47
- #
48
- # @param amount [Integer] Amount to transfer
49
- # @param decimals [Integer] Number of decimals for the token
50
- # @return [Array] 1-byte instruction index + 8-byte amount + decimals
51
- def self.data(amount, decimals)
52
- INSTRUCTION_INDEX +
53
- Solace::Utils::Codecs.encode_le_u64(amount).bytes +
54
- [decimals]
55
- end
56
- end
57
- end
58
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Solace
4
- module Instructions
5
- # Service object for building a System Program transfer instruction
6
- class TransferInstruction
7
- # Instruction ID for System Transfer
8
- INSTRUCTION_ID = [2, 0, 0, 0].freeze
9
-
10
- # Builds a Solace::Instruction for transferring SOL
11
- #
12
- # System Program transfer instruction layout:
13
- # - 4 bytes: instruction index (0 for transfer)
14
- # - 8 bytes: amount (u64, little-endian)
15
- #
16
- # @param lamports [Integer] Amount to transfer (in lamports)
17
- # @param to_index [Integer] Index of the recipient in the transaction's accounts
18
- # @param from_index [Integer] Index of the sender in the transaction's accounts
19
- # @param program_index [Integer] Index of the program in the transaction's accounts (default: 2)
20
- # @return [Solace::Instruction]
21
- def self.build(
22
- lamports:,
23
- to_index:,
24
- from_index:,
25
- program_index: 2
26
- )
27
- Solace::Instruction.new.tap do |ix|
28
- ix.program_index = program_index
29
- ix.accounts = [from_index, to_index]
30
- ix.data = data(lamports)
31
- end
32
- end
33
-
34
- # Instruction data for a transfer instruction
35
- #
36
- # The BufferLayout is:
37
- # - [Instruction ID (4 bytes)]
38
- # - [Amount (8 bytes little-endian u64)]
39
- #
40
- # @param lamports [Integer] Amount to transfer (in lamports)
41
- # @return [Array] 4-byte instruction ID + 8-byte amount
42
- def self.data(lamports)
43
- INSTRUCTION_ID +
44
- Solace::Utils::Codecs.encode_le_u64(lamports).bytes
45
- end
46
- end
47
- end
48
- end