blzrb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+ require 'json'
5
+ require 'bluzelle/utils'
6
+ require 'bluzelle/constants'
7
+
8
+ module Bluzelle
9
+ module Swarm
10
+ class Cosmos
11
+ include Bluzelle::Constants
12
+ include Bluzelle::Utils
13
+
14
+ attr_reader :mnemonic, :endpoint, :address, :chain_id
15
+ attr_accessor :account_info
16
+
17
+ def initialize(options = {})
18
+ @mnemonic = options[:mnemonic]
19
+ @chain_id = options[:chain_id]
20
+ @endpoint = options[:endpoint]
21
+ @account_info = {}
22
+
23
+ @private_key = get_ec_private_key(@mnemonic)
24
+ @address = address_from_mnemonic
25
+
26
+ account
27
+ end
28
+
29
+ def query(endpoint)
30
+ Request.execute(method: 'get', url: "#{@endpoint}/#{endpoint}")
31
+ end
32
+
33
+ def send_transaction(method, endpoint, data, gas_info)
34
+ txn = Transaction.new(method, endpoint, data)
35
+ txn.set_gas(gas_info)
36
+
37
+ # fetch skeleton
38
+ skeleton = fetch_txn_skeleton(txn)
39
+ # set gas
40
+ skeleton = update_gas(txn, skeleton)
41
+ # sort
42
+ skeleton = sort_hash(skeleton)
43
+
44
+ broadcast_transaction(Transaction.new('post', TX_COMMAND, skeleton))
45
+ end
46
+
47
+ private
48
+
49
+ # Account query
50
+ def account
51
+ url = "#{@endpoint}/auth/accounts/#{@address}"
52
+ res = Request.execute(method: 'get', url: url)
53
+
54
+ set_account_details(res.dig('result', 'value'))
55
+ end
56
+
57
+ # Broadcasts a transaction
58
+ #
59
+ # @param [Bluzelle::Swarm::Transaction] txn
60
+ def broadcast_transaction(txn)
61
+ txn.data['memo'] = make_random_string
62
+
63
+ txn.data['signatures'] = [{
64
+ 'account_number' => @account_info['account_number'].to_s,
65
+ 'pub_key' => {
66
+ 'type' => 'tendermint/PubKeySecp256k1',
67
+ 'value' => to_base64(
68
+ [compressed_pub_key(open_key(@private_key))].pack('H*')
69
+ )
70
+ },
71
+ 'sequence' => @account_info['sequence'].to_s,
72
+ 'signature' => sign_transaction(txn.data)
73
+ }]
74
+
75
+ url = "#{@endpoint}/#{txn.endpoint}"
76
+ payload = { 'mode' => 'block', 'tx' => txn.data }
77
+
78
+ res = Request.execute(method: txn.method, url: url, payload: payload)
79
+
80
+ if res.dig('code').nil?
81
+ update_sequence
82
+ decode_json(hex_to_bin(res.dig('data'))) if res.key?('data')
83
+ else
84
+ handle_broadcast_error(res.dig('raw_log'), txn)
85
+ end
86
+ end
87
+
88
+ # Updates account sequence and retries broadcast
89
+ #
90
+ # @param [Bluzelle::Swarm::Transaction] txn
91
+ def update_account_sequence(txn)
92
+ if txn.retries_left != 0
93
+ account
94
+ retry_broadcast(txn)
95
+ else
96
+ raise Error::ApiError, 'Invalid chain id'
97
+ end
98
+ end
99
+
100
+ # Fetch transaction skeleton
101
+ def fetch_txn_skeleton(txn)
102
+ url = "#{@endpoint}/#{txn.endpoint}"
103
+
104
+ data = Request.execute(
105
+ method: txn.method,
106
+ url: url,
107
+ payload: txn.data,
108
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
109
+ )
110
+
111
+ data['value']
112
+ end
113
+
114
+ # Check if address and mnemonic are valid
115
+ def address_from_mnemonic
116
+ pub_key = get_ec_public_key_from_priv(@private_key)
117
+ get_address(pub_key)
118
+ end
119
+
120
+ # Updates account details
121
+ #
122
+ # @param [Hash] data
123
+ def set_account_details(data)
124
+ account_number = data.dig('account_number')
125
+ sequence = data.dig('sequence')
126
+
127
+ @account_info['account_number'] = account_number
128
+
129
+ if @account_info['sequence'] != sequence
130
+ @account_info['sequence'] = sequence
131
+ return true
132
+ end
133
+
134
+ false
135
+ end
136
+
137
+ # Retry broadcast after failure
138
+ #
139
+ # @param [Bluzelle::Swarm::Transaction]
140
+ def retry_broadcast(txn)
141
+ txn.retries_left -= 1
142
+
143
+ sleep BROADCAST_RETRY_SECONDS
144
+
145
+ broadcast_transaction(txn)
146
+ end
147
+
148
+ # Handle broadcast error
149
+ #
150
+ # @param [String] raw_log
151
+ # @param [Bluzelle::Swarm::Transaction] txn
152
+ def handle_broadcast_error(raw_log, txn)
153
+ if raw_log.include?('signature verification failed')
154
+ update_account_sequence(txn)
155
+ else
156
+ raise Error::ApiError, extract_error_message(raw_log)
157
+ end
158
+ end
159
+
160
+ # Update account sequence
161
+ def update_sequence
162
+ @account_info['sequence'] = @account_info['sequence'].to_i + 1
163
+ end
164
+
165
+ # Signs a transaction
166
+ #
167
+ # @param txn
168
+ def sign_transaction(txn)
169
+ payload = {
170
+ 'account_number' => @account_info['account_number'].to_s,
171
+ 'chain_id' => @chain_id,
172
+ 'fee' => txn['fee'],
173
+ 'memo' => txn['memo'],
174
+ 'msgs' => txn['msg'],
175
+ 'sequence' => @account_info['sequence'].to_s
176
+ }
177
+
178
+ to_base64(ecdsa_sign(encode_json(payload), @private_key))
179
+ end
180
+
181
+ def update_gas(txn, data)
182
+ res = data.clone
183
+
184
+ if res.dig('fee', 'gas').to_i > txn.max_gas && txn.max_gas != 0
185
+ res['fee']['gas'] = txn.max_gas.to_s
186
+ end
187
+
188
+ if !txn.max_fee.nil?
189
+ res['fee']['amount'] = [{
190
+ 'denom': TOKEN_NAME,
191
+ 'amount': txn.max_fee.to_s
192
+ }]
193
+ elsif !txn.gas_price.nil?
194
+ res['fee']['amount'] = [{
195
+ 'denom': TOKEN_NAME,
196
+ 'amount': (res['fee']['gas'] * txn.gas_price).to_s
197
+ }]
198
+ end
199
+
200
+ res
201
+ end
202
+
203
+ def update_memo(txn)
204
+ txn['memo'] = make_random_string
205
+ txn
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+
5
+ module Bluzelle
6
+ module Swarm
7
+ class Request
8
+ class << self
9
+ def execute(options = {})
10
+ resp = RestClient::Request.execute(parse_options(options))
11
+ rescue RestClient::ExceptionWithResponse => e
12
+ case e.http_code
13
+ when 404, 500
14
+ raise Error::ApiError, e.response.body
15
+ else
16
+ error(e)
17
+ end
18
+ else
19
+ success(resp)
20
+ end
21
+
22
+ private
23
+
24
+ def parse_options(options = {})
25
+ {
26
+ method: options[:method],
27
+ url: options[:url],
28
+ payload: JSON.generate(options[:payload]),
29
+ headers: options[:headers]
30
+ }
31
+ end
32
+
33
+ def error(err)
34
+ body = JSON.parse(err.response.body)
35
+ error_message = body.dig('error', 'message')
36
+
37
+ if error_message.is_a?(String)
38
+ raise Error::ApiError, error_message
39
+ else
40
+ raise Error::ApiError, 'error occurred'
41
+ end
42
+ end
43
+
44
+ def success(resp)
45
+ JSON.parse(resp.body)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bluzelle
4
+ module Swarm
5
+ class Transaction
6
+ attr_reader :method, :endpoint, :data
7
+ attr_accessor :gas_price, :max_gas, :max_fee, :memo, :retries_left
8
+
9
+ def initialize(method, endpoint, data)
10
+ @method = method
11
+ @endpoint = endpoint
12
+ @data = data
13
+ @gas_price = 0
14
+ @max_gas = 0
15
+ @max_fee = 0
16
+ @retries_left = 10
17
+ end
18
+
19
+ def set_gas(gas_info)
20
+ return if gas_info.nil? || !gas_info.is_a?(Hash)
21
+
22
+ gas_info = Utils.stringify_keys(gas_info)
23
+
24
+ @gas_price = gas_info['gas_price'].to_i if gas_info.key?('gas_price')
25
+
26
+ @max_gas = gas_info['max_gas'].to_i if gas_info.key?('max_gas')
27
+
28
+ @max_fee = gas_info['max_fee'].to_i if gas_info.key?('max_fee')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,185 @@
1
+ require 'bip_mnemonic'
2
+ require 'bitcoin'
3
+ require 'money-tree'
4
+ require 'digest'
5
+ require 'openssl'
6
+ require 'base64'
7
+ require 'secp256k1'
8
+
9
+ module Bluzelle
10
+ module Utils
11
+ module_function
12
+
13
+ def get_ec_private_key(mnemonic)
14
+ seed = bip39_mnemonic_to_seed(mnemonic)
15
+ node = bip32_from_seed(seed)
16
+ child = node.node_for_path(Constants::PATH)
17
+ ec_pair = create_ec_pair(child.private_key.to_hex)
18
+ ec_pair.priv
19
+ end
20
+
21
+ def get_ec_public_key_from_priv(priv)
22
+ key = open_key(priv)
23
+ compressed_pub_key(key)
24
+ end
25
+
26
+ def validate_address(address, mnemonic)
27
+ priv_key = get_ec_private_key(mnemonic)
28
+ pub_key = get_ec_public_key_from_priv(priv_key)
29
+
30
+ if get_address(pub_key) != address
31
+ raise ArgumentError, 'Bad credentials - verify your address and mnemonic'
32
+ end
33
+ end
34
+
35
+ def get_address(pub_key)
36
+ hash = rmd_160_digest(sha_256_digest([pub_key].pack('H*')))
37
+ word = bech32_convert_bits(to_bytes(hash))
38
+ bech32_encode(Constants::PREFIX, word)
39
+ end
40
+
41
+ def to_bytes(obj)
42
+ obj.bytes
43
+ end
44
+
45
+ def rmd_160_digest(hex)
46
+ Digest::RMD160.digest hex
47
+ end
48
+
49
+ def sha_256_digest(hex)
50
+ Digest::SHA256.digest hex
51
+ end
52
+
53
+ def bech32_encode(prefix, word)
54
+ Bitcoin::Bech32.encode(prefix, word)
55
+ end
56
+
57
+ def bech32_convert_bits(bytes, from_bits: 8, to_bits: 5, pad: false)
58
+ Bitcoin::Bech32.convert_bits(bytes, from_bits: from_bits, to_bits: to_bits, pad: pad)
59
+ end
60
+
61
+ def bip39_mnemonic_to_seed(mnemonic)
62
+ BipMnemonic.to_seed(mnemonic: mnemonic)
63
+ end
64
+
65
+ def open_key(priv)
66
+ group = OpenSSL::PKey::EC::Group.new('secp256k1')
67
+ key = OpenSSL::PKey::EC.new(group)
68
+
69
+ key.private_key = OpenSSL::BN.new(priv, 16)
70
+ key.public_key = group.generator.mul(key.private_key)
71
+
72
+ key
73
+ end
74
+
75
+ def compressed_pub_key(key)
76
+ public_key = key.public_key
77
+ public_key.group.point_conversion_form = :compressed
78
+ public_key.to_hex.rjust(66, '0')
79
+ end
80
+
81
+ def bip32_from_seed(seed)
82
+ MoneyTree::Master.new(seed_hex: seed)
83
+ end
84
+
85
+ def create_ec_pair(private_key)
86
+ Bitcoin::Key.new(private_key, nil, { compressed: false })
87
+ end
88
+
89
+ def make_random_string(length = 32)
90
+ random_string = ''
91
+ chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.chars
92
+
93
+ 1.upto(length) { random_string << chars.sample }
94
+
95
+ random_string
96
+ end
97
+
98
+ def to_base64(str)
99
+ Base64.strict_encode64(str)
100
+ end
101
+
102
+ def convert_lease(lease)
103
+ return '0' if lease.nil?
104
+
105
+ seconds = 0
106
+
107
+ seconds += lease.dig(:days).nil? ? 0 : (lease.dig(:days).to_i * 24 * 60 * 60)
108
+ seconds += lease.dig(:hours).nil? ? 0 : (lease.dig(:hours).to_i * 60 * 60)
109
+ seconds += lease.dig(:minutes).nil? ? 0 : (lease.dig(:minutes).to_i * 60)
110
+ seconds += lease.dig(:seconds).nil? ? 0 : lease.dig(:seconds).to_i
111
+
112
+ (seconds / Constants::BLOCK_TIME_IN_SECONDS).to_s
113
+ end
114
+
115
+ def sort_hash(hash)
116
+ hash_clone = hash.clone
117
+
118
+ hash_clone.each do |key, value|
119
+ hash_clone[key] = sort_hash(value) if value.is_a?(Hash)
120
+
121
+ next unless value.is_a?(Array)
122
+
123
+ arr = []
124
+
125
+ hash_clone[key].each do |el|
126
+ arr << sort_hash(el)
127
+ end
128
+
129
+ hash_clone[key] = arr
130
+ end
131
+
132
+ hash_clone.sort.to_h
133
+ end
134
+
135
+ def stringify_keys(hash)
136
+ res = {}
137
+
138
+ hash.each do |key, value|
139
+ if value.is_a?(Hash)
140
+ res[key.to_s] = stringify_keys(value)
141
+ next
142
+ end
143
+ res[key.to_s] = value
144
+ end
145
+
146
+ res
147
+ end
148
+
149
+ def ecdsa_sign(payload, private_key)
150
+ pk = Secp256k1::PrivateKey.new(privkey: hex_to_bin(private_key), raw: true)
151
+ rs = pk.ecdsa_sign(payload)
152
+ r = rs.slice(0, 32).read_string.reverse
153
+ s = rs.slice(32, 32).read_string.reverse
154
+ "#{r}#{s}"
155
+ end
156
+
157
+ def encode_json(obj)
158
+ JSON.generate(obj)
159
+ end
160
+
161
+ def decode_json(str)
162
+ JSON.parse(str)
163
+ end
164
+
165
+ def hex_to_bin(hex_str)
166
+ [hex_str].pack('H*')
167
+ end
168
+
169
+ def extract_error_message(str)
170
+ offset1 = str.index(': ')
171
+
172
+ return str if offset1.nil?
173
+
174
+ prefix = str.slice(0, offset1)
175
+
176
+ case prefix
177
+ when 'insufficient fee'
178
+ return str.slice(offset1 + 2, str.length)
179
+ end
180
+
181
+ offset2 = str.index(': ', offset1 + 1)
182
+ str[(offset1 + 2)..(offset2 - 1)]
183
+ end
184
+ end
185
+ end