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 +4 -4
- data/.DS_Store +0 -0
- data/Gemfile.lock +7 -1
- 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 +281 -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 +56 -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: 9f614bab0b21438c2820d0035a0aa33567a11fedd3ecb998666b39e5ab28681d
|
4
|
+
data.tar.gz: c63fd671d6328732c39994032f94aff219ff77eda8cd899c5d1f0c29f86f4524
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
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
|
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,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:
|
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-
|
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
|