solace 0.0.2

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +0 -0
  3. data/LICENSE +0 -0
  4. data/README.md +661 -0
  5. data/lib/solace/address_lookup_table.rb +50 -0
  6. data/lib/solace/concerns/binary_serializable.rb +30 -0
  7. data/lib/solace/connection.rb +187 -0
  8. data/lib/solace/constants.rb +52 -0
  9. data/lib/solace/instruction.rb +38 -0
  10. data/lib/solace/instructions/associated_token_account/create_associated_token_account_instruction.rb +68 -0
  11. data/lib/solace/instructions/spl_token/initialize_account_instruction.rb +46 -0
  12. data/lib/solace/instructions/spl_token/initialize_mint_instruction.rb +68 -0
  13. data/lib/solace/instructions/spl_token/mint_to_instruction.rb +48 -0
  14. data/lib/solace/instructions/spl_token/transfer_instruction.rb +48 -0
  15. data/lib/solace/instructions/system_program/create_account_instruction.rb +58 -0
  16. data/lib/solace/instructions/transfer_checked_instruction.rb +58 -0
  17. data/lib/solace/instructions/transfer_instruction.rb +48 -0
  18. data/lib/solace/keypair.rb +121 -0
  19. data/lib/solace/message.rb +95 -0
  20. data/lib/solace/programs/associated_token_account.rb +96 -0
  21. data/lib/solace/programs/base.rb +22 -0
  22. data/lib/solace/programs/spl_token.rb +187 -0
  23. data/lib/solace/public_key.rb +74 -0
  24. data/lib/solace/serializable_record.rb +26 -0
  25. data/lib/solace/serializers/address_lookup_table_deserializer.rb +62 -0
  26. data/lib/solace/serializers/address_lookup_table_serializer.rb +54 -0
  27. data/lib/solace/serializers/base.rb +31 -0
  28. data/lib/solace/serializers/base_deserializer.rb +56 -0
  29. data/lib/solace/serializers/base_serializer.rb +52 -0
  30. data/lib/solace/serializers/instruction_deserializer.rb +62 -0
  31. data/lib/solace/serializers/instruction_serializer.rb +54 -0
  32. data/lib/solace/serializers/message_deserializer.rb +116 -0
  33. data/lib/solace/serializers/message_serializer.rb +95 -0
  34. data/lib/solace/serializers/transaction_deserializer.rb +49 -0
  35. data/lib/solace/serializers/transaction_serializer.rb +60 -0
  36. data/lib/solace/transaction.rb +98 -0
  37. data/lib/solace/utils/codecs.rb +220 -0
  38. data/lib/solace/utils/curve25519_dalek.rb +59 -0
  39. data/lib/solace/utils/libcurve25519_dalek-linux/libcurve25519_dalek.so +0 -0
  40. data/lib/solace/utils/libcurve25519_dalek-macos/libcurve25519_dalek.dylib +0 -0
  41. data/lib/solace/utils/libcurve25519_dalek-windows/curve25519_dalek.dll +0 -0
  42. data/lib/solace/utils/pda.rb +100 -0
  43. data/lib/solace/version.rb +5 -0
  44. data/lib/solace.rb +39 -0
  45. metadata +165 -0
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================
4
+ # Transaction Deserializer
5
+ # =============================
6
+ #
7
+ # Deserializes a binary transaction into a Solace::Transaction object.
8
+ module Solace
9
+ module Serializers
10
+ class TransactionDeserializer < Solace::Serializers::BaseDeserializer
11
+ # @!attribute record_class
12
+ # The class of the record being deserialized
13
+ #
14
+ # @return [Class] The class of the record
15
+ self.record_class = Solace::Transaction
16
+
17
+ # @!attribute steps
18
+ # An ordered list of methods to deserialize the transaction
19
+ #
20
+ # @return [Array] The steps to deserialize the transaction
21
+ self.steps = %i[
22
+ next_extract_signatures
23
+ next_extract_message
24
+ ]
25
+
26
+ # Extract signatures from the transaction
27
+ #
28
+ # The BufferLayout is:
29
+ # - [Number of signatures (compact u16)]
30
+ # - [Signatures (variable length)]
31
+ #
32
+ # @return [Array] Array of base58 encoded signatures
33
+ def next_extract_signatures
34
+ count, = Codecs.decode_compact_u16(io)
35
+ record.signatures = count.times.map { io.read(64) }
36
+ end
37
+
38
+ # Extract the message from the transaction
39
+ #
40
+ # The BufferLayout is:
41
+ # - [Message (variable length)]
42
+ #
43
+ # @return [Solace::Message] The deserialized message instance
44
+ def next_extract_message
45
+ record.message = Solace::Serializers::MessageDeserializer.call(io)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================
4
+ # Transaction Serializer
5
+ # =============================
6
+ #
7
+ # Serializes a Solana transaction to a binary format.
8
+ module Solace
9
+ module Serializers
10
+ class TransactionSerializer < Solace::Serializers::BaseSerializer
11
+ # @!const SIGNATURE_PLACEHOLDER
12
+ # @return [String] Placeholder for a signature in the transaction
13
+ SIGNATURE_PLACEHOLDER = ([0] * 64).pack('C*')
14
+
15
+ # @!attribute steps
16
+ # An ordered list of methods to serialize the transaction
17
+ #
18
+ # @return [Array] The steps to serialize the transaction
19
+ self.steps = %i[
20
+ encode_signatures
21
+ encode_message
22
+ ]
23
+
24
+ # Encodes the signatures of the transaction
25
+ #
26
+ # Iterates over the number sum number of signatures and either encodes or sets
27
+ # the placeholder for each expected index in the signatures array.
28
+ #
29
+ # The BufferLayout is:
30
+ # - [Number of signatures (compact u16)]
31
+ # - [Signatures (variable length)]
32
+ #
33
+ # @return [Array<Integer>] The bytes of the encoded signatures
34
+ def encode_signatures
35
+ Codecs.encode_compact_u16(record.signatures.size).bytes +
36
+ index_signatures(record.message.num_required_signatures)
37
+ end
38
+
39
+ # Encodes the message from the transaction
40
+ #
41
+ # @return [Array<Integer>] The bytes of the encoded message
42
+ def encode_message
43
+ record.message.to_bytes
44
+ end
45
+
46
+ private
47
+
48
+ # Index the signatures
49
+ #
50
+ # Positions the signatures by expected index and set placeholders for any missing signatures.
51
+ #
52
+ # @param num_required_signatures [Integer] The number of required signatures
53
+ #
54
+ # @return [Array<Integer>] The bytes of the encoded signatures
55
+ def index_signatures(num_required_signatures)
56
+ (0...num_required_signatures).map { (record.signatures[_1] || SIGNATURE_PLACEHOLDER).bytes }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ # =============================
5
+ # Transaction
6
+ # =============================
7
+ #
8
+ # Class representing a Solana transaction.
9
+ #
10
+ # The BufferLayout is:
11
+ # - [Signatures (variable length)]
12
+ # - [Version (1 byte)] (if versioned)
13
+ # - [Message header (3 bytes)]
14
+ # - [Account keys (variable length)]
15
+ # - [Recent blockhash (32 bytes)]
16
+ # - [Instructions (variable length)]
17
+ # - [Address lookup table (variable length)] (if versioned)
18
+ #
19
+ module Solace
20
+ class Transaction < Solace::SerializableRecord
21
+ # @!const SERIALIZER
22
+ # @return [Solace::Serializers::TransactionSerializer] The serializer for the transaction
23
+ SERIALIZER = Solace::Serializers::TransactionSerializer
24
+
25
+ # @!const DESERIALIZER
26
+ # @return [Solace::Serializers::TransactionDeserializer] The deserializer for the transaction
27
+ DESERIALIZER = Solace::Serializers::TransactionDeserializer
28
+
29
+ # @!const SIGNATURE_PLACEHOLDER
30
+ # @return [String] Placeholder for a signature in the transaction
31
+ SIGNATURE_PLACEHOLDER = Solace::Utils::Codecs.base58_to_binary('1' * 64)
32
+
33
+ # @!attribute [rw] signatures
34
+ # @return [Array<String>] Signatures of the transaction (binary)
35
+ attr_accessor :signatures
36
+
37
+ # @!attribute [rw] message
38
+ # @return [Solace::Message] Message of the transaction
39
+ attr_accessor :message
40
+
41
+ class << self
42
+ # Deserialize a base64 encoded transaction into a Solace::Transaction object
43
+ #
44
+ # @param base64_tx [String] The base64 encoded transaction
45
+ # @return [Solace::Transaction] The deserialized transaction
46
+ def from(base64_tx)
47
+ DESERIALIZER.call Solace::Utils::Codecs.base64_to_bytestream(base64_tx)
48
+ end
49
+ end
50
+
51
+ # Initialize a new transaction
52
+ #
53
+ # @return [Solace::Transaction] The new transaction object
54
+ def initialize(
55
+ signatures: [],
56
+ message: Solace::Message.new
57
+ )
58
+ @signatures = signatures
59
+ @message = message
60
+ end
61
+
62
+ # Sign the transaction
63
+ #
64
+ # Calls sign_and_update_signatures for each keypair passed in.
65
+ #
66
+ # @param keypairs [<Solace::Keypair>] The keypairs to sign the transaction with
67
+ # @return [Array<String>] The signatures of the transaction
68
+ def sign(*keypairs)
69
+ keypairs.map { |keypair| sign_and_update_signatures(keypair) }
70
+ end
71
+
72
+ private
73
+
74
+ # Sign message and update signatures
75
+ #
76
+ # Signs the transaction's message and updates the signatures array with the
77
+ # signature.
78
+ #
79
+ # @return [Array<String>] The signatures of the transaction
80
+ def sign_and_update_signatures(keypair)
81
+ keypair.sign(message.to_binary).tap { |signature| set_signature(keypair.address, signature) }
82
+ end
83
+
84
+ # Update the transaction signatures
85
+ #
86
+ # Updates the signatures array according to the accounts of the message.
87
+ #
88
+ # @param public_key [String] The public key of the signer
89
+ # @param signature [String] The signature to insert
90
+ def set_signature(public_key, signature)
91
+ index = message.accounts.index(public_key)
92
+
93
+ raise ArgumentError, 'Public key not found in transaction' if index.nil?
94
+
95
+ signatures[index] = signature
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'rbnacl'
5
+ require 'base58'
6
+ require 'stringio'
7
+
8
+ module Solace
9
+ module Utils
10
+ module Codecs
11
+ # =============================
12
+ # Helper: IO Stream
13
+ # =============================
14
+ #
15
+ # Creates a StringIO from a base64 string.
16
+ #
17
+ # Args:
18
+ # base64 (String): The base64 string to decode
19
+ #
20
+ # Returns:
21
+ # StringIO: A StringIO object containing the decoded bytes
22
+ #
23
+ def self.base64_to_bytestream(base64)
24
+ StringIO.new(Base64.decode64(base64))
25
+ end
26
+
27
+ # =============================
28
+ # Helper: Compact-u16 Encoding (ShortVec)
29
+ # =============================
30
+ #
31
+ # Encodes a u16 value in a compact form
32
+ #
33
+ # Args:
34
+ # n (Integer): The u16 value to encode
35
+ #
36
+ # Returns:
37
+ # String: The compactly encoded u16 value
38
+ #
39
+ def self.encode_compact_u16(n)
40
+ out = []
41
+ loop do
42
+ # In general, n >> 7 shifts the bits of n to the right by
43
+ # 7 positions, effectively dividing n by 128 and discarding
44
+ # the remainder (integer division). This is commonly used in
45
+ # encoding schemes to process one "byte" (7 bits) at a time.
46
+ if (n >> 7).zero?
47
+ out << n
48
+ break
49
+ else
50
+ # The expression out << ((n & 0x7F) | 0x80) is used in variable-length
51
+ # integer encoding, such as the compact-u16 encoding.
52
+ #
53
+ # n & 0x7F:
54
+ # - 0x7F is 127 in decimal, or 0111 1111 in binary.
55
+ # - n & 0x7F masks out all but the lowest 7 bits of n. This extracts the least significant 7 bits of n.
56
+ #
57
+ # (n & 0x7F) | 0x80:
58
+ # - 0x80 is 128 in decimal, or 1000 0000 in binary.
59
+ # - | (bitwise OR) sets the highest bit (the 8th bit) to 1.
60
+ # - This is a signal that there are more bytes to come in the encoding (i.e., the value hasn't been fully encoded yet).
61
+ #
62
+ # out << ...:
63
+ # - This appends the resulting byte to the out array.
64
+ out << ((n & 0x7F) | 0x80)
65
+ n >>= 7
66
+ end
67
+ end
68
+ out.pack('C*')
69
+ end
70
+
71
+ # =============================
72
+ # Helper: Compact-u16 Decoding (ShortVec)
73
+ # =============================
74
+ #
75
+ # Decodes a compact-u16 (ShortVec) value from an IO-like object.
76
+ # Reads bytes one at a time, accumulating the result until the MSB is 0.
77
+ #
78
+ # Args:
79
+ # io (IO or StringIO): The input to read bytes from.
80
+ #
81
+ # Returns:
82
+ # [Integer, Integer]: The decoded value and the number of bytes read.
83
+ #
84
+ def self.decode_compact_u16(io)
85
+ value = 0
86
+ shift = 0
87
+ bytes_read = 0
88
+ loop do
89
+ byte = io.read(1)
90
+ raise EOFError, 'Unexpected end of input while decoding compact-u16' unless byte
91
+
92
+ byte = byte.ord
93
+ value |= (byte & 0x7F) << shift
94
+ bytes_read += 1
95
+ break if (byte & 0x80).zero?
96
+
97
+ shift += 7
98
+ end
99
+ [value, bytes_read]
100
+ end
101
+
102
+ # =============================
103
+ # Helper: Little-Endian u64 Encoding
104
+ # =============================
105
+ #
106
+ # Encodes a u64 value in little-endian format
107
+ #
108
+ # Args:
109
+ # n (Integer): The u64 value to encode
110
+ #
111
+ # Returns:
112
+ # String: The little-endian encoded u64 value
113
+ #
114
+ def self.encode_le_u64(n)
115
+ [n].pack('Q<') # 64-bit little-endian
116
+ end
117
+
118
+ # =============================
119
+ # Helper: Little-Endian u64 Decoding
120
+ # =============================
121
+ #
122
+ # Decodes a little-endian u64 value from a sequence of bytes
123
+ #
124
+ # Args:
125
+ # io (IO or StringIO): The input to read bytes from.
126
+ #
127
+ # Returns:
128
+ # Integer: The decoded u64 value
129
+ #
130
+ def self.decode_le_u64(io)
131
+ io.read(8).unpack1('Q<')
132
+ end
133
+
134
+ # =============================
135
+ # Helper: Binary to Base58 Encoding
136
+ # =============================
137
+ #
138
+ # Encodes a sequence of bytes in Base58 format
139
+ #
140
+ # Args:
141
+ # bytes (String): The bytes to encode
142
+ #
143
+ # Returns:
144
+ # String: The Base58 encoded string
145
+ #
146
+ def self.binary_to_base58(binary)
147
+ Base58.binary_to_base58(binary, :bitcoin)
148
+ end
149
+
150
+ # =============================
151
+ # Helper: Base58 Decoding
152
+ # =============================
153
+ #
154
+ # Decodes a Base58 string into a binary string
155
+ #
156
+ # Args:
157
+ # string (String): The Base58 encoded string
158
+ #
159
+ # Returns:
160
+ # String: The decoded binary string
161
+ #
162
+ def self.base58_to_binary(string)
163
+ base58_to_bytes(string).pack('C*')
164
+ end
165
+
166
+ # =============================
167
+ # Helper: Base58 Encoding
168
+ # =============================
169
+ #
170
+ # Encodes a sequence of bytes in Base58 format
171
+ #
172
+ # Args:
173
+ # bytes (String): The bytes to encode
174
+ #
175
+ # Returns:
176
+ # String: The Base58 encoded string
177
+ #
178
+ def self.bytes_to_base58(bytes)
179
+ binary_to_base58(bytes.pack('C*'))
180
+ end
181
+
182
+ # =============================
183
+ # Helper: Base58 Decoding
184
+ # =============================
185
+ #
186
+ # Decodes a Base58 string into a sequence of bytes
187
+ #
188
+ # Args:
189
+ # string (String): The Base58 encoded string
190
+ #
191
+ # Returns:
192
+ # String: The decoded bytes
193
+ #
194
+ def self.base58_to_bytes(string)
195
+ Base58.base58_to_binary(string, :bitcoin).bytes
196
+ end
197
+
198
+ # =============================
199
+ # Helper: Base58 Validation
200
+ # =============================
201
+ #
202
+ # Checks if a string is a valid Base58 string
203
+ #
204
+ # Args:
205
+ # string (String): The string to check
206
+ #
207
+ # Returns:
208
+ # Boolean: True if the string is a valid Base58 string, false otherwise
209
+ #
210
+ def self.valid_base58?(string)
211
+ return false if string.nil? || string.empty?
212
+
213
+ Base58.decode(string)
214
+ true
215
+ rescue StandardError => _e
216
+ false
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,59 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ require 'ffi'
5
+
6
+ module Solace
7
+ module Utils
8
+ module Curve25519Dalek
9
+ extend FFI::Library
10
+
11
+ # Load the native library
12
+ #
13
+ # If the platform is not supported, a RuntimeError is raised. The native library
14
+ # can be built by compiling the Rust code in the root/ext directory.
15
+ #
16
+ # @return [String] The path to the native library
17
+ # @raise [RuntimeError] If the platform is not supported
18
+ libfile = case RUBY_PLATFORM
19
+ when /linux/ then 'libcurve25519_dalek-linux/libcurve25519_dalek.so'
20
+ when /darwin/ then 'libcurve25519_dalek-macos/libcurve25519_dalek.dylib'
21
+ when /mingw|mswin/ then 'libcurve25519_dalek-windows/curve25519_dalek.dll'
22
+ else raise 'Unsupported platform'
23
+ end
24
+
25
+ # The path to the native library
26
+ #
27
+ # @return [String] The path to the native library
28
+ LIB_PATH = File.expand_path(libfile, __dir__)
29
+
30
+ # Load the native library
31
+ ffi_lib LIB_PATH
32
+
33
+ # Attach the native function
34
+ #
35
+ # @return [FFI::Function] The native function
36
+ attach_function :is_on_curve, [:pointer], :int
37
+
38
+ # Checks if a point is on the curve
39
+ #
40
+ # @param bytes [Array] The bytes to check
41
+ # @return [Boolean] True if the point is on the curve, false otherwise
42
+ # @raise [ArgumentError] If the input is not 32 bytes
43
+ def self.on_curve?(bytes)
44
+ raise ArgumentError, 'Must be 32 bytes' unless bytes.bytesize == 32
45
+
46
+ FFI::MemoryPointer.new(:uchar, 32) do |ptr|
47
+ ptr.put_bytes(0, bytes)
48
+ result = Curve25519Dalek.is_on_curve(ptr)
49
+
50
+ case result
51
+ when 1 then return true
52
+ when 0 then return false
53
+ else raise "Unexpected return code from native is_on_curve: #{result}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,100 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ require 'digest'
5
+
6
+ module Solace
7
+ module Utils
8
+ module PDA
9
+ # !@class InvalidPDAError
10
+ # An error raised when an invalid PDA is generated
11
+ #
12
+ # @return [StandardError]
13
+ class InvalidPDAError < StandardError; end
14
+
15
+ # !@const PDA_MARKER
16
+ # The marker used in PDA calculations
17
+ #
18
+ # @return [String]
19
+ PDA_MARKER = 'ProgramDerivedAddress'
20
+
21
+ # !@const MAX_BUMP_SEED
22
+ # The maximum seed value for PDA calculations
23
+ #
24
+ # @return [Integer]
25
+ MAX_BUMP_SEED = 255
26
+
27
+ # Finds a valid program address by trying different seeds
28
+ #
29
+ # @param seeds [Array] The seeds to use in the calculation
30
+ # @param program_id [String] The program ID to use in the calculation
31
+ # @return [Array] The program address and bump seed
32
+ # @raise [InvalidPDAError] If no valid program address is found
33
+ def self.find_program_address(seeds, program_id)
34
+ MAX_BUMP_SEED.downto(0) do |bump|
35
+ address = create_program_address(seeds + [bump], program_id)
36
+ return [address, bump]
37
+ rescue InvalidPDAError
38
+ next
39
+ end
40
+
41
+ raise 'Unable to find a valid program address'
42
+ end
43
+
44
+ # Creates a program address from seeds and program ID
45
+ #
46
+ # @param seeds [Array] The seeds to use in the calculation
47
+ # @param program_id [String] The program ID to use in the calculation
48
+ # @return [String] The program address
49
+ # @raise [InvalidPDAError] If the program address is invalid
50
+ def self.create_program_address(seeds, program_id)
51
+ seed_bytes = seeds.map { |seed| seed_to_bytes(seed) }.flatten
52
+
53
+ program_id_bytes = Solace::Utils::Codecs.base58_to_bytes(program_id)
54
+
55
+ combined = seed_bytes + program_id_bytes + PDA_MARKER.bytes
56
+
57
+ hash_bin = Digest::SHA256.digest(combined.pack('C*'))
58
+
59
+ raise InvalidPDAError if Solace::Utils::Curve25519Dalek.on_curve?(hash_bin)
60
+
61
+ Solace::Utils::Codecs.bytes_to_base58(hash_bin.bytes)
62
+ end
63
+
64
+ # Prepares a list of seeds for creating a program address
65
+ #
66
+ # @param seed [String, Integer, Array] The seed to prepare
67
+ # @return [Array] The prepared seeds
68
+ def self.seed_to_bytes(seed)
69
+ case seed
70
+ when String
71
+ if looks_like_base58_address?(seed)
72
+ Solace::Utils::Codecs.base58_to_bytes(seed)
73
+ else
74
+ seed.bytes
75
+ end
76
+ when Integer
77
+ if seed.between?(0, 255)
78
+ [seed]
79
+ else
80
+ seed.digits(256)
81
+ end
82
+ when Array
83
+ seed
84
+ else
85
+ seed.to_s.bytes
86
+ end
87
+ end
88
+
89
+ # Checks if a string looks like a base58 address
90
+ #
91
+ # @param string [String] The string to check
92
+ # @return [Boolean] True if the string looks like a base58 address, false otherwise
93
+ def self.looks_like_base58_address?(string)
94
+ string.length >= 32 &&
95
+ string.length <= 44 &&
96
+ Solace::Utils::Codecs.valid_base58?(string)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solace
4
+ VERSION = '0.0.2'
5
+ end
data/lib/solace.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 🏷️ Version
4
+ require_relative 'solace/version'
5
+
6
+ # 🛠️ Helpers
7
+ require_relative 'solace/constants'
8
+ require_relative 'solace/connection'
9
+ require_relative 'solace/utils/codecs'
10
+ require_relative 'solace/utils/pda'
11
+ require_relative 'solace/utils/curve25519_dalek'
12
+ require_relative 'solace/concerns/binary_serializable'
13
+
14
+ # ✨ Serializers
15
+ require_relative 'solace/serializers/base'
16
+ require_relative 'solace/serializers/base_serializer'
17
+ require_relative 'solace/serializers/base_deserializer'
18
+
19
+ # Base classes
20
+ require_relative 'solace/serializable_record'
21
+
22
+ # 🧬 Primitives
23
+ require_relative 'solace/keypair'
24
+ require_relative 'solace/public_key'
25
+ require_relative 'solace/transaction'
26
+ require_relative 'solace/message'
27
+ require_relative 'solace/instruction'
28
+ require_relative 'solace/address_lookup_table'
29
+
30
+ # 📦 Instructions (Builders)
31
+ #
32
+ # Glob require all instructions
33
+ Dir[File.join(__dir__, 'solace/instructions', '**', '*.rb')].sort.each { |file| require file }
34
+
35
+ # 📦 Programs
36
+ require_relative 'solace/programs/base'
37
+ require_relative 'solace/programs/spl_token'
38
+ require_relative 'solace/programs/associated_token_account'
39
+