solace 0.0.2 → 0.0.5
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 +4 -4
- data/CHANGELOG +57 -0
- data/LICENSE +21 -0
- data/README.md +142 -287
- data/lib/solace/address_lookup_table.rb +34 -18
- data/lib/solace/composers/base.rb +45 -0
- data/lib/solace/composers/spl_token_program_transfer_checked_composer.rb +113 -0
- data/lib/solace/composers/system_program_transfer_composer.rb +80 -0
- data/lib/solace/concerns/binary_serializable.rb +39 -0
- data/lib/solace/connection.rb +101 -44
- data/lib/solace/constants.rb +7 -14
- data/lib/solace/instruction.rb +30 -19
- data/lib/solace/instructions/associated_token_account/create_associated_token_account_instruction.rb +18 -3
- data/lib/solace/instructions/spl_token/initialize_account_instruction.rb +24 -3
- data/lib/solace/instructions/spl_token/initialize_mint_instruction.rb +18 -1
- data/lib/solace/instructions/spl_token/mint_to_instruction.rb +16 -3
- data/lib/solace/instructions/spl_token/transfer_checked_instruction.rb +76 -0
- data/lib/solace/instructions/spl_token/transfer_instruction.rb +15 -2
- data/lib/solace/instructions/system_program/create_account_instruction.rb +18 -3
- data/lib/solace/instructions/system_program/transfer_instruction.rb +58 -0
- data/lib/solace/keypair.rb +64 -31
- data/lib/solace/message.rb +22 -10
- data/lib/solace/programs/associated_token_account.rb +58 -11
- data/lib/solace/programs/base.rb +6 -0
- data/lib/solace/programs/spl_token.rb +52 -14
- data/lib/solace/public_key.rb +45 -20
- data/lib/solace/serializers/address_lookup_table_deserializer.rb +3 -5
- data/lib/solace/serializers/address_lookup_table_serializer.rb +7 -7
- data/lib/solace/serializers/base_deserializer.rb +29 -19
- data/lib/solace/serializers/base_serializer.rb +18 -9
- data/lib/solace/serializers/instruction_deserializer.rb +5 -7
- data/lib/solace/serializers/instruction_serializer.rb +4 -6
- data/lib/solace/serializers/message_deserializer.rb +3 -5
- data/lib/solace/serializers/message_serializer.rb +3 -5
- data/lib/solace/serializers/transaction_deserializer.rb +5 -7
- data/lib/solace/serializers/transaction_serializer.rb +5 -7
- data/lib/solace/transaction.rb +38 -23
- data/lib/solace/transaction_composer.rb +115 -0
- data/lib/solace/utils/account_context.rb +252 -0
- data/lib/solace/utils/codecs.rb +56 -128
- data/lib/solace/utils/curve25519_dalek.rb +9 -4
- data/lib/solace/utils/pda.rb +22 -24
- data/lib/solace/version.rb +2 -1
- data/lib/solace.rb +9 -7
- metadata +15 -12
- data/lib/solace/instructions/transfer_checked_instruction.rb +0 -58
- data/lib/solace/instructions/transfer_instruction.rb +0 -48
- data/lib/solace/serializable_record.rb +0 -26
- data/lib/solace/serializers/base.rb +0 -31
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/solace/transaction_composer.rb
|
4
|
+
module Solace
|
5
|
+
# Composes multi-instruction transactions with automatic account management
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# # Initialize a transaction composer
|
9
|
+
# composer = Solace::TransactionComposer.new(connection: connection)
|
10
|
+
#
|
11
|
+
# # Add an instruction composer
|
12
|
+
# composer.add_instruction(
|
13
|
+
# Solace::Composers::SystemProgramTransferComposer.new(
|
14
|
+
# to: 'pubkey1',
|
15
|
+
# from: 'pubkey2',
|
16
|
+
# lamports: 100
|
17
|
+
# )
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# # Add another instruction composer
|
21
|
+
# composer.add_instruction(
|
22
|
+
# Solace::Composers::SplTokenProgramTransferCheckedComposer.new(
|
23
|
+
# from: 'pubkey4',
|
24
|
+
# to: 'pubkey5',
|
25
|
+
# mint: 'pubkey6',
|
26
|
+
# authority: 'pubkey7',
|
27
|
+
# amount: 1_000_000,
|
28
|
+
# decimals: 6
|
29
|
+
# )
|
30
|
+
# )
|
31
|
+
#
|
32
|
+
# # Set the fee payer
|
33
|
+
# composer.set_fee_payer('pubkey8')
|
34
|
+
#
|
35
|
+
# # Compose the transaction
|
36
|
+
# tx = composer.compose_transaction
|
37
|
+
#
|
38
|
+
# # Sign the transaction with all required signers
|
39
|
+
# tx.sign(*required_signers)
|
40
|
+
#
|
41
|
+
# @since 0.0.1
|
42
|
+
class TransactionComposer
|
43
|
+
# @!attribute connection
|
44
|
+
# The connection to the Solana cluster
|
45
|
+
attr_reader :connection
|
46
|
+
|
47
|
+
# @!attribute context
|
48
|
+
# The account context
|
49
|
+
attr_reader :context
|
50
|
+
|
51
|
+
# @!attribute instruction_composers
|
52
|
+
# The instruction composers
|
53
|
+
attr_reader :instruction_composers
|
54
|
+
|
55
|
+
# Initialize the composer
|
56
|
+
#
|
57
|
+
# @param connection [Solace::Connection] The connection to the Solana cluster
|
58
|
+
def initialize(connection:)
|
59
|
+
@connection = connection
|
60
|
+
@instruction_composers = []
|
61
|
+
@context = Utils::AccountContext.new
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add an instruction composer to the transaction
|
65
|
+
#
|
66
|
+
# @param composer [Composers::Base] The instruction composer
|
67
|
+
# @return [TransactionComposer] Self for chaining
|
68
|
+
def add_instruction(composer)
|
69
|
+
merge_accounts(composer.account_context)
|
70
|
+
instruction_composers << composer
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
# Set the fee payer for the transaction
|
75
|
+
#
|
76
|
+
# @param pubkey [String, Solace::PublicKey, Solace::Keypair] The fee payer pubkey
|
77
|
+
# @return [TransactionComposer] Self for chaining
|
78
|
+
def set_fee_payer(pubkey)
|
79
|
+
context.set_fee_payer(pubkey)
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Compose the final transaction
|
84
|
+
#
|
85
|
+
# @return [Solace::Transaction] The composed transaction (unsigned)
|
86
|
+
def compose_transaction
|
87
|
+
context.compile
|
88
|
+
|
89
|
+
message = Solace::Message.new(
|
90
|
+
header: context.header,
|
91
|
+
accounts: context.accounts,
|
92
|
+
instructions: build_instructions,
|
93
|
+
recent_blockhash: connection.get_latest_blockhash
|
94
|
+
)
|
95
|
+
|
96
|
+
Solace::Transaction.new(message: message)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Build all instructions with resolved indices
|
102
|
+
#
|
103
|
+
# @return [Array<Solace::Instruction>] The built instructions
|
104
|
+
def build_instructions
|
105
|
+
instruction_composers.map { _1.build_instruction(context) }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Merge all accounts from another AccountContext into this one
|
109
|
+
#
|
110
|
+
# @param account_context [AccountContext] The other context to merge from
|
111
|
+
def merge_accounts(account_context)
|
112
|
+
context.merge_from(account_context)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solace
|
4
|
+
module Utils
|
5
|
+
# Utility for managing account context for composers
|
6
|
+
#
|
7
|
+
# This utility is used to manage the accounts in a transaction composer and instructions composer. It
|
8
|
+
# provides methods for managing the accounts and their permissions, as well as compiling the accounts
|
9
|
+
# into the final format required by the instruction builders. Concerns like deduplication and ordering
|
10
|
+
# are handled by this utility.
|
11
|
+
#
|
12
|
+
# @example Usage
|
13
|
+
# # Create a new account context
|
14
|
+
# context = Solace::Utils::AccountContext.new
|
15
|
+
#
|
16
|
+
# # Add accounts
|
17
|
+
# context.add_writable_signer('pubkey1')
|
18
|
+
# context.add_readonly_nonsigner('pubkey2')
|
19
|
+
#
|
20
|
+
# # Merge accounts from another context
|
21
|
+
# context = context.merge_from(other_context)
|
22
|
+
#
|
23
|
+
# # Set fee payer
|
24
|
+
# context.set_fee_payer('pubkey3')
|
25
|
+
#
|
26
|
+
# # Compile the accounts
|
27
|
+
# context.compile
|
28
|
+
#
|
29
|
+
# @see Solace::TransactionComposer
|
30
|
+
# @see Solace::Composers::Base
|
31
|
+
# @since 0.0.3
|
32
|
+
class AccountContext
|
33
|
+
# @!attribute DEFAULT_ACCOUNT
|
34
|
+
# The default account data
|
35
|
+
#
|
36
|
+
# @return [Hash] The default account data with lowest level of permissions
|
37
|
+
DEFAULT_ACCOUNT = {
|
38
|
+
signer: false,
|
39
|
+
writable: false,
|
40
|
+
fee_payer: false
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
# @!attribute header
|
44
|
+
# The header for the transaction
|
45
|
+
#
|
46
|
+
# @return [Array<Integer>] The header for the transaction
|
47
|
+
attr_accessor :header
|
48
|
+
|
49
|
+
# @!attribute accounts
|
50
|
+
# The accounts in the transaction
|
51
|
+
#
|
52
|
+
# @return [Array<String>] The accounts
|
53
|
+
attr_accessor :accounts
|
54
|
+
|
55
|
+
# @!attribute pubkey_account_map
|
56
|
+
# The map of accounts
|
57
|
+
#
|
58
|
+
# @return [Hash] The map of accounts
|
59
|
+
attr_accessor :pubkey_account_map
|
60
|
+
|
61
|
+
# Initialize the account context
|
62
|
+
def initialize
|
63
|
+
@header = []
|
64
|
+
@accounts = []
|
65
|
+
@pubkey_account_map = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Set the fee payer account
|
69
|
+
#
|
70
|
+
# @param pubkey [Solace::Keypair, Solace::PublicKey, String] The pubkey of the fee payer account
|
71
|
+
def set_fee_payer(pubkey)
|
72
|
+
merge_account(pubkey, signer: true, writable: true, fee_payer: true)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Add a signer account
|
76
|
+
#
|
77
|
+
# @param pubkey [Solace::Keypair, Solace::PublicKey, String] The pubkey of the signer account
|
78
|
+
def add_writable_signer(pubkey)
|
79
|
+
merge_account(pubkey, signer: true, writable: true)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add a writable account
|
83
|
+
#
|
84
|
+
# @param pubkey [Solace::Keypair, Solace::PublicKey, String] The pubkey of the writable account
|
85
|
+
def add_writable_nonsigner(pubkey)
|
86
|
+
merge_account(pubkey, signer: false, writable: true)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Add a readonly signer account
|
90
|
+
#
|
91
|
+
# @param pubkey [Solace::Keypair, Solace::PublicKey, String] The pubkey of the readonly signer account
|
92
|
+
def add_readonly_signer(pubkey)
|
93
|
+
merge_account(pubkey, signer: true, writable: false)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Add a readonly account
|
97
|
+
#
|
98
|
+
# @param pubkey [Solace::Keypair, Solace::PublicKey, String] The pubkey of the readonly account
|
99
|
+
def add_readonly_nonsigner(pubkey)
|
100
|
+
merge_account(pubkey, signer: false, writable: false)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Predicate to check if an account is a fee payer
|
104
|
+
#
|
105
|
+
# @param pubkey [String] The pubkey of the account
|
106
|
+
# @return [Boolean] Whether the account is a fee payer
|
107
|
+
def fee_payer?(pubkey)
|
108
|
+
@pubkey_account_map[pubkey].try { |acc| acc[:fee_payer] }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Predicate to check if an account is a signer
|
112
|
+
#
|
113
|
+
# @param pubkey [String] The pubkey of the account
|
114
|
+
# @return [Boolean] Whether the account is a signer
|
115
|
+
def signer?(pubkey)
|
116
|
+
@pubkey_account_map[pubkey].try { |acc| acc[:signer] }
|
117
|
+
end
|
118
|
+
|
119
|
+
# Predicate to check if an account is writable
|
120
|
+
#
|
121
|
+
# @param pubkey [String] The pubkey of the account
|
122
|
+
# @return [Boolean] Whether the account is writable
|
123
|
+
def writable?(pubkey)
|
124
|
+
@pubkey_account_map[pubkey].try { |acc| acc[:writable] }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Predicate to check if an account is a writable signer
|
128
|
+
#
|
129
|
+
# @param pubkey [String] The pubkey of the account
|
130
|
+
# @return [Boolean] Whether the account is a writable signer
|
131
|
+
def writable_signer?(pubkey)
|
132
|
+
@pubkey_account_map[pubkey].try { |acc| acc[:signer] && acc[:writable] }
|
133
|
+
end
|
134
|
+
|
135
|
+
# Predicate to check if an account is writable and not a signer
|
136
|
+
#
|
137
|
+
# @param pubkey [String] The pubkey of the account
|
138
|
+
# @return [Boolean] Whether the account is writable and not a signer
|
139
|
+
def writable_nonsigner?(pubkey)
|
140
|
+
@pubkey_account_map[pubkey].try { |acc| !acc[:signer] && acc[:writable] }
|
141
|
+
end
|
142
|
+
|
143
|
+
# Predicate to check if an account is a readonly signer
|
144
|
+
#
|
145
|
+
# @param pubkey [String] The pubkey of the account
|
146
|
+
# @return [Boolean] Whether the account is a readonly signer
|
147
|
+
def readonly_signer?(pubkey)
|
148
|
+
@pubkey_account_map[pubkey].try { |acc| acc[:signer] && !acc[:writable] }
|
149
|
+
end
|
150
|
+
|
151
|
+
# Predicate to check if an account is readonly and not a signer
|
152
|
+
#
|
153
|
+
# @param pubkey [String] The pubkey of the account
|
154
|
+
# @return [Boolean] Whether the account is readonly and not a signer
|
155
|
+
def readonly_nonsigner?(pubkey)
|
156
|
+
@pubkey_account_map[pubkey].try { |acc| !acc[:signer] && !acc[:writable] }
|
157
|
+
end
|
158
|
+
|
159
|
+
# Merge all accounts from another AccountContext into this one
|
160
|
+
#
|
161
|
+
# @param other_context [AccountContext] The other context to merge from
|
162
|
+
def merge_from(other_context)
|
163
|
+
other_context.pubkey_account_map.each do |pubkey, data|
|
164
|
+
signer, writable, fee_payer = data.values_at(:signer, :writable, :fee_payer)
|
165
|
+
merge_account(pubkey, signer: signer, writable: writable, fee_payer: fee_payer)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Compile accounts into final format
|
170
|
+
#
|
171
|
+
# Gets unique accounts and sorts them in the following order:
|
172
|
+
# - Signers first (Solana requirement)
|
173
|
+
# - Then writable accounts
|
174
|
+
# - Then readonly accounts
|
175
|
+
#
|
176
|
+
# @return [Hash] The compiled accounts and header
|
177
|
+
def compile
|
178
|
+
self.header = calculate_header
|
179
|
+
self.accounts = order_accounts
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
# Index of a pubkey in the accounts array
|
184
|
+
#
|
185
|
+
# @param pubkey_str [String] The public key of the account
|
186
|
+
# @return [Integer] The index of the pubkey in the accounts array or -1 if not found
|
187
|
+
def index_of(pubkey_str)
|
188
|
+
indices[pubkey_str] || -1
|
189
|
+
end
|
190
|
+
|
191
|
+
# Get map of indicies for pubkeys in accounts array
|
192
|
+
#
|
193
|
+
# @return [Hash{String => Integer}] The indices of the pubkeys in the accounts array
|
194
|
+
def indices
|
195
|
+
accounts.each_with_index.to_h
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
# Add or merge an account into the context
|
201
|
+
#
|
202
|
+
# @param pubkey [String, Solace::PublicKey, Solace::Keypair] The public key of the account
|
203
|
+
# @param signer [Boolean] Whether the account is a signer
|
204
|
+
# @param writable [Boolean] Whether the account is writable
|
205
|
+
# @param [Boolean] fee_payer
|
206
|
+
def merge_account(pubkey, signer:, writable:, fee_payer: false)
|
207
|
+
pubkey_str = pubkey.is_a?(String) ? pubkey : pubkey.address
|
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
|
212
|
+
@pubkey_account_map[pubkey_str][:fee_payer] ||= fee_payer
|
213
|
+
|
214
|
+
self
|
215
|
+
end
|
216
|
+
|
217
|
+
# Order accounts by signer, writable, readonly signer, readonly
|
218
|
+
#
|
219
|
+
# @return [Array<String>] The ordered accounts
|
220
|
+
def order_accounts
|
221
|
+
@pubkey_account_map.keys.sort_by do |pubkey|
|
222
|
+
if fee_payer?(pubkey) then 0
|
223
|
+
elsif writable_signer?(pubkey) then 1
|
224
|
+
elsif readonly_signer?(pubkey) then 2
|
225
|
+
elsif writable_nonsigner?(pubkey) then 3
|
226
|
+
elsif readonly_nonsigner?(pubkey) then 4
|
227
|
+
else
|
228
|
+
raise ArgumentError, "Unknown account type for pubkey: #{pubkey}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Calculate the header for the transaction
|
234
|
+
#
|
235
|
+
# @note The header is an array of three integers:
|
236
|
+
# - The number of signers (writable + readonly)
|
237
|
+
# - The number of readonly signers
|
238
|
+
# - The number of readonly unsigned accounts
|
239
|
+
#
|
240
|
+
# @return [Array] The header for the transaction
|
241
|
+
def calculate_header
|
242
|
+
@pubkey_account_map.keys.each_with_object([0, 0, 0]) do |pubkey, acc|
|
243
|
+
acc[0] += 1 if signer?(pubkey)
|
244
|
+
|
245
|
+
if readonly_signer?(pubkey) then acc[1] += 1
|
246
|
+
elsif readonly_nonsigner?(pubkey) then acc[2] += 1
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
data/lib/solace/utils/codecs.rb
CHANGED
@@ -7,206 +7,134 @@ require 'stringio'
|
|
7
7
|
|
8
8
|
module Solace
|
9
9
|
module Utils
|
10
|
+
# Module for encoding and decoding data
|
11
|
+
#
|
12
|
+
# @since 0.0.1
|
10
13
|
module Codecs
|
11
|
-
# =============================
|
12
|
-
# Helper: IO Stream
|
13
|
-
# =============================
|
14
|
-
#
|
15
14
|
# Creates a StringIO from a base64 string.
|
16
15
|
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# Returns:
|
21
|
-
# StringIO: A StringIO object containing the decoded bytes
|
22
|
-
#
|
16
|
+
# @param base64 [String] The base64 string to decode
|
17
|
+
# @return [StringIO] A StringIO object containing the decoded bytes
|
23
18
|
def self.base64_to_bytestream(base64)
|
24
19
|
StringIO.new(Base64.decode64(base64))
|
25
20
|
end
|
26
21
|
|
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
|
22
|
+
# Encodes a compact-u16 value in a compact form (shortvec)
|
35
23
|
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
|
39
|
-
def self.encode_compact_u16(n)
|
24
|
+
# @param u16 [Integer] The compact-u16 value to encode
|
25
|
+
# @return [String] The compactly encoded compact-u16 value
|
26
|
+
def self.encode_compact_u16(u16)
|
40
27
|
out = []
|
28
|
+
|
41
29
|
loop do
|
42
30
|
# In general, n >> 7 shifts the bits of n to the right by
|
43
31
|
# 7 positions, effectively dividing n by 128 and discarding
|
44
32
|
# the remainder (integer division). This is commonly used in
|
45
33
|
# encoding schemes to process one "byte" (7 bits) at a time.
|
46
|
-
if (
|
47
|
-
out <<
|
34
|
+
if (u16 >> 7).zero?
|
35
|
+
out << u16
|
48
36
|
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
37
|
end
|
38
|
+
# The expression out << ((n & 0x7F) | 0x80) is used in variable-length
|
39
|
+
# integer encoding, such as the compact-u16 encoding.
|
40
|
+
#
|
41
|
+
# n & 0x7F:
|
42
|
+
# - 0x7F is 127 in decimal, or 0111 1111 in binary.
|
43
|
+
# - n & 0x7F masks out all but the lowest 7 bits of n. This extracts the least significant 7 bits of n.
|
44
|
+
#
|
45
|
+
# (n & 0x7F) | 0x80:
|
46
|
+
# - 0x80 is 128 in decimal, or 1000 0000 in binary.
|
47
|
+
# - | (bitwise OR) sets the highest bit (the 8th bit) to 1.
|
48
|
+
# - This is a signal that there are more bytes to come in the encoding (i.e., the value hasn't been fully
|
49
|
+
# encoded yet).
|
50
|
+
#
|
51
|
+
# out << ...:
|
52
|
+
# - This appends the resulting byte to the out array.
|
53
|
+
out << ((u16 & 0x7F) | 0x80)
|
54
|
+
u16 >>= 7
|
67
55
|
end
|
56
|
+
|
68
57
|
out.pack('C*')
|
69
58
|
end
|
70
59
|
|
71
|
-
# =============================
|
72
|
-
# Helper: Compact-u16 Decoding (ShortVec)
|
73
|
-
# =============================
|
74
|
-
#
|
75
60
|
# 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
61
|
#
|
81
|
-
#
|
82
|
-
# [Integer, Integer]: The decoded value and the number of bytes read.
|
62
|
+
# Reads bytes one at a time, accumulating the result until the MSB is 0.
|
83
63
|
#
|
84
|
-
|
64
|
+
# @param stream [IO, StringIO] The input to read bytes from.
|
65
|
+
# @return [Integer, Integer] The decoded value and the number of bytes read.
|
66
|
+
def self.decode_compact_u16(stream)
|
85
67
|
value = 0
|
86
68
|
shift = 0
|
87
69
|
bytes_read = 0
|
70
|
+
|
88
71
|
loop do
|
89
|
-
byte =
|
72
|
+
byte = stream.read(1)
|
90
73
|
raise EOFError, 'Unexpected end of input while decoding compact-u16' unless byte
|
91
74
|
|
92
75
|
byte = byte.ord
|
93
76
|
value |= (byte & 0x7F) << shift
|
94
77
|
bytes_read += 1
|
95
|
-
break if (
|
78
|
+
break if byte.nobits?(0x80)
|
96
79
|
|
97
80
|
shift += 7
|
98
81
|
end
|
82
|
+
|
99
83
|
[value, bytes_read]
|
100
84
|
end
|
101
85
|
|
102
|
-
# =============================
|
103
|
-
# Helper: Little-Endian u64 Encoding
|
104
|
-
# =============================
|
105
|
-
#
|
106
86
|
# Encodes a u64 value in little-endian format
|
107
87
|
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
# String: The little-endian encoded u64 value
|
113
|
-
#
|
114
|
-
def self.encode_le_u64(n)
|
115
|
-
[n].pack('Q<') # 64-bit little-endian
|
88
|
+
# @param u64 [Integer] The u64 value to encode
|
89
|
+
# @return [String] The little-endian encoded u64 value
|
90
|
+
def self.encode_le_u64(u64)
|
91
|
+
[u64].pack('Q<') # 64-bit little-endian
|
116
92
|
end
|
117
93
|
|
118
|
-
# =============================
|
119
|
-
# Helper: Little-Endian u64 Decoding
|
120
|
-
# =============================
|
121
|
-
#
|
122
94
|
# Decodes a little-endian u64 value from a sequence of bytes
|
123
95
|
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
|
127
|
-
|
128
|
-
# Integer: The decoded u64 value
|
129
|
-
#
|
130
|
-
def self.decode_le_u64(io)
|
131
|
-
io.read(8).unpack1('Q<')
|
96
|
+
# @param stream [IO, StringIO] The input to read bytes from.
|
97
|
+
# @return [Integer] The decoded u64 value
|
98
|
+
def self.decode_le_u64(stream)
|
99
|
+
stream.read(8).unpack1('Q<')
|
132
100
|
end
|
133
101
|
|
134
|
-
# =============================
|
135
|
-
# Helper: Binary to Base58 Encoding
|
136
|
-
# =============================
|
137
|
-
#
|
138
102
|
# Encodes a sequence of bytes in Base58 format
|
139
103
|
#
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
# Returns:
|
144
|
-
# String: The Base58 encoded string
|
145
|
-
#
|
104
|
+
# @param binary [String] The bytes to encode
|
105
|
+
# @return [String] The Base58 encoded string
|
146
106
|
def self.binary_to_base58(binary)
|
147
107
|
Base58.binary_to_base58(binary, :bitcoin)
|
148
108
|
end
|
149
109
|
|
150
|
-
# =============================
|
151
|
-
# Helper: Base58 Decoding
|
152
|
-
# =============================
|
153
|
-
#
|
154
110
|
# Decodes a Base58 string into a binary string
|
155
111
|
#
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
# Returns:
|
160
|
-
# String: The decoded binary string
|
161
|
-
#
|
112
|
+
# @param string [String] The Base58 encoded string
|
113
|
+
# @return [String] The decoded binary string
|
162
114
|
def self.base58_to_binary(string)
|
163
115
|
base58_to_bytes(string).pack('C*')
|
164
116
|
end
|
165
117
|
|
166
|
-
# =============================
|
167
|
-
# Helper: Base58 Encoding
|
168
|
-
# =============================
|
169
|
-
#
|
170
118
|
# Encodes a sequence of bytes in Base58 format
|
171
119
|
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
175
|
-
# Returns:
|
176
|
-
# String: The Base58 encoded string
|
177
|
-
#
|
120
|
+
# @param bytes [String] The bytes to encode
|
121
|
+
# @return [String] The Base58 encoded string
|
178
122
|
def self.bytes_to_base58(bytes)
|
179
123
|
binary_to_base58(bytes.pack('C*'))
|
180
124
|
end
|
181
125
|
|
182
|
-
# =============================
|
183
|
-
# Helper: Base58 Decoding
|
184
|
-
# =============================
|
185
|
-
#
|
186
126
|
# Decodes a Base58 string into a sequence of bytes
|
187
127
|
#
|
188
|
-
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
# Returns:
|
192
|
-
# String: The decoded bytes
|
193
|
-
#
|
128
|
+
# @param string [String] The Base58 encoded string
|
129
|
+
# @return [String] The decoded bytes
|
194
130
|
def self.base58_to_bytes(string)
|
195
131
|
Base58.base58_to_binary(string, :bitcoin).bytes
|
196
132
|
end
|
197
133
|
|
198
|
-
# =============================
|
199
|
-
# Helper: Base58 Validation
|
200
|
-
# =============================
|
201
|
-
#
|
202
134
|
# Checks if a string is a valid Base58 string
|
203
135
|
#
|
204
|
-
#
|
205
|
-
#
|
206
|
-
#
|
207
|
-
# Returns:
|
208
|
-
# Boolean: True if the string is a valid Base58 string, false otherwise
|
209
|
-
#
|
136
|
+
# @param string [String] The string to check
|
137
|
+
# @return [Boolean] True if the string is a valid Base58 string, false otherwise
|
210
138
|
def self.valid_base58?(string)
|
211
139
|
return false if string.nil? || string.empty?
|
212
140
|
|
@@ -5,6 +5,14 @@ require 'ffi'
|
|
5
5
|
|
6
6
|
module Solace
|
7
7
|
module Utils
|
8
|
+
# Module for interacting with the Curve25519 Dalek library
|
9
|
+
#
|
10
|
+
# This module provides a wrapper around the Curve25519 Dalek library, which is a pure-Rust
|
11
|
+
# implementation of the Curve25519 elliptic curve. It uses FFI to interface with the native
|
12
|
+
# rust library when checking if a point is on the curve.
|
13
|
+
#
|
14
|
+
# @see Solace::Utils::PDA
|
15
|
+
# @since 0.0.2
|
8
16
|
module Curve25519Dalek
|
9
17
|
extend FFI::Library
|
10
18
|
|
@@ -22,8 +30,7 @@ module Solace
|
|
22
30
|
else raise 'Unsupported platform'
|
23
31
|
end
|
24
32
|
|
25
|
-
#
|
26
|
-
#
|
33
|
+
# !@attribute LIB_PATH
|
27
34
|
# @return [String] The path to the native library
|
28
35
|
LIB_PATH = File.expand_path(libfile, __dir__)
|
29
36
|
|
@@ -31,8 +38,6 @@ module Solace
|
|
31
38
|
ffi_lib LIB_PATH
|
32
39
|
|
33
40
|
# Attach the native function
|
34
|
-
#
|
35
|
-
# @return [FFI::Function] The native function
|
36
41
|
attach_function :is_on_curve, [:pointer], :int
|
37
42
|
|
38
43
|
# Checks if a point is on the curve
|