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.
- checksums.yaml +7 -0
- data/CHANGELOG +0 -0
- data/LICENSE +0 -0
- data/README.md +661 -0
- data/lib/solace/address_lookup_table.rb +50 -0
- data/lib/solace/concerns/binary_serializable.rb +30 -0
- data/lib/solace/connection.rb +187 -0
- data/lib/solace/constants.rb +52 -0
- data/lib/solace/instruction.rb +38 -0
- data/lib/solace/instructions/associated_token_account/create_associated_token_account_instruction.rb +68 -0
- data/lib/solace/instructions/spl_token/initialize_account_instruction.rb +46 -0
- data/lib/solace/instructions/spl_token/initialize_mint_instruction.rb +68 -0
- data/lib/solace/instructions/spl_token/mint_to_instruction.rb +48 -0
- data/lib/solace/instructions/spl_token/transfer_instruction.rb +48 -0
- data/lib/solace/instructions/system_program/create_account_instruction.rb +58 -0
- data/lib/solace/instructions/transfer_checked_instruction.rb +58 -0
- data/lib/solace/instructions/transfer_instruction.rb +48 -0
- data/lib/solace/keypair.rb +121 -0
- data/lib/solace/message.rb +95 -0
- data/lib/solace/programs/associated_token_account.rb +96 -0
- data/lib/solace/programs/base.rb +22 -0
- data/lib/solace/programs/spl_token.rb +187 -0
- data/lib/solace/public_key.rb +74 -0
- data/lib/solace/serializable_record.rb +26 -0
- data/lib/solace/serializers/address_lookup_table_deserializer.rb +62 -0
- data/lib/solace/serializers/address_lookup_table_serializer.rb +54 -0
- data/lib/solace/serializers/base.rb +31 -0
- data/lib/solace/serializers/base_deserializer.rb +56 -0
- data/lib/solace/serializers/base_serializer.rb +52 -0
- data/lib/solace/serializers/instruction_deserializer.rb +62 -0
- data/lib/solace/serializers/instruction_serializer.rb +54 -0
- data/lib/solace/serializers/message_deserializer.rb +116 -0
- data/lib/solace/serializers/message_serializer.rb +95 -0
- data/lib/solace/serializers/transaction_deserializer.rb +49 -0
- data/lib/solace/serializers/transaction_serializer.rb +60 -0
- data/lib/solace/transaction.rb +98 -0
- data/lib/solace/utils/codecs.rb +220 -0
- data/lib/solace/utils/curve25519_dalek.rb +59 -0
- data/lib/solace/utils/libcurve25519_dalek-linux/libcurve25519_dalek.so +0 -0
- data/lib/solace/utils/libcurve25519_dalek-macos/libcurve25519_dalek.dylib +0 -0
- data/lib/solace/utils/libcurve25519_dalek-windows/curve25519_dalek.dll +0 -0
- data/lib/solace/utils/pda.rb +100 -0
- data/lib/solace/version.rb +5 -0
- data/lib/solace.rb +39 -0
- 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
|
Binary file
|
Binary file
|
@@ -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
|
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
|
+
|