solana-ruby-web3js 1.0.1.beta3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 769dd8aff42953255243d337b811ab9bd61e27e19e6957d235e9e015300f925b
4
- data.tar.gz: e940aa048c15af34c5f06bf5de1db3716e8f68f1f83c41fe480a6de748082310
3
+ metadata.gz: 9f614bab0b21438c2820d0035a0aa33567a11fedd3ecb998666b39e5ab28681d
4
+ data.tar.gz: c63fd671d6328732c39994032f94aff219ff77eda8cd899c5d1f0c29f86f4524
5
5
  SHA512:
6
- metadata.gz: 7cd8eb6a1ff5a1c749cb1ded759323bd1a7a43dea78e6d19444f1d1e8dc77ca24f757c5fc53412431020f09960db0cef8f2bd519641a0bdc5790c4dc8e60a581
7
- data.tar.gz: d6c68d6c97ae798d5b79b4fcaae962ac3e34e7e2d28d61b80124b4725b3926803c49f20bd055bc7c6af9b46229558111a3ea0dae932d15750f726484a0c6b460
6
+ metadata.gz: 4015bdc6179d57105ea1da82daa9d6ec19f623098d636271fb0e7ab9210d563414eae364271e58be82a2ce9ed6dc0804de53db7e7a15c5d353da4ff30faf5124
7
+ data.tar.gz: 8fca9ec2ccdcc5b3ea72f54fa6fad27fee3fe1aef20b145107382c523a0a70df90812f269fdcdee3d9d62b732e6ebe7ce6ad3e1514257d379db7088de55941d1
data/.DS_Store CHANGED
Binary file
data/Gemfile.lock CHANGED
@@ -1,9 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- solana-ruby-web3js (1.0.1.beta1)
4
+ solana-ruby-web3js (1.0.1.beta3)
5
5
  base58 (~> 0.2.3)
6
6
  base64 (~> 0.2.0)
7
+ ed25519
8
+ rbnacl (~> 6.0)
7
9
  websocket-client-simple (~> 0.8.0)
8
10
 
9
11
  GEM
@@ -64,6 +66,8 @@ GEM
64
66
  ed25519 (1.3.0)
65
67
  erubi (1.13.0)
66
68
  event_emitter (0.2.6)
69
+ ffi (1.17.0)
70
+ ffi (1.17.0-arm64-darwin)
67
71
  flay (2.13.3)
68
72
  erubi (~> 1.10)
69
73
  path_expander (~> 1.0)
@@ -93,6 +97,8 @@ GEM
93
97
  public_suffix (6.0.1)
94
98
  racc (1.8.1)
95
99
  rainbow (3.1.1)
100
+ rbnacl (6.0.1)
101
+ ffi
96
102
  reek (6.3.0)
97
103
  dry-schema (~> 1.13.0)
98
104
  parser (~> 3.3.0)
@@ -0,0 +1,30 @@
1
+ module SolanaRuby
2
+ module DataTypes
3
+ class Blob
4
+ attr_reader :size
5
+
6
+ # Constructor to initialize size of the blob
7
+ def initialize(size)
8
+ raise ArgumentError, "Size must be a positive integer" unless size.is_a?(Integer) && size > 0
9
+ @size = size
10
+ end
11
+
12
+ # Serialize the given object to a byte array
13
+ def serialize(obj)
14
+ # Ensure obj is an array and then convert to byte array
15
+ obj = [obj] unless obj.is_a?(Array)
16
+ raise ArgumentError, "Object must be an array of bytes" unless obj.all? { |e| e.is_a?(Integer) && e.between?(0, 255) }
17
+
18
+ obj.pack('C*').bytes
19
+ end
20
+
21
+ # Deserialize a byte array into the original object format
22
+ def deserialize(bytes)
23
+ # Ensure the byte array is of the correct size
24
+ raise ArgumentError, "Byte array size must match the expected size" unless bytes.length == @size
25
+
26
+ bytes.pack('C*')
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ module SolanaRuby
2
+ module DataTypes
3
+ class Layout
4
+ attr_reader :fields
5
+
6
+ def initialize(fields)
7
+ @fields = fields
8
+ end
9
+
10
+ def serialize(params)
11
+ fields.flat_map do |field, type|
12
+ data_type = type.is_a?(Symbol) ? SolanaRuby::DataTypes.send(type) : type
13
+ data_type.serialize(params[field])
14
+ end
15
+ end
16
+
17
+ def deserialize(bytes)
18
+ result = {}
19
+ fields.map do |field, type|
20
+ data_type = type.is_a?(Symbol) ? SolanaRuby::DataTypes.send(type) : type
21
+ result[field] = data_type.deserialize(bytes.shift(data_type.size))
22
+ end
23
+ result
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ module SolanaRuby
2
+ module DataTypes
3
+ class NearInt64
4
+ attr_reader :size
5
+
6
+ V2E32 = 2.pow(32)
7
+
8
+ def initialize
9
+ @size = 8
10
+ end
11
+
12
+ def serialize(obj)
13
+ uint = UnsignedInt.new(32)
14
+ numbers = divmod_int64(obj)
15
+ numbers.map{|x| uint.serialize(x)}.flatten
16
+ end
17
+
18
+ def deserialize(bytes)
19
+ raise "Invalid serialization (wrong size)" if @size && bytes.size != @size
20
+ uint = UnsignedInt.new(32)
21
+ half_size = @size/2
22
+
23
+ lo, hi = [bytes[0..half_size-1], bytes[half_size..-1]].map{|x| uint.deserialize(x)}
24
+
25
+ rounded_int64(hi, lo)
26
+ end
27
+
28
+ def divmod_int64(obj)
29
+ obj = obj * 1.0
30
+ hi32 = (obj / V2E32).floor
31
+ lo32 = (obj - (hi32 * V2E32)).floor
32
+ [lo32, hi32]
33
+ end
34
+
35
+ def rounded_int64(hi32, lo32)
36
+ hi32 * V2E32 + lo32
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ module SolanaRuby
2
+ module DataTypes
3
+ class Sequence
4
+ def initialize count, type
5
+ @count = count
6
+ @type = type
7
+ end
8
+
9
+ def serialize items
10
+ items.map do |item|
11
+ @type.serialize(item)
12
+ end.flatten
13
+ end
14
+
15
+ def deserialize bytes
16
+ @count.times.map do
17
+ current_bytes = bytes.shift(@type.size)
18
+ @type.deserialize(current_bytes)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module SolanaRuby
2
+ module DataTypes
3
+ class UnsignedInt
4
+ attr_reader :size
5
+
6
+ BITS = {
7
+ 8 => { directive: 'C*', size: 1 },
8
+ 32 => { directive: 'L*', size: 4 },
9
+ 64 => { directive: 'Q*', size: 8 }
10
+ }
11
+
12
+ def initialize(bits)
13
+ @bits = bits
14
+ type = BITS[@bits]
15
+ raise "Can only fit #{BITS.keys}" unless type
16
+ @size = type[:size]
17
+ @directive = type[:directive]
18
+ end
19
+
20
+ # Serialize the unsigned integer into bytes
21
+ def serialize(obj)
22
+ raise "Can only serialize integers" unless obj.is_a?(Integer)
23
+ raise "Cannot serialize negative integers" if obj < 0
24
+
25
+ if obj >= 256**@size
26
+ raise "Integer too large (does not fit in #{@size} bytes)"
27
+ end
28
+
29
+ [obj].pack(@directive).bytes
30
+ end
31
+
32
+ # Deserialize bytes into the unsigned integer
33
+ def deserialize(bytes)
34
+ raise "Invalid serialization (wrong size)" if bytes.size != @size
35
+
36
+ bytes.pack('C*').unpack(@directive).first
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ module SolanaRuby
2
+ module DataTypes
3
+ extend self
4
+
5
+ def uint8
6
+ UnsignedInt.new(8)
7
+ end
8
+
9
+ def uint32
10
+ UnsignedInt.new(32)
11
+ end
12
+
13
+ def uint64
14
+ UnsignedInt.new(64)
15
+ end
16
+
17
+ def near_int64
18
+ NearInt64.new
19
+ end
20
+
21
+ def blob1
22
+ Blob.new(1)
23
+ end
24
+ end
25
+ end
@@ -23,7 +23,8 @@ module SolanaRuby
23
23
 
