solana-ruby-web3js 1.0.1.beta3 → 2.0.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/Gemfile.lock +7 -1
- data/README.md +72 -0
- data/lib/solana_ruby/data_types/blob.rb +30 -0
- data/lib/solana_ruby/data_types/layout.rb +27 -0
- data/lib/solana_ruby/data_types/near_int64.rb +40 -0
- data/lib/solana_ruby/data_types/sequence.rb +23 -0
- data/lib/solana_ruby/data_types/unsigned_int.rb +40 -0
- data/lib/solana_ruby/data_types.rb +25 -0
- data/lib/solana_ruby/http_client.rb +2 -1
- data/lib/solana_ruby/keypair.rb +42 -0
- data/lib/solana_ruby/message.rb +107 -0
- data/lib/solana_ruby/transaction.rb +283 -0
- data/lib/solana_ruby/transaction_helper.rb +138 -0
- data/lib/solana_ruby/transaction_instruction.rb +42 -0
- data/lib/solana_ruby/utils.rb +66 -0
- data/lib/solana_ruby/version.rb +1 -1
- data/lib/solana_ruby.rb +2 -4
- data/transaction_testing/create_account.rb +32 -0
- data/transaction_testing/sol_transfer.rb +54 -0
- data/transaction_testing/spl_token_transfer.rb +55 -0
- metadata +46 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37360e6f27ca521194022c29c3419d2dc06e5dcae0499af04892e3ca29299b0d
|
4
|
+
data.tar.gz: c5748b0ddacfaa170e79aba9a16b883c64a05ac7f776f0589c3914a5ad418356
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 683573ae5898ae932dea765b65cc4d73136f198429920be4e923da76bfed3b515e941f96761238f42dd92cedadc7d28c03f43dc3357fb07451a4940b3a2e93e0
|
7
|
+
data.tar.gz: e6c3b0e84760561c6b22290f3dab047cb041f9de7ed02d1a7cd88cc5ed285a6fb5c2ca97e7a719f709044c044b3c769aa0d0c612bb35b5894e0e70f1955e9c4e
|
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.
|
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)
|
data/README.md
CHANGED
@@ -332,3 +332,75 @@ The following methods are supported by the SolanaRuby::WebSocketClient:
|
|
332
332
|
|
333
333
|
Several methods have optional parameters where default options are defined in the client. These options can be customized or overridden when calling the methods, but if left unspecified, the client will use its internal defaults.
|
334
334
|
|
335
|
+
## Transaction Helpers
|
336
|
+
|
337
|
+
### Transfer SOL Between Accounts
|
338
|
+
|
339
|
+
To transfer SOL (the native cryptocurrency of the Solana blockchain) from one account to another, follow these steps:
|
340
|
+
|
341
|
+
#### Requirements:
|
342
|
+
|
343
|
+
- **Sender's Keypair:** Either generate a new keypair or provide the private key for an existing sender account. This keypair is used to sign the transaction.
|
344
|
+
- **Receiver's Public Key:** Specify the public key of the destination account. You can generate a new keypair for the receiver or use an existing public key.
|
345
|
+
- **Airdrop Functionality:** For Devnet or Testnet transactions, ensure that the sender's account is funded with sufficient lamports using the Solana airdrop feature.
|
346
|
+
- An initialized client to interact with the Solana blockchain.
|
347
|
+
|
348
|
+
#### Example Usage:
|
349
|
+
|
350
|
+
require 'solana_ruby'
|
351
|
+
|
352
|
+
# Initialize the client (defaults to Mainnet(https://api.mainnet-beta.solana.com))
|
353
|
+
client = SolanaRuby::HttpClient.new('https://api.devnet.solana.com')
|
354
|
+
|
355
|
+
# Fetch the recent blockhash
|
356
|
+
recent_blockhash = client.get_latest_blockhash["blockhash"]
|
357
|
+
|
358
|
+
# Generate or fetch the sender's keypair
|
359
|
+
# Option 1: Generate a new keypair
|
360
|
+
sender_keypair = SolanaRuby::Keypair.generate
|
361
|
+
# Option 2: Use an existing private key
|
362
|
+
# sender_keypair = SolanaRuby::Keypair.from_private_key("InsertPrivateKeyHere")
|
363
|
+
|
364
|
+
sender_pubkey = sender_keypair[:public_key]
|
365
|
+
|
366
|
+
|
367
|
+
# Airdrop some lamports to the sender's account
|
368
|
+
lamports = 10 * 1_000_000_000
|
369
|
+
sleep(1)
|
370
|
+
result = client.request_airdrop(sender_pubkey, lamports)
|
371
|
+
puts "Solana Balance #{lamports} lamports added sucessfully for the public key: #{sender_pubkey}"
|
372
|
+
sleep(10)
|
373
|
+
|
374
|
+
|
375
|
+
# Generate or use an existing receiver's public key
|
376
|
+
# Option 1: Generate a new keypair for the receiver
|
377
|
+
receiver_keypair = SolanaRuby::Keypair.generate # generate receiver keypair
|
378
|
+
receiver_pubkey = receiver_keypair[:public_key]
|
379
|
+
# Option 2: Use an existing public key
|
380
|
+
# receiver_pubkey = 'InsertExistingPublicKeyHere'
|
381
|
+
|
382
|
+
transfer_lamports = 1 * 1_000_000
|
383
|
+
puts "Payer's full private key: #{sender_keypair[:full_private_key]}"
|
384
|
+
puts "Receiver's full private key: #{receiver_keypair[:full_private_key]}"
|
385
|
+
puts "Receiver's Public Key: #{receiver_keypair[:public_key]}"
|
386
|
+
|
387
|
+
# Create a new transaction
|
388
|
+
transaction = SolanaRuby::TransactionHelper.new_sol_transaction(
|
389
|
+
sender_pubkey,
|
390
|
+
receiver_pubkey,
|
391
|
+
transfer_lamports,
|
392
|
+
recent_blockhash
|
393
|
+
)
|
394
|
+
|
395
|
+
# Get the sender's private key (ensure it's a string)
|
396
|
+
private_key = sender_keypair[:private_key]
|
397
|
+
puts "Private key type: #{private_key.class}, Value: #{private_key.inspect}"
|
398
|
+
|
399
|
+
# Sign the transaction
|
400
|
+
signed_transaction = transaction.sign([sender_keypair])
|
401
|
+
|
402
|
+
# Send the transaction to the Solana network
|
403
|
+
sleep(5)
|
404
|
+
response = client.send_transaction(transaction.to_base64, { encoding: 'base64' })
|
405
|
+
puts "Response: #{response}"
|
406
|
+
|
@@ -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
|
-
|
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,283 @@
|
|
1
|
+
Dir[File.join(__dir__, 'data_types', '*.rb')].each { |file| require file }
|
2
|
+
|
3
|
+
module SolanaRuby
|
4
|
+
class Transaction
|
5
|
+
require 'rbnacl'
|
6
|
+
SIGNATURE_LENGTH = 64
|
7
|
+
PACKET_DATA_SIZE = 1280 - 40 - 8
|
8
|
+
DEFAULT_SIGNATURE = Array.new(64, 0)
|
9
|
+
|
10
|
+
attr_accessor :instructions, :signatures, :fee_payer, :recent_blockhash, :message
|
11
|
+
|
12
|
+
def initialize(recent_blockhash: nil, signatures: [], instructions: [], fee_payer: nil)
|
13
|
+
@recent_blockhash = recent_blockhash
|
14
|
+
@signatures = signatures
|
15
|
+
@instructions = instructions
|
16
|
+
@fee_payer = fee_payer
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_instruction(instruction)
|
20
|
+
@instructions << instruction
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_fee_payer(pubkey)
|
24
|
+
puts "Setting fee payer: #{pubkey.inspect}" # Debugging output
|
25
|
+
@fee_payer = pubkey # Store as-is since Base58 gem can handle encoding/decoding
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_recent_blockhash(blockhash)
|
29
|
+
# raise "Invalid Base58 blockhash" unless Base58.valid?(blockhash)
|
30
|
+
@recent_blockhash = blockhash # Store as-is for similar reasons
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.from(base64_string)
|
34
|
+
bytes = Base64.decode64(base64_string).bytes
|
35
|
+
signature_count = Utils.decode_length(bytes)
|
36
|
+
signatures = signature_count.times.map do
|
37
|
+
signature_bytes = bytes.slice!(0, SIGNATURE_LENGTH)
|
38
|
+
Utils.bytes_to_base58(signature_bytes)
|
39
|
+
end
|
40
|
+
msg = Message.from(bytes)
|
41
|
+
self.populate(msg, signatures)
|
42
|
+
end
|
43
|
+
|
44
|
+
def serialize
|
45
|
+
sign_data = serialize_message
|
46
|
+
|
47
|
+
signature_count = Utils.encode_length(signatures.length)
|
48
|
+
raise 'invalid length!' if signatures.length > 256
|
49
|
+
|
50
|
+
wire_transaction = signature_count
|
51
|
+
|
52
|
+
signatures.each do |signature|
|
53
|
+
if signature
|
54
|
+
signature_bytes = signature[:signature]
|
55
|
+
raise 'signature is empty' unless (signature_bytes)
|
56
|
+
raise 'signature has invalid length' unless (signature_bytes.length == 64)
|
57
|
+
wire_transaction += signature_bytes
|
58
|
+
raise "Transaction too large: #{wire_transaction.length} > #{PACKET_DATA_SIZE}" unless wire_transaction.length <= PACKET_DATA_SIZE
|
59
|
+
wire_transaction
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
wire_transaction += sign_data
|
64
|
+
wire_transaction
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_base64
|
68
|
+
Base64.strict_encode64(serialize.pack('C*'))
|
69
|
+
end
|
70
|
+
|
71
|
+
def add(item)
|
72
|
+
instructions.push(item)
|
73
|
+
end
|
74
|
+
|
75
|
+
def sign(keys)
|
76
|
+
raise 'No signers' unless keys.any?
|
77
|
+
|
78
|
+
keys = keys.uniq{ |k| key[:public_key] }
|
79
|
+
@signatures = keys.map do |key|
|
80
|
+
{
|
81
|
+
signature: nil,
|
82
|
+
public_key: key[:public_key]
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
message = compile_message
|
87
|
+
partial_sign(message, keys)
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def serialize_message
|
94
|
+
compile.serialize
|
95
|
+
end
|
96
|
+
|
97
|
+
def compile
|
98
|
+
message = compile_message
|
99
|
+
signed_keys = message.account_keys.slice(0, message.header[:num_required_signatures])
|
100
|
+
|
101
|
+
if signatures.length == signed_keys.length
|
102
|
+
valid = signatures.each_with_index.all?{|pair, i| signed_keys[i] == pair[:public_key]}
|
103
|
+
return message if valid
|
104
|
+
end
|
105
|
+
|
106
|
+
@signatures = signed_keys.map do |public_key|
|
107
|
+
{
|
108
|
+
signature: nil,
|
109
|
+
public_key: public_key
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
message
|
114
|
+
end
|
115
|
+
|
116
|
+
def compile_message
|
117
|
+
check_for_errors
|
118
|
+
fetch_message_data
|
119
|
+
message = Message.new(
|
120
|
+
header: {
|
121
|
+
num_required_signatures: @num_required_signatures,
|
122
|
+
num_readonly_signed_accounts: @num_readonly_signed_accounts,
|
123
|
+
num_readonly_unsigned_accounts: @num_readonly_unsigned_accounts,
|
124
|
+
},
|
125
|
+
account_keys: @account_keys, recent_blockhash: recent_blockhash, instructions: @instructs
|
126
|
+
)
|
127
|
+
message
|
128
|
+
end
|
129
|
+
|
130
|
+
def check_for_errors
|
131
|
+
raise 'Transaction recent_blockhash required' unless recent_blockhash
|
132
|
+
|
133
|
+
puts 'No instructions provided' if instructions.length < 1
|
134
|
+
|
135
|
+
if fee_payer.nil? && signatures.length > 0 && signatures[0][:public_key]
|
136
|
+
@fee_payer = signatures[0][:public_key] if (signatures.length > 0 && signatures[0][:public_key])
|
137
|
+
end
|
138
|
+
|
139
|
+
raise('Transaction fee payer required') if @fee_payer.nil?
|
140
|
+
|
141
|
+
instructions.each_with_index do |instruction, i|
|
142
|
+
raise("Transaction instruction index #{i} has undefined program id") unless instruction.program_id
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def fetch_message_data
|
147
|
+
program_ids = []
|
148
|
+
account_metas= []
|
149
|
+
|
150
|
+
instructions.each do |instruction|
|
151
|
+
account_metas += instruction.keys
|
152
|
+
program_ids.push(instruction.program_id) unless program_ids.include?(instruction.program_id)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Append programID account metas
|
156
|
+
append_program_id(program_ids, account_metas)
|
157
|
+
|
158
|
+
# Sort. Prioritizing first by signer, then by writable
|
159
|
+
signer_order(account_metas)
|
160
|
+
|
161
|
+
# Cull duplicate account metas
|
162
|
+
unique_metas = []
|
163
|
+
add_unique_meta_data(unique_metas, account_metas)
|
164
|
+
|
165
|
+
add_fee_payer_meta(unique_metas)
|
166
|
+
|
167
|
+
# Disallow unknown signers
|
168
|
+
disallow_signers(signatures, unique_metas)
|
169
|
+
|
170
|
+
# Split out signing from non-signing keys and count header values
|
171
|
+
signed_keys = []
|
172
|
+
unsigned_keys = []
|
173
|
+
header_params = split_keys(unique_metas, signed_keys, unsigned_keys)
|
174
|
+
@account_keys = signed_keys + unsigned_keys
|
175
|
+
|
176
|
+
# add instruction structure
|
177
|
+
@instructs = add_instructs
|
178
|
+
end
|
179
|
+
|
180
|
+
def append_program_id(program_ids, account_metas)
|
181
|
+
program_ids.each do |programId|
|
182
|
+
account_metas.push({
|
183
|
+
pubkey: programId,
|
184
|
+
is_signer: false,
|
185
|
+
is_writable: false,
|
186
|
+
})
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def signer_order(account_metas)
|
191
|
+
account_metas.sort! do |x, y|
|
192
|
+
check_signer = x[:is_signer] == y[:is_signer] ? nil : x[:is_signer] ? -1 : 1
|
193
|
+
check_writable = x[:is_writable] == y[:is_writable] ? nil : (x[:is_writable] ? -1 : 1)
|
194
|
+
(check_signer || check_writable) || 0
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def add_unique_meta_data(unique_metas, account_metas)
|
199
|
+
account_metas.each do |account_meta|
|
200
|
+
pubkey_string = account_meta[:pubkey]
|
201
|
+
unique_index = unique_metas.find_index{|x| x[:pubkey] == pubkey_string }
|
202
|
+
if unique_index
|
203
|
+
unique_metas[unique_index][:is_writable] = unique_metas[unique_index][:is_writable] || account_meta[:is_writable]
|
204
|
+
else
|
205
|
+
unique_metas.push(account_meta);
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def add_fee_payer_meta(unique_metas)
|
211
|
+
# Move fee payer to the front
|
212
|
+
fee_payer_index = unique_metas.find_index { |x| x[:pubkey] == fee_payer }
|
213
|
+
if fee_payer_index
|
214
|
+
payer_meta = unique_metas.delete_at(fee_payer_index)
|
215
|
+
payer_meta[:is_signer] = true
|
216
|
+
payer_meta[:is_writable] = true
|
217
|
+
unique_metas.unshift(payer_meta)
|
218
|
+
else
|
219
|
+
unique_metas.unshift({
|
220
|
+
pubkey: fee_payer,
|
221
|
+
is_signer: true,
|
222
|
+
is_writable: true,
|
223
|
+
})
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def disallow_signers(signatures, unique_metas)
|
228
|
+
signatures.each do |signature|
|
229
|
+
unique_index = unique_metas.find_index{ |x| x[:pubkey] == signature[:public_key] }
|
230
|
+
|
231
|
+
if unique_index
|
232
|
+
unique_metas[unique_index][:is_signer] = true unless unique_metas[unique_index][:is_signer]
|
233
|
+
else
|
234
|
+
raise "unknown signer: #{signature[:public_key]}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def add_instructs
|
240
|
+
instructions.map do |instruction|
|
241
|
+
{
|
242
|
+
program_id_index: @account_keys.index(instruction.program_id),
|
243
|
+
accounts: instruction.keys.map { |meta| @account_keys.index(meta[:pubkey]) },
|
244
|
+
data: instruction.data
|
245
|
+
}
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def split_keys(unique_metas, signed_keys, unsigned_keys)
|
250
|
+
@num_required_signatures = 0
|
251
|
+
@num_readonly_signed_accounts = 0
|
252
|
+
@num_readonly_unsigned_accounts = 0
|
253
|
+
unique_metas.each do |meta|
|
254
|
+
if meta[:is_signer]
|
255
|
+
signed_keys.push(meta[:pubkey])
|
256
|
+
@num_required_signatures += 1
|
257
|
+
@num_readonly_signed_accounts += 1 if (!meta[:is_writable])
|
258
|
+
else
|
259
|
+
unsigned_keys.push(meta[:pubkey])
|
260
|
+
@num_readonly_unsigned_accounts += 1 if (!meta[:is_writable])
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def partial_sign(message, keys)
|
266
|
+
sign_data = message.serialize
|
267
|
+
keys.each do |key|
|
268
|
+
private_key_bytes = [key[:private_key]].pack('H*')
|
269
|
+
signing_key = RbNaCl::Signatures::Ed25519::SigningKey.new(private_key_bytes)
|
270
|
+
signature = signing_key.sign(sign_data.pack('C*')).bytes
|
271
|
+
add_signature(key[:public_key], signature)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def add_signature(pubkey, signature)
|
276
|
+
raise 'error' unless signature.length === 64
|
277
|
+
index = signatures.find_index{|s| s[:public_key] == pubkey}
|
278
|
+
raise "unknown signer: #{pubkey}" unless index
|
279
|
+
|
280
|
+
@signatures[index][:signature] = signature
|
281
|
+
end
|
282
|
+
end
|
283
|
+
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
|
data/lib/solana_ruby/version.rb
CHANGED
data/lib/solana_ruby.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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,54 @@
|
|
1
|
+
require 'pry'
|
2
|
+
|
3
|
+
# SOL Transfer Testing Script
|
4
|
+
|
5
|
+
# Initialize the Solana client
|
6
|
+
client = SolanaRuby::HttpClient.new('http://127.0.0.1:8899')
|
7
|
+
|
8
|
+
# Fetch the recent blockhash
|
9
|
+
recent_blockhash = client.get_latest_blockhash["blockhash"]
|
10
|
+
|
11
|
+
# Generate a sender keypair and public key or Fetch payers keypair using private key
|
12
|
+
# sender_keypair = SolanaRuby::Keypair.from_private_key("InsertPrivateKeyHere")
|
13
|
+
sender_keypair = SolanaRuby::Keypair.generate
|
14
|
+
sender_pubkey = sender_keypair[:public_key]
|
15
|
+
|
16
|
+
|
17
|
+
# Airdrop some lamports to the sender's account
|
18
|
+
lamports = 10 * 1_000_000_000
|
19
|
+
sleep(1)
|
20
|
+
result = client.request_airdrop(sender_pubkey, lamports)
|
21
|
+
puts "Solana Balance #{lamports} lamports added sucessfully for the public key: #{sender_pubkey}"
|
22
|
+
sleep(10)
|
23
|
+
|
24
|
+
|
25
|
+
# Generate or existing receiver keypair and public key
|
26
|
+
keypair = SolanaRuby::Keypair.generate # generate receiver keypair
|
27
|
+
receiver_pubkey = keypair[:public_key]
|
28
|
+
# receiver_pubkey = 'InsertExistingPublicKeyHere'
|
29
|
+
|
30
|
+
transfer_lamports = 1 * 1_000_000
|
31
|
+
puts "Payer's full private key: #{sender_keypair[:full_private_key]}"
|
32
|
+
puts "Receiver's full private key: #{keypair[:full_private_key]}"
|
33
|
+
puts "Receiver's Public Key: #{keypair[:public_key]}"
|
34
|
+
|
35
|
+
# Create a new transaction
|
36
|
+
transaction = SolanaRuby::TransactionHelper.new_sol_transaction(
|
37
|
+
sender_pubkey,
|
38
|
+
receiver_pubkey,
|
39
|
+
transfer_lamports,
|
40
|
+
recent_blockhash
|
41
|
+
)
|
42
|
+
|
43
|
+
# Get the sender's private key (ensure it's a string)
|
44
|
+
private_key = sender_keypair[:private_key]
|
45
|
+
puts "Private key type: #{private_key.class}, Value: #{private_key.inspect}"
|
46
|
+
|
47
|
+
# Sign the transaction
|
48
|
+
signed_transaction = transaction.sign([sender_keypair])
|
49
|
+
|
50
|
+
# Send the transaction to the Solana network
|
51
|
+
sleep(5)
|
52
|
+
response = client.send_transaction(transaction.to_base64, { encoding: 'base64' })
|
53
|
+
puts "Response: #{response}"
|
54
|
+
|
@@ -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:
|
4
|
+
version: 2.0.0beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- BuildSquad
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
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.
|
287
|
+
rubygems_version: 3.5.23
|
245
288
|
signing_key:
|
246
289
|
specification_version: 4
|
247
290
|
summary: Solana Ruby SDK
|