solana-ruby-web3js 1.0.1.beta4 → 2.0.0beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -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 +29 -0
- data/lib/solana_ruby/http_client.rb +2 -1
- data/lib/solana_ruby/keypair.rb +8 -5
- data/lib/solana_ruby/message.rb +107 -0
- data/lib/solana_ruby/transaction.rb +234 -55
- data/lib/solana_ruby/transaction_helper.rb +132 -9
- data/lib/solana_ruby/utils.rb +66 -0
- data/lib/solana_ruby/version.rb +1 -1
- data/transaction_testing/create_account.rb +57 -0
- data/transaction_testing/sol_transfer.rb +56 -0
- data/transaction_testing/spl_token_transfer.rb +55 -0
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6bb6650de7a5beae14649fe9cfac20973923d0be4c4f74e9d1e8f373e70842b9
|
4
|
+
data.tar.gz: c3ad4316d61d4326327bb281a3eb64991011ae14d51a7974de6eaf9705d4ffa5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94c8304f1d05d7cbd8ec22947d98ede21c1892b210ad5cdb43f3e6293f4c6ab288c8aa83b1ba6e40a28d08a191a32ba775966eeb370631d02112cac981309fb6
|
7
|
+
data.tar.gz: '0085b47b4f44786e0bd0af77827a12cbc191744a16aff61448cafff641a53efa798677ef51b3748966f76547b883fd767d3f48ffe5297f8519cc08eb1ee81e90'
|
data/Gemfile.lock
CHANGED
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 Mainnet, 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 when needed.
|
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
|
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.sol_transfer(
|
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-bit unsigned integer
|
8
|
+
32 => { directive: 'L<', size: 4 }, # 32-bit little-endian unsigned integer
|
9
|
+
64 => { directive: 'Q<', size: 8 } # 64-bit little-endian unsigned integer
|
10
|
+
}
|
11
|
+
|
12
|
+
def initialize(bits)
|
13
|
+
@bits = bits
|
14
|
+
type = BITS[@bits]
|
15
|
+
raise "Unsupported size. Supported sizes: #{BITS.keys.join(', ')} bits" unless type
|
16
|
+
@size = type[:size]
|
17
|
+
@directive = type[:directive]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Serialize the unsigned integer into properly aligned bytes
|
21
|
+
def serialize(obj)
|
22
|
+
raise "Can only serialize integers" unless obj.is_a?(Integer)
|
23
|
+
raise "Cannot serialize negative integers" if obj.negative?
|
24
|
+
|
25
|
+
if obj >= 256**@size
|
26
|
+
raise "Integer too large to 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 (expected #{@size} bytes, got #{bytes.size})" if bytes.size != @size
|
35
|
+
|
36
|
+
bytes.pack('C*').unpack(@directive).first
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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
|
+
|
25
|
+
def blob32
|
26
|
+
Blob.new(32)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
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 = {
|
data/lib/solana_ruby/keypair.rb
CHANGED
@@ -7,12 +7,14 @@ module SolanaRuby
|
|
7
7
|
def self.generate
|
8
8
|
signing_key = RbNaCl::Signatures::Ed25519::SigningKey.generate
|
9
9
|
public_key_bytes = signing_key.verify_key.to_bytes # Binary format for public key
|
10
|
-
|
10
|
+
private_key_bytes = signing_key.to_bytes
|
11
|
+
private_key_hex = private_key_bytes.unpack1('H*') # Hex format for private key
|
11
12
|
|
12
13
|
# Convert public key binary to Base58 for readability and compatibility
|
13
14
|
{
|
14
|
-
public_key: Base58.binary_to_base58(public_key_bytes),
|
15
|
-
private_key: private_key_hex
|
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)
|
16
18
|
}
|
17
19
|
end
|
18
20
|
|
@@ -31,8 +33,9 @@ module SolanaRuby
|
|
31
33
|
|
32
34
|
# Return public key in Base58 format and private key in hex format
|
33
35
|
{
|
34
|
-
public_key: Base58.binary_to_base58(public_key_bytes),
|
35
|
-
private_key: private_key_hex
|
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)
|
36
39
|
}
|
37
40
|
end
|
38
41
|
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
|
@@ -1,14 +1,19 @@
|
|
1
|
+
Dir[File.join(__dir__, 'data_types', '*.rb')].each { |file| require file }
|
2
|
+
|
1
3
|
module SolanaRuby
|
2
4
|
class Transaction
|
3
5
|
require 'rbnacl'
|
6
|
+
SIGNATURE_LENGTH = 64
|
7
|
+
PACKET_DATA_SIZE = 1280 - 40 - 8
|
8
|
+
DEFAULT_SIGNATURE = Array.new(64, 0)
|
4
9
|
|
5
|
-
attr_accessor :instructions, :signatures, :fee_payer, :recent_blockhash
|
10
|
+
attr_accessor :instructions, :signatures, :fee_payer, :recent_blockhash, :message
|
6
11
|
|
7
|
-
def initialize
|
8
|
-
@
|
9
|
-
@signatures =
|
10
|
-
@
|
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
|
12
17
|
end
|
13
18
|
|
14
19
|
def add_instruction(instruction)
|
@@ -17,92 +22,266 @@ module SolanaRuby
|
|
17
22
|
|
18
23
|
def set_fee_payer(pubkey)
|
19
24
|
puts "Setting fee payer: #{pubkey.inspect}" # Debugging output
|
20
|
-
unless Base58.valid?(pubkey)
|
21
|
-
raise "Invalid Base58 public key for fee payer: #{pubkey.inspect}"
|
22
|
-
end
|
23
25
|
@fee_payer = pubkey # Store as-is since Base58 gem can handle encoding/decoding
|
24
26
|
end
|
25
27
|
|
26
28
|
def set_recent_blockhash(blockhash)
|
27
|
-
raise "Invalid Base58 blockhash" unless Base58.valid?(blockhash)
|
29
|
+
# raise "Invalid Base58 blockhash" unless Base58.valid?(blockhash)
|
28
30
|
@recent_blockhash = blockhash # Store as-is for similar reasons
|
29
31
|
end
|
30
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
|
+
|
31
44
|
def serialize
|
32
|
-
|
33
|
-
raise "Fee payer not set" if @fee_payer.nil?
|
45
|
+
sign_data = serialize_message
|
34
46
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
47
|
+
signature_count = Utils.encode_length(signatures.length)
|
48
|
+
raise 'invalid length!' if signatures.length > 256
|
49
|
+
|
50
|
+
wire_transaction = signature_count
|
39
51
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
44
61
|
end
|
45
62
|
|
46
|
-
|
47
|
-
|
63
|
+
wire_transaction += sign_data
|
64
|
+
wire_transaction
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_base64
|
68
|
+
Base64.strict_encode64(serialize.pack('C*'))
|
69
|
+
end
|
48
70
|
|
49
|
-
|
71
|
+
def add(item)
|
72
|
+
instructions.push(item)
|
50
73
|
end
|
51
74
|
|
52
|
-
def sign(
|
53
|
-
|
54
|
-
signing_key = RbNaCl::Signatures::Ed25519::SigningKey.new(private_key_bytes)
|
75
|
+
def sign(keypairs)
|
76
|
+
raise 'No signers' unless keypairs.any?
|
55
77
|
|
56
|
-
|
57
|
-
|
78
|
+
keys = keypairs.uniq { |kp| kp[:public_key] }
|
79
|
+
@signatures = keys.map do |key|
|
80
|
+
{
|
81
|
+
signature: nil,
|
82
|
+
public_key: key[:public_key]
|
83
|
+
}
|
84
|
+
end
|
58
85
|
|
59
|
-
|
60
|
-
|
86
|
+
message = compile_message
|
87
|
+
partial_sign(message, keys)
|
88
|
+
true
|
61
89
|
end
|
62
90
|
|
63
91
|
private
|
64
92
|
|
65
93
|
def serialize_message
|
66
|
-
|
94
|
+
compile.serialize
|
95
|
+
end
|
67
96
|
|
68
|
-
|
69
|
-
|
70
|
-
|
97
|
+
def compile
|
98
|
+
message = compile_message
|
99
|
+
signed_keys = message.account_keys.slice(0, message.header[:num_required_signatures])
|
71
100
|
|
72
|
-
|
73
|
-
|
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
|
74
104
|
end
|
75
105
|
|
76
|
-
|
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
|
+
|
120
|
+
# add instruction structure
|
121
|
+
instructs = add_instructs
|
122
|
+
|
123
|
+
message_data = Message.new(
|
124
|
+
header: @header,
|
125
|
+
account_keys: @account_keys,
|
126
|
+
recent_blockhash: recent_blockhash,
|
127
|
+
instructions: instructs
|
128
|
+
)
|
129
|
+
message_data
|
130
|
+
end
|
131
|
+
|
132
|
+
def check_for_errors
|
133
|
+
raise 'Transaction recent_blockhash required' unless recent_blockhash
|
77
134
|
|
78
|
-
|
79
|
-
|
135
|
+
puts 'No instructions provided' if instructions.length < 1
|
136
|
+
|
137
|
+
if fee_payer.nil? && signatures.length > 0 && signatures[0][:public_key]
|
138
|
+
@fee_payer = signatures[0][:public_key] if (signatures.length > 0 && signatures[0][:public_key])
|
80
139
|
end
|
140
|
+
|
141
|
+
raise('Transaction fee payer required') if @fee_payer.nil?
|
81
142
|
|
82
|
-
|
143
|
+
instructions.each_with_index do |instruction, i|
|
144
|
+
raise("Transaction instruction index #{i} has undefined program id") unless instruction.program_id
|
145
|
+
end
|
83
146
|
end
|
84
147
|
|
85
|
-
def
|
86
|
-
|
87
|
-
|
148
|
+
def fetch_message_data
|
149
|
+
program_ids = []
|
150
|
+
account_metas= []
|
151
|
+
|
152
|
+
instructions.each do |instruction|
|
153
|
+
account_metas += instruction.keys
|
154
|
+
program_ids.push(instruction.program_id) unless program_ids.include?(instruction.program_id)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Append programID account metas
|
158
|
+
append_program_id(program_ids, account_metas)
|
159
|
+
|
160
|
+
# Sort. Prioritizing first by signer, then by writable
|
161
|
+
signer_order(account_metas)
|
162
|
+
|
163
|
+
# Cull duplicate account metas
|
164
|
+
unique_metas = []
|
165
|
+
add_unique_meta_data(unique_metas, account_metas)
|
88
166
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
167
|
+
add_fee_payer_meta(unique_metas)
|
168
|
+
|
169
|
+
# Disallow unknown signers
|
170
|
+
disallow_signers(signatures, unique_metas)
|
171
|
+
|
172
|
+
# Split out signing from non-signing keys and count header values
|
173
|
+
signed_keys = []
|
174
|
+
unsigned_keys = []
|
175
|
+
@header = split_keys(unique_metas, signed_keys, unsigned_keys)
|
176
|
+
@account_keys = signed_keys + unsigned_keys
|
177
|
+
end
|
178
|
+
|
179
|
+
def append_program_id(program_ids, account_metas)
|
180
|
+
program_ids.each do |programId|
|
181
|
+
account_metas.push({
|
182
|
+
pubkey: programId,
|
183
|
+
is_signer: false,
|
184
|
+
is_writable: false,
|
185
|
+
})
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def signer_order(account_metas)
|
190
|
+
account_metas.sort! do |x, y|
|
191
|
+
check_signer = x[:is_signer] == y[:is_signer] ? nil : x[:is_signer] ? -1 : 1
|
192
|
+
check_writable = x[:is_writable] == y[:is_writable] ? nil : (x[:is_writable] ? -1 : 1)
|
193
|
+
(check_signer || check_writable) || 0
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_unique_meta_data(unique_metas, account_metas)
|
198
|
+
account_metas.each do |account_meta|
|
199
|
+
pubkey_string = account_meta[:pubkey]
|
200
|
+
unique_index = unique_metas.find_index{|x| x[:pubkey] == pubkey_string }
|
201
|
+
if unique_index
|
202
|
+
unique_metas[unique_index][:is_writable] = unique_metas[unique_index][:is_writable] || account_meta[:is_writable]
|
203
|
+
else
|
204
|
+
unique_metas.push(account_meta);
|
93
205
|
end
|
94
206
|
end
|
207
|
+
end
|
95
208
|
|
96
|
-
|
209
|
+
def add_fee_payer_meta(unique_metas)
|
210
|
+
# Move fee payer to the front
|
211
|
+
fee_payer_index = unique_metas.find_index { |x| x[:pubkey] == fee_payer }
|
212
|
+
if fee_payer_index
|
213
|
+
payer_meta = unique_metas.delete_at(fee_payer_index)
|
214
|
+
payer_meta[:is_signer] = true
|
215
|
+
payer_meta[:is_writable] = true
|
216
|
+
unique_metas.unshift(payer_meta)
|
217
|
+
else
|
218
|
+
unique_metas.unshift({
|
219
|
+
pubkey: fee_payer,
|
220
|
+
is_signer: true,
|
221
|
+
is_writable: true,
|
222
|
+
})
|
223
|
+
end
|
97
224
|
end
|
98
|
-
end
|
99
|
-
end
|
100
225
|
|
101
|
-
|
102
|
-
|
226
|
+
def disallow_signers(signatures, unique_metas)
|
227
|
+
signatures.each do |signature|
|
228
|
+
unique_index = unique_metas.find_index{ |x| x[:pubkey] == signature[:public_key] }
|
103
229
|
|
104
|
-
|
105
|
-
|
106
|
-
|
230
|
+
if unique_index
|
231
|
+
unique_metas[unique_index][:is_signer] = true unless unique_metas[unique_index][:is_signer]
|
232
|
+
else
|
233
|
+
raise "unknown signer: #{signature[:public_key]}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def add_instructs
|
239
|
+
instructions.map do |instruction|
|
240
|
+
{
|
241
|
+
program_id_index: @account_keys.index(instruction.program_id),
|
242
|
+
accounts: instruction.keys.map { |meta| @account_keys.index(meta[:pubkey]) },
|
243
|
+
data: instruction.data
|
244
|
+
}
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def split_keys(unique_metas, signed_keys, unsigned_keys)
|
249
|
+
num_required_signatures = 0
|
250
|
+
num_readonly_signed_accounts = 0
|
251
|
+
num_readonly_unsigned_accounts = 0
|
252
|
+
unique_metas.each do |meta|
|
253
|
+
if meta[:is_signer]
|
254
|
+
signed_keys.push(meta[:pubkey])
|
255
|
+
num_required_signatures += 1
|
256
|
+
num_readonly_signed_accounts += 1 if (!meta[:is_writable])
|
257
|
+
else
|
258
|
+
unsigned_keys.push(meta[:pubkey])
|
259
|
+
num_readonly_unsigned_accounts += 1 if (!meta[:is_writable])
|
260
|
+
end
|
261
|
+
end
|
262
|
+
{
|
263
|
+
num_required_signatures: num_required_signatures,
|
264
|
+
num_readonly_signed_accounts: num_readonly_signed_accounts,
|
265
|
+
num_readonly_unsigned_accounts: num_readonly_unsigned_accounts,
|
266
|
+
}
|
267
|
+
end
|
268
|
+
|
269
|
+
def partial_sign(message, keys)
|
270
|
+
sign_data = message.serialize
|
271
|
+
keys.each do |key|
|
272
|
+
private_key_bytes = [key[:private_key]].pack('H*')
|
273
|
+
signing_key = RbNaCl::Signatures::Ed25519::SigningKey.new(private_key_bytes)
|
274
|
+
signature = signing_key.sign(sign_data.pack('C*')).bytes
|
275
|
+
add_signature(key[:public_key], signature)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def add_signature(pubkey, signature)
|
280
|
+
raise 'error' unless signature.length === 64
|
281
|
+
index = signatures.find_index{|s| s[:public_key] == pubkey}
|
282
|
+
raise "unknown signer: #{pubkey}" unless index
|
283
|
+
|
284
|
+
@signatures[index][:signature] = signature
|
285
|
+
end
|
107
286
|
end
|
108
287
|
end
|
@@ -1,29 +1,152 @@
|
|
1
1
|
module SolanaRuby
|
2
2
|
class TransactionHelper
|
3
3
|
require 'base58'
|
4
|
-
|
4
|
+
require 'pry'
|
5
5
|
|
6
|
-
|
7
|
-
|
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: :uint32,
|
25
|
+
lamports: :uint64,
|
26
|
+
space: :uint64,
|
27
|
+
program_id: :blob32
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
# Method to create a system account (e.g., for SPL token or SOL)
|
32
|
+
def self.account_instruction(from_pubkey, new_account_pubkey, lamports, space, program_id)
|
33
|
+
# Encode the instruction data
|
34
|
+
instruction_data = encode_data(
|
35
|
+
INSTRUCTION_LAYOUTS[:create_account],
|
36
|
+
{
|
37
|
+
instruction: 0, # '0' corresponds to the Create Account instruction
|
38
|
+
lamports: lamports, # The amount of lamports to transfer to the new account
|
39
|
+
space: space, # Amount of space allocated for the account's data
|
40
|
+
program_id: Base58.base58_to_binary(program_id, :bitcoin).bytes # Convert public key to binary
|
41
|
+
}
|
42
|
+
)
|
43
|
+
|
44
|
+
# Construct the transaction instruction
|
45
|
+
create_account_instruction = TransactionInstruction.new(
|
46
|
+
keys: [
|
47
|
+
{ pubkey: from_pubkey, is_signer: true, is_writable: true }, # Funder's account
|
48
|
+
{ pubkey: new_account_pubkey, is_signer: true, is_writable: true } # New account
|
49
|
+
],
|
50
|
+
program_id: program_id, # Use Solana's system program for account creation
|
51
|
+
data: instruction_data # Encoded instruction data
|
52
|
+
)
|
53
|
+
|
54
|
+
# return instruction data
|
55
|
+
create_account_instruction
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def self.create_account(from_pubkey, new_account_pubkey, lamports, space, recent_blockhash, program_id = SYSTEM_PROGRAM_ID)
|
60
|
+
# Create the transaction
|
61
|
+
transaction = Transaction.new
|
62
|
+
transaction.set_fee_payer(from_pubkey)
|
63
|
+
transaction.set_recent_blockhash(recent_blockhash)
|
64
|
+
|
65
|
+
# Add the create account instruction to the transaction
|
66
|
+
instruction = account_instruction(from_pubkey, new_account_pubkey, lamports, space, program_id)
|
67
|
+
transaction.add_instruction(instruction)
|
68
|
+
|
69
|
+
# return the transaction for signing
|
70
|
+
transaction
|
71
|
+
end
|
72
|
+
|
73
|
+
# Method to create a SOL transfer instruction
|
74
|
+
def self.transfer_sol_instruction(from_pubkey, to_pubkey, lamports)
|
75
|
+
fields = INSTRUCTION_LAYOUTS[:sol_transfer]
|
76
|
+
data = encode_data(fields, { instruction: 2, lamports: lamports })
|
77
|
+
TransactionInstruction.new(
|
8
78
|
keys: [
|
9
79
|
{ pubkey: from_pubkey, is_signer: true, is_writable: true },
|
10
80
|
{ pubkey: to_pubkey, is_signer: false, is_writable: true }
|
11
81
|
],
|
12
|
-
program_id:
|
13
|
-
data:
|
82
|
+
program_id: SYSTEM_PROGRAM_ID,
|
83
|
+
data: data
|
14
84
|
)
|
15
|
-
transfer_instruction
|
16
85
|
end
|
17
86
|
|
18
|
-
# Helper to
|
19
|
-
def self.
|
87
|
+
# Helper to create a new transaction for SOL transfer
|
88
|
+
def self.sol_transfer(from_pubkey, to_pubkey, lamports, recent_blockhash)
|
20
89
|
transaction = Transaction.new
|
21
90
|
transaction.set_fee_payer(from_pubkey)
|
22
91
|
transaction.set_recent_blockhash(recent_blockhash)
|
92
|
+
transfer_instruction = transfer_sol_instruction(from_pubkey, to_pubkey, lamports)
|
93
|
+
transaction.add_instruction(transfer_instruction)
|
94
|
+
transaction
|
95
|
+
end
|
23
96
|
|
24
|
-
|
97
|
+
# Method to create an SPL token transfer instruction
|
98
|
+
def self.transfer_spl_token(source, destination, owner, amount)
|
99
|
+
fields = INSTRUCTION_LAYOUTS[:spl_transfer]
|
100
|
+
data = encode_data(fields, { instruction: 3, amount: amount }) # Instruction type 3: Transfer tokens
|
101
|
+
TransactionInstruction.new(
|
102
|
+
keys: [
|
103
|
+
{ pubkey: source, is_signer: false, is_writable: true },
|
104
|
+
{ pubkey: destination, is_signer: false, is_writable: true },
|
105
|
+
{ pubkey: owner, is_signer: true, is_writable: false }
|
106
|
+
],
|
107
|
+
program_id: TOKEN_PROGRAM_ID,
|
108
|
+
data: data
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Helper to create a new transaction for SPL token transfer
|
113
|
+
def self.new_spl_token_transaction(source, destination, owner, amount, recent_blockhash)
|
114
|
+
transaction = Transaction.new
|
115
|
+
transaction.set_fee_payer(owner)
|
116
|
+
transaction.set_recent_blockhash(recent_blockhash)
|
117
|
+
transfer_instruction = transfer_spl_token(source, destination, owner, amount)
|
25
118
|
transaction.add_instruction(transfer_instruction)
|
26
119
|
transaction
|
27
120
|
end
|
121
|
+
|
122
|
+
# Method to create an associated token account for a given token mint
|
123
|
+
def self.create_associated_token_account(payer, mint, owner)
|
124
|
+
data = [0, 0, 0, 0] # No data required for account creation
|
125
|
+
create_account_instruction = TransactionInstruction.new(
|
126
|
+
keys: [
|
127
|
+
{ pubkey: payer, is_signer: true, is_writable: true },
|
128
|
+
{ pubkey: associated_token, is_signer: false, is_writable: true },
|
129
|
+
{ pubkey: owner, is_signer: false, is_writable: false },
|
130
|
+
{ pubkey: mint, is_signer: false, is_writable: false },
|
131
|
+
{ pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, is_signer: false, is_writable: false },
|
132
|
+
{ pubkey: SYSTEM_PROGRAM_ID, is_signer: false, is_writable: false }
|
133
|
+
],
|
134
|
+
program_id: ASSOCIATED_TOKEN_PROGRAM_ID,
|
135
|
+
data: data
|
136
|
+
)
|
137
|
+
create_account_instruction
|
138
|
+
end
|
139
|
+
|
140
|
+
# Utility to encode data using predefined layouts
|
141
|
+
def self.encode_data(fields, data)
|
142
|
+
layout = SolanaRuby::DataTypes::Layout.new(fields)
|
143
|
+
layout.serialize(data)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Utility to decode data using predefined layouts
|
147
|
+
def self.decode_data(fields, data)
|
148
|
+
layout = SolanaRuby::DataTypes::Layout.new(fields)
|
149
|
+
layout.deserialize(data)
|
150
|
+
end
|
28
151
|
end
|
29
152
|
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
@@ -0,0 +1,57 @@
|
|
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
|
+
# Initialize Solana client
|
7
|
+
client = SolanaRuby::HttpClient.new('http://127.0.0.1:8899')
|
8
|
+
|
9
|
+
# Fetch the recent blockhash
|
10
|
+
recent_blockhash = client.get_latest_blockhash["blockhash"]
|
11
|
+
puts "Recent Blockhash: #{recent_blockhash}"
|
12
|
+
|
13
|
+
# Sender keypair and public key
|
14
|
+
private_key = "d22867a84ee1d91485a52c587793002dcaa7ce79a58bb605b3af2682099bb778"
|
15
|
+
sender_keypair = SolanaRuby::Keypair.from_private_key(private_key)
|
16
|
+
sender_pubkey = sender_keypair[:public_key]
|
17
|
+
puts "Sender Public Key: #{sender_pubkey}"
|
18
|
+
|
19
|
+
# Check sender's account balance
|
20
|
+
balance = client.get_balance(sender_pubkey)
|
21
|
+
puts "Sender account balance: #{balance} lamports"
|
22
|
+
if balance == 0
|
23
|
+
puts "Balance is zero, waiting for balance update..."
|
24
|
+
sleep(10)
|
25
|
+
end
|
26
|
+
|
27
|
+
# new keypair and public key (new account)
|
28
|
+
new_account = SolanaRuby::Keypair.generate
|
29
|
+
new_account_pubkey = new_account[:public_key]
|
30
|
+
puts "New Account Public Key: #{new_account_pubkey}"
|
31
|
+
|
32
|
+
# Parameters for account creation
|
33
|
+
lamports = 1 * 1_000_000_000 # Lamports to transfer
|
34
|
+
space = 165 # Space allocation (bytes)
|
35
|
+
program_id = SolanaRuby::TransactionHelper::SYSTEM_PROGRAM_ID
|
36
|
+
|
37
|
+
# Create and sign the transaction
|
38
|
+
transaction = SolanaRuby::TransactionHelper.create_account(
|
39
|
+
sender_pubkey,
|
40
|
+
new_account_pubkey,
|
41
|
+
lamports,
|
42
|
+
space,
|
43
|
+
recent_blockhash,
|
44
|
+
program_id
|
45
|
+
)
|
46
|
+
|
47
|
+
# Sign transaction with both sender and new account keypairs
|
48
|
+
transaction.sign([sender_keypair, new_account])
|
49
|
+
|
50
|
+
# Send the transaction
|
51
|
+
puts "Sending transaction..."
|
52
|
+
response = client.send_transaction(transaction.to_base64, { encoding: 'base64' })
|
53
|
+
|
54
|
+
# Output transaction results
|
55
|
+
puts "Transaction Signature: #{response}"
|
56
|
+
puts "New account created successfully with Public Key: #{new_account_pubkey}"
|
57
|
+
|
@@ -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.sol_transfer(
|
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.0beta2
|
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-
|
11
|
+
date: 2024-11-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: websocket-client-simple
|
@@ -229,6 +229,12 @@ files:
|
|
229
229
|
- lib/solana_ruby.rb
|
230
230
|
- lib/solana_ruby/.DS_Store
|
231
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
|
232
238
|
- lib/solana_ruby/http_client.rb
|
233
239
|
- lib/solana_ruby/http_methods/account_methods.rb
|
234
240
|
- lib/solana_ruby/http_methods/basic_methods.rb
|
@@ -240,9 +246,11 @@ files:
|
|
240
246
|
- lib/solana_ruby/http_methods/token_methods.rb
|
241
247
|
- lib/solana_ruby/http_methods/transaction_methods.rb
|
242
248
|
- lib/solana_ruby/keypair.rb
|
249
|
+
- lib/solana_ruby/message.rb
|
243
250
|
- lib/solana_ruby/transaction.rb
|
244
251
|
- lib/solana_ruby/transaction_helper.rb
|
245
252
|
- lib/solana_ruby/transaction_instruction.rb
|
253
|
+
- lib/solana_ruby/utils.rb
|
246
254
|
- lib/solana_ruby/version.rb
|
247
255
|
- lib/solana_ruby/web_socket_client.rb
|
248
256
|
- lib/solana_ruby/web_socket_handlers.rb
|
@@ -251,6 +259,9 @@ files:
|
|
251
259
|
- lib/solana_ruby/web_socket_methods/root_methods.rb
|
252
260
|
- lib/solana_ruby/web_socket_methods/signature_methods.rb
|
253
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
|
254
265
|
homepage: https://github.com/Build-Squad/solana-ruby
|
255
266
|
licenses:
|
256
267
|
- MIT
|
@@ -273,7 +284,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
273
284
|
- !ruby/object:Gem::Version
|
274
285
|
version: '0'
|
275
286
|
requirements: []
|
276
|
-
rubygems_version: 3.5.
|
287
|
+
rubygems_version: 3.5.23
|
277
288
|
signing_key:
|
278
289
|
specification_version: 4
|
279
290
|
summary: Solana Ruby SDK
|