24
24
  def request(method, params = [])
25
25
  http = Net::HTTP.new(@uri.host, @uri.port)
26
- http.use_ssl = true
26
+ local_hosts = ['localhost', '127.0.0.1', '[::1]']
27
+ http.use_ssl = true unless local_hosts.include?(@uri.host.downcase)
27
28
 
28
29
  request = Net::HTTP::Post.new(@uri.request_uri, {'Content-Type' => 'application/json'})
29
30
  request.body = {
@@ -0,0 +1,42 @@
1
+ module SolanaRuby
2
+ class Keypair
3
+ require 'rbnacl'
4
+ require 'base58'
5
+
6
+ # Generates a new Ed25519 keypair
7
+ def self.generate
8
+ signing_key = RbNaCl::Signatures::Ed25519::SigningKey.generate
9
+ public_key_bytes = signing_key.verify_key.to_bytes # Binary format for public key
10
+ private_key_bytes = signing_key.to_bytes
11
+ private_key_hex = private_key_bytes.unpack1('H*') # Hex format for private key
12
+
13
+ # Convert public key binary to Base58 for readability and compatibility
14
+ {
15
+ public_key: Base58.binary_to_base58(public_key_bytes, :bitcoin),
16
+ private_key: private_key_hex,
17
+ full_private_key: Base58.binary_to_base58((private_key_bytes + public_key_bytes), :bitcoin)
18
+ }
19
+ end
20
+
21
+ # Restores a keypair from a private key in hex format
22
+ def self.from_private_key(private_key_hex)
23
+ raise ArgumentError, "Invalid private key length" unless private_key_hex.size == 64
24
+
25
+ # Convert hex private key to binary format for signing key
26
+ private_key_bytes = [private_key_hex].pack('H*')
27
+
28
+ # Initialize signing key
29
+ signing_key = RbNaCl::Signatures::Ed25519::SigningKey.new(private_key_bytes)
30
+
31
+ # Extract public key in binary format
32
+ public_key_bytes = signing_key.verify_key.to_bytes
33
+
34
+ # Return public key in Base58 format and private key in hex format
35
+ {
36
+ public_key: Base58.binary_to_base58(public_key_bytes, :bitcoin),
37
+ private_key: private_key_hex,
38
+ full_private_key: Base58.binary_to_base58((private_key_bytes + public_key_bytes), :bitcoin)
39
+ }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,107 @@
1
+ module SolanaRuby
2
+ class Message
3
+ PUBKEY_LENGTH = 32
4
+
5
+ attr_reader :header, :account_keys, :recent_blockhash, :instructions
6
+
7
+ def initialize(header:, account_keys:, recent_blockhash:, instructions:)
8
+ @header = header
9
+ @account_keys = account_keys
10
+ @recent_blockhash = recent_blockhash
11
+ @instructions = instructions
12
+ end
13
+
14
+ def self.from(bytes)
15
+ bytes = bytes.dup
16
+ num_required_signatures = bytes.shift
17
+ num_readonly_signed_accounts = bytes.shift
18
+ num_readonly_unsigned_accounts = bytes.shift
19
+ account_count = Utils.decode_length(bytes)
20
+
21
+ account_keys = account_count.times.map do
22
+ account_bytes = bytes.slice!(0, PUBKEY_LENGTH)
23
+ Utils.bytes_to_base58(account_bytes)
24
+ end
25
+
26
+ recent_blockhash_bytes = bytes.slice!(0, PUBKEY_LENGTH)
27
+ recent_blockhash = Utils.bytes_to_base58(recent_blockhash_bytes)
28
+
29
+ instruction_count = Utils.decode_length(bytes)
30
+ instructions = instruction_count.times.map do
31
+ program_id_index = bytes.shift
32
+ account_count = Utils.decode_length(bytes)
33
+
34
+ accounts = bytes.slice!(0, account_count)
35
+
36
+ data_length = Utils.decode_length(bytes)
37
+ data_bytes = bytes.slice!(0, data_length)
38
+ {program_id_index: program_id_index, accounts: accounts, data: data_bytes}
39
+ end
40
+ self.new({
41
+ header: {
42
+ num_required_signatures: num_required_signatures,
43
+ num_readonly_signed_accounts:num_readonly_signed_accounts,
44
+ num_readonly_unsigned_accounts:num_readonly_unsigned_accounts,
45
+ },
46
+ account_keys: account_keys,
47
+ recent_blockhash: recent_blockhash,
48
+ instructions: instructions
49
+ })
50
+ end
51
+
52
+ def serialize
53
+ num_keys = account_keys.length
54
+ key_count = Utils.encode_length(num_keys)
55
+
56
+ layout = SolanaRuby::DataTypes::Layout.new({
57
+ num_required_signatures: :blob1,
58
+ num_readonly_signed_accounts: :blob1,
59
+ num_readonly_unsigned_accounts: :blob1,
60
+ key_count: SolanaRuby::DataTypes::Blob.new(key_count.length),
61
+ keys: SolanaRuby::DataTypes::Sequence.new(num_keys, SolanaRuby::DataTypes::Blob.new(32)),
62
+ recent_blockhash: SolanaRuby::DataTypes::Blob.new(32)
63
+ })
64
+
65
+ sign_data = layout.serialize({
66
+ num_required_signatures: header[:num_required_signatures],
67
+ num_readonly_signed_accounts: header[:num_readonly_signed_accounts],
68
+ num_readonly_unsigned_accounts: header[:num_readonly_unsigned_accounts],
69
+ key_count: key_count,
70
+ keys: account_keys.map{|x| Utils.base58_to_bytes(x)},
71
+ recent_blockhash: Utils.base58_to_bytes(recent_blockhash)
72
+ })
73
+
74
+ instruction_count = Utils.encode_length(@instructions.length)
75
+ sign_data += instruction_count
76
+
77
+ data = @instructions.map do |instruction|
78
+ instruction_layout = SolanaRuby::DataTypes::Layout.new({
79
+ program_id_index: :uint8,
80
+ key_indices_count: SolanaRuby::DataTypes::Blob.new(key_count.length),
81
+ key_indices: SolanaRuby::DataTypes::Sequence.new(num_keys, SolanaRuby::DataTypes::Blob.new(8)),
82
+ data_length: SolanaRuby::DataTypes::Blob.new(key_count.length),
83
+ data: SolanaRuby::DataTypes::Sequence.new(num_keys, SolanaRuby::DataTypes::UnsignedInt.new(8)),
84
+ })
85
+
86
+ key_indices_count = Utils.encode_length(instruction[:accounts].length)
87
+ data_count = Utils.encode_length(instruction[:data].length)
88
+
89
+ instruction_layout.serialize({
90
+ program_id_index: instruction[:program_id_index],
91
+ key_indices_count: key_indices_count,
92
+ key_indices: instruction[:accounts],
93
+ data_length: data_count,
94
+ data: instruction[:data]
95
+ })
96
+ end.flatten
97
+
98
+ sign_data += data
99
+ sign_data
100
+ end
101
+
102
+ def is_account_writable(index)
103
+ index < header[:num_required_signatures] - header[:num_readonly_signed_accounts] ||
104
+ (index >= header[:num_required_signatures] && index < account_keys.length - header[:num_readonly_unsigned_accounts])
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,281 @@
1
+ module SolanaRuby
2
+ class Transaction
3
+ require 'rbnacl'
4
+ SIGNATURE_LENGTH = 64
5
+ PACKET_DATA_SIZE = 1280 - 40 - 8
6
+ DEFAULT_SIGNATURE = Array.new(64, 0)
7
+
8
+ attr_accessor :instructions, :signatures, :fee_payer, :recent_blockhash, :message
9
+
10
+ def initialize(recent_blockhash: nil, signatures: [], instructions: [], fee_payer: nil)
11
+ @recent_blockhash = recent_blockhash
12
+ @signatures = signatures
13
+ @instructions = instructions
14
+ @fee_payer = fee_payer
15
+ end
16
+
17
+ def add_instruction(instruction)
18
+ @instructions << instruction
19
+ end
20
+
21
+ def set_fee_payer(pubkey)
22
+ puts "Setting fee payer: #{pubkey.inspect}" # Debugging output
23
+ @fee_payer = pubkey # Store as-is since Base58 gem can handle encoding/decoding
24
+ end
25
+
26
+ def set_recent_blockhash(blockhash)
27
+ # raise "Invalid Base58 blockhash" unless Base58.valid?(blockhash)
28
+ @recent_blockhash = blockhash # Store as-is for similar reasons
29
+ end
30
+
31
+ def self.from(base64_string)
32
+ bytes = Base64.decode64(base64_string).bytes
33
+ signature_count = Utils.decode_length(bytes)
34
+ signatures = signature_count.times.map do
35
+ signature_bytes = bytes.slice!(0, SIGNATURE_LENGTH)
36
+ Utils.bytes_to_base58(signature_bytes)
37
+ end
38
+ msg = Message.from(bytes)
39
+ self.populate(msg, signatures)
40
+ end
41
+
42
+ def serialize
43
+ sign_data = serialize_message
44
+
45
+ signature_count = Utils.encode_length(signatures.length)
46
+ raise 'invalid length!' if signatures.length > 256
47
+
48
+ wire_transaction = signature_count
49
+
50
+ signatures.each do |signature|
51
+ if signature
52
+ signature_bytes = signature[:signature]
53
+ raise 'signature is empty' unless (signature_bytes)
54
+ raise 'signature has invalid length' unless (signature_bytes.length == 64)
55
+ wire_transaction += signature_bytes
56
+ raise "Transaction too large: #{wire_transaction.length} > #{PACKET_DATA_SIZE}" unless wire_transaction.length <= PACKET_DATA_SIZE
57
+ wire_transaction
58
+ end
59
+ end
60
+
61
+ wire_transaction += sign_data
62
+ wire_transaction
63
+ end
64
+
65
+ def to_base64
66
+ Base64.strict_encode64(serialize.pack('C*'))
67
+ end
68
+
69
+ def add(item)
70
+ instructions.push(item)
71
+ end
72
+
73
+ def sign(keys)
74
+ raise 'No signers' unless keys.any?
75
+
76
+ keys = keys.uniq{ |k| key[:public_key] }
77
+ @signatures = keys.map do |key|
78
+ {
79
+ signature: nil,
80
+ public_key: key[:public_key]
81
+ }
82
+ end
83
+
84
+ message = compile_message
85
+ partial_sign(message, keys)
86
+ true
87
+ end
88
+
89
+ private
90
+
91
+ def serialize_message
92
+ compile.serialize
93
+ end
94
+
95
+ def compile
96
+ message = compile_message
97
+ signed_keys = message.account_keys.slice(0, message.header[:num_required_signatures])
98
+
99
+ if signatures.length == signed_keys.length
100
+ valid = signatures.each_with_index.all?{|pair, i| signed_keys[i] == pair[:public_key]}
101
+ return message if valid
102
+ end
103
+
104
+ @signatures = signed_keys.map do |public_key|
105
+ {
106
+ signature: nil,
107
+ public_key: public_key
108
+ }
109
+ end
110
+
111
+ message
112
+ end
113
+
114
+ def compile_message
115
+ check_for_errors
116
+ fetch_message_data
117
+ message = Message.new(
118
+ header: {
119
+ num_required_signatures: @num_required_signatures,
120
+ num_readonly_signed_accounts: @num_readonly_signed_accounts,
121
+ num_readonly_unsigned_accounts: @num_readonly_unsigned_accounts,
122
+ },
123
+ account_keys: @account_keys, recent_blockhash: recent_blockhash, instructions: @instructs
124
+ )
125
+ message
126
+ end
127
+
128
+ def check_for_errors
129
+ raise 'Transaction recent_blockhash required' unless recent_blockhash
130
+
131
+ puts 'No instructions provided' if instructions.length < 1
132
+
133
+ if fee_payer.nil? && signatures.length > 0 && signatures[0][:public_key]
134
+ @fee_payer = signatures[0][:public_key] if (signatures.length > 0 && signatures[0][:public_key])
135
+ end
136
+
137
+ raise('Transaction fee payer required') if @fee_payer.nil?
138
+
139
+ instructions.each_with_index do |instruction, i|
140
+ raise("Transaction instruction index #{i} has undefined program id") unless instruction.program_id
141
+ end
142
+ end
143
+
144
+ def fetch_message_data
145
+ program_ids = []
146
+ account_metas= []
147
+
148
+ instructions.each do |instruction|
149
+ account_metas += instruction.keys
150
+ program_ids.push(instruction.program_id) unless program_ids.include?(instruction.program_id)
151
+ end
152
+
153
+ # Append programID account metas
154
+ append_program_id(program_ids, account_metas)
155
+
156
+ # Sort. Prioritizing first by signer, then by writable
157
+ signer_order(account_metas)
158
+
159
+ # Cull duplicate account metas
160
+ unique_metas = []
161
+ add_unique_meta_data(unique_metas, account_metas)
162
+
163
+ add_fee_payer_meta(unique_metas)
164
+
165
+ # Disallow unknown signers
166
+ disallow_signers(signatures, unique_metas)
167
+
168
+ # Split out signing from non-signing keys and count header values
169
+ signed_keys = []
170
+ unsigned_keys = []
171
+ header_params = split_keys(unique_metas, signed_keys, unsigned_keys)
172
+ @account_keys = signed_keys + unsigned_keys
173
+
174
+ # add instruction structure
175
+ @instructs = add_instructs
176
+ end
177
+
178
+ def append_program_id(program_ids, account_metas)
179
+ program_ids.each do |programId|
180
+ account_metas.push({
181
+ pubkey: programId,
182
+ is_signer: false,
183
+ is_writable: false,
184
+ })
185
+ end
186
+ end
187
+
188
+ def signer_order(account_metas)
189
+ account_metas.sort! do |x, y|
190
+ check_signer = x[:is_signer] == y[:is_signer] ? nil : x[:is_signer] ? -1 : 1
191
+ check_writable = x[:is_writable] == y[:is_writable] ? nil : (x[:is_writable] ? -1 : 1)
192
+ (check_signer || check_writable) || 0
193
+ end
194
+ end
195
+
196
+ def add_unique_meta_data(unique_metas, account_metas)
197
+ account_metas.each do |account_meta|
198
+ pubkey_string = account_meta[:pubkey]
199
+ unique_index = unique_metas.find_index{|x| x[:pubkey] == pubkey_string }
200
+ if unique_index
201
+ unique_metas[unique_index][:is_writable] = unique_metas[unique_index][:is_writable] || account_meta[:is_writable]
202
+ else
203
+ unique_metas.push(account_meta);
204
+ end
205
+ end
206
+ end
207
+
208
+ def add_fee_payer_meta(unique_metas)
209
+ # Move fee payer to the front
210
+ fee_payer_index = unique_metas.find_index { |x| x[:pubkey] == fee_payer }
211
+ if fee_payer_index
212
+ payer_meta = unique_metas.delete_at(fee_payer_index)
213
+ payer_meta[:is_signer] = true
214
+ payer_meta[:is_writable] = true
215
+ unique_metas.unshift(payer_meta)
216
+ else
217
+ unique_metas.unshift({
218
+ pubkey: fee_payer,
219
+ is_signer: true,
220
+ is_writable: true,
221
+ })
222
+ end
223
+ end
224
+
225
+ def disallow_signers(signatures, unique_metas)
226
+ signatures.each do |signature|
227
+ unique_index = unique_metas.find_index{ |x| x[:pubkey] == signature[:public_key] }
228
+
229
+ if unique_index
230
+ unique_metas[unique_index][:is_signer] = true unless unique_metas[unique_index][:is_signer]
231
+ else
232
+ raise "unknown signer: #{signature[:public_key]}"
233
+ end
234
+ end
235
+ end
236
+
237
+ def add_instructs
238
+ instructions.map do |instruction|
239
+ {
240
+ program_id_index: @account_keys.index(instruction.program_id),
241
+ accounts: instruction.keys.map { |meta| @account_keys.index(meta[:pubkey]) },
242
+ data: instruction.data
243
+ }
244
+ end
245
+ end
246
+
247
+ def split_keys(unique_metas, signed_keys, unsigned_keys)
248
+ @num_required_signatures = 0
249
+ @num_readonly_signed_accounts = 0
250
+ @num_readonly_unsigned_accounts = 0
251
+ unique_metas.each do |meta|
252
+ if meta[:is_signer]
253
+ signed_keys.push(meta[:pubkey])
254
+ @num_required_signatures += 1
255
+ @num_readonly_signed_accounts += 1 if (!meta[:is_writable])
256
+ else
257
+ unsigned_keys.push(meta[:pubkey])
258
+ @num_readonly_unsigned_accounts += 1 if (!meta[:is_writable])
259
+ end
260
+ end
261
+ end
262
+
263
+ def partial_sign(message, keys)
264
+ sign_data = message.serialize
265
+ keys.each do |key|
266
+ private_key_bytes = [key[:private_key]].pack('H*')
267
+ signing_key = RbNaCl::Signatures::Ed25519::SigningKey.new(private_key_bytes)
268
+ signature = signing_key.sign(sign_data.pack('C*')).bytes
269
+ add_signature(key[:public_key], signature)
270
+ end
271
+ end
272
+
273
+ def add_signature(pubkey, signature)
274
+ raise 'error' unless signature.length === 64
275
+ index = signatures.find_index{|s| s[:public_key] == pubkey}
276
+ raise "unknown signer: #{pubkey}" unless index
277
+
278
+ @signatures[index][:signature] = signature
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,138 @@
1
+ module SolanaRuby
2
+ class TransactionHelper
3
+ require 'base58'
4
+ require 'pry'
5
+
6
+ # Constants for program IDs
7
+ SYSTEM_PROGRAM_ID = '11111111111111111111111111111111'
8
+ TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
9
+ ASSOCIATED_TOKEN_PROGRAM_ID = 'ATokenGP3evbxxpQ7bYPLNNaxD2c4bqtvWjpKbmz6HjH'
10
+
11
+ INSTRUCTION_LAYOUTS = {
12
+ # Native SOL transfer
13
+ sol_transfer: {
14
+ instruction: :uint32,
15
+ lamports: :near_int64
16
+ },
17
+ # SPL token transfer
18
+ spl_transfer: {
19
+ instruction: :uint8,
20
+ amount: :uint64
21
+ },
22
+ # Create account layout
23
+ create_account: {
24
+ instruction: :uint8,
25
+ lamports: :uint64,
26
+ space: :uint64
27
+ }
28
+ }
29
+
30
+ # Method to create a system account (e.g., for SPL token or SOL)
31
+ def self.create_account(from_pubkey, new_account_pubkey, lamports, space, owner_pubkey = SYSTEM_PROGRAM_ID)
32
+ instruction_data = encode_data(INSTRUCTION_LAYOUTS[:create_account], { instruction: 0, lamports: lamports, space: space })
33
+ create_account_instruction = TransactionInstruction.new(
34
+ keys: [
35
+ { pubkey: from_pubkey, is_signer: true, is_writable: true },
36
+ { pubkey: new_account_pubkey, is_signer: false, is_writable: true },
37
+ { pubkey: owner_pubkey, is_signer: false, is_writable: false }
38
+ ],
39
+ program_id: owner_pubkey,
40
+ data: instruction_data.bytes
41
+ )
42
+ create_account_instruction
43
+ end
44
+
45
+ def self.create_and_sign_transaction(from_pubkey, new_account_pubkey, lamports, space, recent_blockhash)
46
+ # Create the transaction
47
+ transaction = Transaction.new
48
+ transaction.set_fee_payer(from_pubkey)
49
+ transaction.set_recent_blockhash(recent_blockhash)
50
+
51
+ # Add the create account instruction to the transaction
52
+ create_account_instruction = create_account(from_pubkey, new_account_pubkey, lamports, space)
53
+ transaction.add_instruction(create_account_instruction)
54
+
55
+ # You would then sign the transaction and send it as needed
56
+ # Example: signing and sending the transaction
57
+ transaction
58
+ end
59
+
60
+ # Method to create a SOL transfer instruction
61
+ def self.transfer_sol_transaction(from_pubkey, to_pubkey, lamports)
62
+ fields = INSTRUCTION_LAYOUTS[:sol_transfer]
63
+ data = encode_data(fields, { instruction: 2, lamports: lamports })
64
+ TransactionInstruction.new(
65
+ keys: [
66
+ { pubkey: from_pubkey, is_signer: true, is_writable: true },
67
+ { pubkey: to_pubkey, is_signer: false, is_writable: true }
68
+ ],
69
+ program_id: SYSTEM_PROGRAM_ID,
70
+ data: data
71
+ )
72
+ end
73
+
74
+ # Helper to create a new transaction for SOL transfer
75
+ def self.new_sol_transaction(from_pubkey, to_pubkey, lamports, recent_blockhash)
76
+ transaction = Transaction.new
77
+ transaction.set_fee_payer(from_pubkey)
78
+ transaction.set_recent_blockhash(recent_blockhash)
79
+ transfer_instruction = transfer_sol_transaction(from_pubkey, to_pubkey, lamports)
80
+ transaction.add_instruction(transfer_instruction)
81
+ transaction
82
+ end
83
+
84
+ # Method to create an SPL token transfer instruction
85
+ def self.transfer_spl_token(source, destination, owner, amount)
86
+ fields = INSTRUCTION_LAYOUTS[:spl_transfer]
87
+ data = encode_data(fields, { instruction: 3, amount: amount }) # Instruction type 3: Transfer tokens
88
+ TransactionInstruction.new(
89
+ keys: [
90
+ { pubkey: source, is_signer: false, is_writable: true },
91
+ { pubkey: destination, is_signer: false, is_writable: true },
92
+ { pubkey: owner, is_signer: true, is_writable: false }
93
+ ],
94
+ program_id: TOKEN_PROGRAM_ID,
95
+ data: data
96
+ )
97
+ end
98
+
99
+ # Helper to create a new transaction for SPL token transfer
100
+ def self.new_spl_token_transaction(source, destination, owner, amount, recent_blockhash)
101
+ transaction = Transaction.new
102
+ transaction.set_fee_payer(owner)
103
+ transaction.set_recent_blockhash(recent_blockhash)
104
+ transfer_instruction = transfer_spl_token(source, destination, owner, amount)
105
+ transaction.add_instruction(transfer_instruction)
106
+ transaction
107
+ end
108
+
109
+ # Method to create an associated token account for a given token mint
110
+ def self.create_associated_token_account(from_pubkey, token_mint, owner_pubkey)
111
+ data = [0, 0, 0, 0] # No data required for account creation
112
+ create_account_instruction = TransactionInstruction.new(
113
+ keys: [
114
+ { pubkey: from_pubkey, is_signer: true, is_writable: true },
115
+ { pubkey: owner_pubkey, is_signer: false, is_writable: true },
116
+ { pubkey: token_mint, is_signer: false, is_writable: false },
117
+ { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, is_signer: false, is_writable: false },
118
+ { pubkey: SYSTEM_PROGRAM_ID, is_signer: false, is_writable: false }
119
+ ],
120
+ program_id: ASSOCIATED_TOKEN_PROGRAM_ID,
121
+ data: data
122
+ )
123
+ create_account_instruction
124
+ end
125
+
126
+ # Utility to encode data using predefined layouts
127
+ def self.encode_data(fields, data)
128
+ layout = SolanaRuby::DataTypes::Layout.new(fields)
129
+ layout.serialize(data)
130
+ end
131
+
132
+ # Utility to decode data using predefined layouts
133
+ def self.decode_data(fields, data)
134
+ layout = SolanaRuby::DataTypes::Layout.new(fields)
135
+ layout.deserialize(data)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,42 @@
1
+ module SolanaRuby
2
+ class TransactionInstruction
3
+ require 'base58'
4
+
5
+ attr_accessor :keys, :program_id, :data
6
+
7
+ def initialize(keys:, program_id:, data:)
8
+ @keys = keys # Array of account metadata hashes
9
+ @program_id = program_id # Program ID in Base58
10
+ @data = data # Binary data for the instruction
11
+ end
12
+
13
+ def serialize
14
+ serialized_instruction = ""
15
+
16
+ # Convert and serialize the program ID from Base58 to binary
17
+ program_id_binary = Base58.base58_to_binary(@program_id)
18
+ serialized_instruction << program_id_binary
19
+
20
+ # Serialize the number of keys
21
+ serialized_instruction << [@keys.length].pack("C")
22
+
23
+ # Serialize each key (pubkey in binary, is_signer, is_writable flags)
24
+ @keys.each do |key_meta|
25
+ # Convert public key to binary and serialize it
26
+ pubkey_binary = Base58.base58_to_binary(key_meta[:pubkey])
27
+ serialized_instruction << pubkey_binary
28
+
29
+ # Serialize meta flags (is_signer and is_writable)
30
+ meta_flags = (key_meta[:is_signer] ? 1 : 0) | (key_meta[:is_writable] ? 2 : 0)
31
+ serialized_instruction << [meta_flags].pack("C")
32
+ end
33
+
34
+ # Serialize data length (encoded as a single byte, can adjust with C, S, and L accordingly if data is larger)
35
+ serialized_instruction << [@data.length].pack("C")
36
+
37
+ # Serialize the actual data in binary format
38
+ serialized_instruction << @data
39
+ serialized_instruction
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,66 @@
1
+ require 'base58'
2
+ require 'digest/sha2'
3
+
4
+ module SolanaRuby
5
+ class Utils
6
+ class << self
7
+ # Decodes a length-prefixed byte array using a variable-length encoding.
8
+ def decode_length(bytes)
9
+ raise ArgumentError, "Input must be an array of bytes" unless bytes.is_a?(Array)
10
+
11
+ length = 0
12
+ size = 0
13
+ loop do
14
+ raise "Unexpected end of bytes during length decoding" if bytes.empty?
15
+
16
+ byte = bytes.shift
17
+ length |= (byte & 0x7F) << (size * 7)
18
+ size += 1
19
+ break if (byte & 0x80).zero?
20
+ end
21
+ length
22
+ end
23
+
24
+ # Encodes a length as a variable-length byte array.
25
+ def encode_length(length)
26
+ raise ArgumentError, "Length must be a non-negative integer" unless length.is_a?(Integer) && length >= 0
27
+
28
+ bytes = []
29
+ loop do
30
+ byte = length & 0x7F
31
+ length >>= 7
32
+ if length.zero?
33
+ bytes << byte
34
+ break
35
+ else
36
+ bytes << (byte | 0x80)
37
+ end
38
+ end
39
+ bytes
40
+ end
41
+
42
+ # Converts a byte array to a Base58-encoded string.
43
+ def bytes_to_base58(bytes)
44
+ raise ArgumentError, "Input must be an array of bytes" unless bytes.is_a?(Array)
45
+
46
+ Base58.binary_to_base58(bytes.pack('C*'), :bitcoin)
47
+ end
48
+
49
+ # Converts a Base58-encoded string to a byte array.
50
+ def base58_to_bytes(base58_string)
51
+ raise ArgumentError, "Input must be a non-empty string" unless base58_string.is_a?(String) && !base58_string.empty?
52
+
53
+ Base58.base58_to_binary(base58_string, :bitcoin).bytes
54
+ rescue ArgumentError
55
+ raise "Invalid Base58 string: #{base58_string}"
56
+ end
57
+
58
+ # Computes the SHA-256 hash of the given data and returns it as a hexadecimal string.
59
+ def sha256(data)
60
+ raise ArgumentError, "Data must be a string" unless data.is_a?(String)
61
+
62
+ Digest::SHA256.hexdigest(data)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolanaRuby
4
- VERSION = "1.0.1.beta3"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/solana_ruby.rb CHANGED
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "solana_ruby/version"
4
- require_relative "solana_ruby/http_client"
5
- require_relative "solana_ruby/web_socket_client"
3
+ Dir[File.join(__dir__, 'solana_ruby', '*.rb')].each { |file| require file }
6
4
  # Dir["solana_ruby/*.rb"].each { |f| require_relative f.delete(".rb") }
7
-
5
+ require 'pry'
8
6
  module SolanaRuby
9
7
  class Error < StandardError; end
10
8
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(File.dirname(__dir__), 'lib/solana_ruby/*.rb')].each { |file| require file }
4
+ Dir[File.join(File.dirname(__dir__), 'lib/solana_ruby/**/*.rb')].each { |file| require file }
5
+
6
+ # Testing Script
7
+
8
+ client = SolanaRuby::HttpClient.new('http://127.0.0.1:8899')
9
+
10
+ # Fetch the recent blockhash
11
+ recent_blockhash = client.get_latest_blockhash["blockhash"]
12
+
13
+ # Generate a sender keypair and public key
14
+ sender_keypair = SolanaRuby::Keypair.from_private_key("d22867a84ee1d91485a52c587793002dcaa7ce79a58bb605b3af2682099bb778")
15
+ sender_pubkey = sender_keypair[:public_key]
16
+ lamports = 1 * 1_000_000_000
17
+ space = 165
18
+ balance = client.get_balance(sender_pubkey)
19
+ puts "sender account balance: #{balance}, wait for few seconds to update the balance in solana when the balance 0"
20
+
21
+
22
+ # Generate a receiver keypair and public key
23
+ new_account = SolanaRuby::Keypair.generate
24
+ new_account_pubkey = new_account[:public_key]
25
+
26
+ # create a transaction instruction
27
+ transaction = SolanaRuby::TransactionHelper.create_and_sign_transaction(sender_pubkey, new_account_pubkey, lamports, space, recent_blockhash)
28
+
29
+ signed_transaction = transaction.sign([sender_keypair])
30
+ sleep(5)
31
+ response = client.send_transaction(transaction.to_base64, { encoding: 'base64' })
32
+ puts "Response: #{response}"
@@ -0,0 +1,56 @@
1
+ Dir[File.join(File.dirname(__dir__), 'lib/solana_ruby/*.rb')].each { |file| require file }
2
+ Dir[File.join(File.dirname(__dir__), 'lib/solana_ruby/**/*.rb')].each { |file| require file }
3
+ require 'pry'
4
+
5
+ # SOL Transfer Testing Script
6
+
7
+ # Initialize the Solana client
8
+ client = SolanaRuby::HttpClient.new('http://127.0.0.1:8899')
9
+
10
+ # Fetch the recent blockhash
11
+ recent_blockhash = client.get_latest_blockhash["blockhash"]
12
+
13
+ # Generate a sender keypair and public key or Fetch payers keypair using private key
14
+ # sender_keypair = SolanaRuby::Keypair.from_private_key("InsertPrivateKeyHere")
15
+ sender_keypair = SolanaRuby::Keypair.generate
16
+ sender_pubkey = sender_keypair[:public_key]
17
+
18
+
19
+ # Airdrop some lamports to the sender's account
20
+ lamports = 10 * 1_000_000_000
21
+ sleep(1)
22
+ result = client.request_airdrop(sender_pubkey, lamports)
23
+ puts "Solana Balance #{lamports} lamports added sucessfully for the public key: #{sender_pubkey}"
24
+ sleep(10)
25
+
26
+
27
+ # Generate or existing receiver keypair and public key
28
+ keypair = SolanaRuby::Keypair.generate # generate receiver keypair
29
+ receiver_pubkey = keypair[:public_key]
30
+ # receiver_pubkey = 'InsertExistingPublicKeyHere'
31
+
32
+ transfer_lamports = 1 * 1_000_000
33
+ puts "Payer's full private key: #{sender_keypair[:full_private_key]}"
34
+ puts "Receiver's full private key: #{keypair[:full_private_key]}"
35
+ puts "Receiver's Public Key: #{keypair[:public_key]}"
36
+
37
+ # Create a new transaction
38
+ transaction = SolanaRuby::TransactionHelper.new_sol_transaction(
39
+ sender_pubkey,
40
+ receiver_pubkey,
41
+ transfer_lamports,
42
+ recent_blockhash
43
+ )
44
+
45
+ # Get the sender's private key (ensure it's a string)
46
+ private_key = sender_keypair[:private_key]
47
+ puts "Private key type: #{private_key.class}, Value: #{private_key.inspect}"
48
+
49
+ # Sign the transaction
50
+ signed_transaction = transaction.sign([sender_keypair])
51
+
52
+ # Send the transaction to the Solana network
53
+ sleep(5)
54
+ response = client.send_transaction(transaction.to_base64, { encoding: 'base64' })
55
+ puts "Response: #{response}"
56
+
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(File.dirname(__dir__), 'lib/solana_ruby/*.rb')].each { |file| require file }
4
+ Dir[File.join(File.dirname(__dir__), 'lib/solana_ruby/**/*.rb')].each { |file| require file }
5
+ # Dir["solana_ruby/*.rb"].each { |f| require_relative f.delete(".rb") }
6
+
7
+
8
+ # Testing Script
9
+
10
+ client = SolanaRuby::HttpClient.new('http://127.0.0.1:8899')
11
+
12
+ # Fetch the recent blockhash
13
+ recent_blockhash = client.get_latest_blockhash["blockhash"]
14
+
15
+ # Generate a sender keypair and public key
16
+ fee_payer = SolanaRuby::Keypair.from_private_key("d22867a84ee1d91485a52c587793002dcaa7ce79a58bb605b3af2682099bb778")
17
+ fee_payer_pubkey = fee_payer[:public_key]
18
+ lamports = 10 * 1_000_000_000
19
+ space = 165
20
+
21
+ # get balance for the fee payer
22
+ balance = client.get_balance(fee_payer_pubkey)
23
+ puts "sender account balance: #{balance}, wait for few seconds to update the balance in solana when the balance 0"
24
+
25
+
26
+ # # Generate a receiver keypair and public key
27
+ keypair = SolanaRuby::Keypair.generate
28
+ receiver_pubkey = keypair[:public_key]
29
+ transfer_lamports = 1 * 1_000_000
30
+ # puts "Payer's full private key: #{sender_keypair[:full_private_key]}"
31
+ # # puts "Receiver's full private key: #{keypair[:full_private_key]}"
32
+ # # puts "Receiver's Public Key: #{keypair[:public_key]}"
33
+ mint_address = '9BvJGQC5FkLJzUC2TmYpi1iU8n9vt2388GLT5zvu8S1G'
34
+ token_program_id = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
35
+
36
+ # Create a new transaction
37
+ transaction = SolanaRuby::TransactionHelper.new_spl_token_transaction(
38
+ "9BvJGQC5FkLJzUC2TmYpi1iU8n9vt2388GLT5zvu8S1G",
39
+ receiver_pubkey,
40
+ fee_payer_pubkey,
41
+ transfer_lamports,
42
+ recent_blockhash
43
+ )
44
+ # # Get the sender's private key (ensure it's a string)
45
+ private_key = fee_payer[:private_key]
46
+ puts "Private key type: #{private_key.class}, Value: #{private_key.inspect}"
47
+
48
+ # Sign the transaction
49
+ signed_transaction = transaction.sign([fee_payer])
50
+
51
+ # Send the transaction to the Solana network
52
+ sleep(5)
53
+ response = client.send_transaction(transaction.to_base64, { encoding: 'base64' })
54
+ puts "Response: #{response}"
55
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solana-ruby-web3js
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1.beta3
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BuildSquad
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-03 00:00:00.000000000 Z
11
+ date: 2024-11-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket-client-simple
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.2.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rbnacl
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '6.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '6.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ed25519
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: brakeman
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -201,6 +229,12 @@ files:
201
229
  - lib/solana_ruby.rb
202
230
  - lib/solana_ruby/.DS_Store
203
231
  - lib/solana_ruby/base_client.rb
232
+ - lib/solana_ruby/data_types.rb
233
+ - lib/solana_ruby/data_types/blob.rb
234
+ - lib/solana_ruby/data_types/layout.rb
235
+ - lib/solana_ruby/data_types/near_int64.rb
236
+ - lib/solana_ruby/data_types/sequence.rb
237
+ - lib/solana_ruby/data_types/unsigned_int.rb
204
238
  - lib/solana_ruby/http_client.rb
205
239
  - lib/solana_ruby/http_methods/account_methods.rb
206
240
  - lib/solana_ruby/http_methods/basic_methods.rb
@@ -211,6 +245,12 @@ files:
211
245
  - lib/solana_ruby/http_methods/slot_methods.rb
212
246
  - lib/solana_ruby/http_methods/token_methods.rb
213
247
  - lib/solana_ruby/http_methods/transaction_methods.rb
248
+ - lib/solana_ruby/keypair.rb
249
+ - lib/solana_ruby/message.rb
250
+ - lib/solana_ruby/transaction.rb
251
+ - lib/solana_ruby/transaction_helper.rb
252
+ - lib/solana_ruby/transaction_instruction.rb
253
+ - lib/solana_ruby/utils.rb
214
254
  - lib/solana_ruby/version.rb
215
255
  - lib/solana_ruby/web_socket_client.rb
216
256
  - lib/solana_ruby/web_socket_handlers.rb
@@ -219,6 +259,9 @@ files:
219
259
  - lib/solana_ruby/web_socket_methods/root_methods.rb
220
260
  - lib/solana_ruby/web_socket_methods/signature_methods.rb
221
261
  - lib/solana_ruby/web_socket_methods/slot_methods.rb
262
+ - transaction_testing/create_account.rb
263
+ - transaction_testing/sol_transfer.rb
264
+ - transaction_testing/spl_token_transfer.rb
222
265
  homepage: https://github.com/Build-Squad/solana-ruby
223
266
  licenses:
224
267
  - MIT
@@ -241,7 +284,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
284
  - !ruby/object:Gem::Version
242
285
  version: '0'
243
286
  requirements: []
244
- rubygems_version: 3.5.20
287
+ rubygems_version: 3.5.23
245
288
  signing_key:
246
289
  specification_version: 4
247
290
  summary: Solana Ruby SDK