waves_ruby_client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # simulate 32 bit integer with overflow
4
+ class Int32
5
+ attr_accessor :val
6
+ def initialize(val)
7
+ self.val = if val.is_a?(Int32)
8
+ val.val
9
+ else
10
+ val
11
+ end
12
+ end
13
+
14
+ def ==(other)
15
+ to_i == if other.is_a?(Int32)
16
+ other.to_i
17
+ else
18
+ other
19
+ end
20
+ end
21
+
22
+ def self.force_overflow_signed(i)
23
+ force_overflow_unsigned(i + 2**31) - 2**31
24
+ end
25
+
26
+ def self.force_overflow_unsigned(i)
27
+ i % 2**32 # or equivalently: i & 0xffffffff
28
+ end
29
+
30
+ def r_shift_pos(other)
31
+ other_val = other.is_a?(Int32) ? other.val : other
32
+ Int32.new(self.class.force_overflow_unsigned(val) >> other_val)
33
+ end
34
+
35
+ def to_i
36
+ self.class.force_overflow_signed(val)
37
+ end
38
+
39
+ def dup
40
+ Int32.new(val)
41
+ end
42
+
43
+ ['|', '^', '+', '-', '&', '*'].each do |op|
44
+ define_method op do |other|
45
+ if other.is_a?(Int32)
46
+ Int32.new(to_i.send(op, other.to_i))
47
+ else
48
+ super(other)
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def respond_to_missing?(meth, _include_private = false)
56
+ val.respond_to?(meth)
57
+ end
58
+
59
+ def method_missing(meth, *args, &block)
60
+ return super unless respond_to_missing?(meth)
61
+ Int32.new(self.class.force_overflow_signed(val.send(meth, *args, &block)))
62
+ end
63
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'active_support/core_ext/object/instance_variables'
5
+ require 'active_support/duration'
6
+ require 'active_support/core_ext/numeric'
7
+ require 'httparty'
8
+ require 'singleton'
9
+
10
+ require 'sign/int32'
11
+ require 'sign/axlsign'
12
+
13
+ require 'btc/data'
14
+ require 'btc/base58'
15
+
16
+ require 'waves_ruby_client/version'
17
+ require 'waves_ruby_client/asset'
18
+ require 'waves_ruby_client/conversion'
19
+ require 'waves_ruby_client/api'
20
+ require 'waves_ruby_client/order_book'
21
+ require 'waves_ruby_client/order_data/place'
22
+ require 'waves_ruby_client/order_data/cancel'
23
+ require 'waves_ruby_client/order_data/user_orders'
24
+ require 'waves_ruby_client/order'
25
+ require 'waves_ruby_client/wallet'
26
+ require 'waves_ruby_client/transaction'
27
+
28
+ module WavesRubyClient
29
+ WAVES_PUBLIC_KEY = ENV['WAVES_PUBLIC_KEY']
30
+ WAVES_PRIVATE_KEY = ENV['WAVES_PRIVATE_KEY']
31
+ WAVES_ADDRESS = ENV['WAVES_ADDRESS']
32
+ API_URL = ENV['WAVES_API_URL'] || 'https://nodes.wavesnodes.com'
33
+ MATCHER_PUBLIC_KEY = ENV['WAVES_MATCHER_PUBLIC_KEY'] ||
34
+ '7kPFrHDiGw1rCm7LPszuECwWYL3dMf6iMifLRDJQZMzy'
35
+
36
+ BTC_ASSET_ID = '8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS'
37
+ WAVES_ASSET_ID = 'WAVES'
38
+
39
+ MATCHER_FEE = 0.003
40
+ NUMBER_MULTIPLIKATOR = 10**8
41
+
42
+ PRICE_ASSET = WavesRubyClient::Asset.btc
43
+ AMOUNT_ASSET = WavesRubyClient::Asset.waves
44
+
45
+ OrderAlreadyFilled = Class.new(StandardError)
46
+
47
+ # try method call several times
48
+ def self.try_many_times(times = 5)
49
+ tries ||= times
50
+ yield
51
+ rescue StandardError => e
52
+ sleep(5)
53
+ retry unless (tries -= 1).zero?
54
+ raise e
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WavesRubyClient
4
+ # Access to waves api
5
+ class Api
6
+ include Singleton
7
+
8
+ def call_matcher(path, method = :get, args = {})
9
+ WavesRubyClient.try_many_times do
10
+ call('/matcher' + path, method, args)
11
+ end
12
+ end
13
+
14
+ def call(path, method = :get, args = {})
15
+ response = HTTParty.send(method, WavesRubyClient::API_URL + path, args)
16
+ JSON.parse(response.body)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WavesRubyClient
4
+ class Asset
5
+ include ActiveModel::Model
6
+ attr_accessor :name, :id, :url_id
7
+
8
+ def self.waves
9
+ new(name: 'WAVES', id: '', url_id: WavesRubyClient::WAVES_ASSET_ID)
10
+ end
11
+
12
+ def self.btc
13
+ new(name: 'BTC', id: WavesRubyClient::BTC_ASSET_ID,
14
+ url_id: WavesRubyClient::BTC_ASSET_ID)
15
+ end
16
+
17
+ def to_bytes
18
+ if name == WavesRubyClient::WAVES_ASSET_ID
19
+ [0]
20
+ else
21
+ [1].concat(WavesRubyClient::Conversion.base58_to_bytes(id))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WavesRubyClient
4
+ # Conversion Helpers
5
+ module Conversion
6
+ module ClassMethods # :nodoc:
7
+ def long_to_bytes(long)
8
+ long = long.to_i
9
+ bytes = []
10
+ 8.times do
11
+ bytes << (long & 255)
12
+ long /= 256
13
+ end
14
+ bytes.reverse
15
+ end
16
+
17
+ def bytes_to_base58(bytes)
18
+ BTC::Base58.base58_from_data(bytes.map(&:chr).join)
19
+ end
20
+
21
+ def base58_to_bytes(input)
22
+ BTC::Base58.data_from_base58(input).bytes
23
+ end
24
+ end
25
+
26
+ extend ClassMethods
27
+ include ClassMethods
28
+ end
29
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WavesRubyClient
4
+ # A limit order
5
+ class Order
6
+ include ActiveModel::Model
7
+
8
+ JSON_HEADERS = { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }.freeze
9
+
10
+ attr_accessor :type, :amount, :id, :timestamp, :status, :price
11
+ attr_writer :filled
12
+
13
+ # get all orders for WAVES_PUBLIC_KEY
14
+ def self.all
15
+ url = ['/orderbook', WavesRubyClient::AMOUNT_ASSET.url_id,
16
+ WavesRubyClient::PRICE_ASSET.url_id,
17
+ 'publicKey', WavesRubyClient::WAVES_PUBLIC_KEY].join('/')
18
+ data = WavesRubyClient::OrderData::UserOrders.new.data_with_signature
19
+ orders = WavesRubyClient::Api.instance.call_matcher(url, :get, headers: data)
20
+ orders.map do |order_hash|
21
+ attributes = %i[filled price amount].map do |attribute|
22
+ { attribute => order_hash[attribute.to_s].to_f / WavesRubyClient::NUMBER_MULTIPLIKATOR }
23
+ end.reduce({}, :merge)
24
+ new(order_hash.slice('id', 'status', 'type', 'timestamp').merge(attributes))
25
+ end
26
+ end
27
+
28
+ # get all orders waiting to be filled for WAVES_PUBLIC_KEY
29
+ def self.active
30
+ all.select { |o| o.status == 'Accepted' || o.status == 'PartiallyFilled' }
31
+ end
32
+
33
+ # order is waiting to be filled
34
+ def pending?
35
+ status != 'Filled' && status != 'PartiallyFilled'
36
+ end
37
+
38
+ # filled amount
39
+ def filled
40
+ refresh_from_collection
41
+ @filled
42
+ end
43
+
44
+ # place order
45
+ # any error is raised
46
+ def place
47
+ data = WavesRubyClient::OrderData::Place.new(self).data_with_signature
48
+ res = WavesRubyClient::Api.instance.call_matcher('/orderbook', :post,
49
+ body: data.to_json,
50
+ headers: JSON_HEADERS)
51
+ raise res.to_s unless res['status'] == 'OrderAccepted'
52
+ self.id = res['message']['id']
53
+ self
54
+ end
55
+
56
+ # cancel order
57
+ # any error is raised
58
+ def cancel
59
+ res = remove('cancel')
60
+ raise WavesRubyClient::OrderAlreadyFilled if res['message']&.match?(/Order is already Filled/)
61
+ raise res.to_s unless res['status'] == 'OrderCanceled'
62
+ end
63
+
64
+ # delete order after it has been cancelled
65
+ # any error is raised
66
+ def delete
67
+ res = remove('delete')
68
+ raise res.to_s unless res['status'] == 'OrderDeleted'
69
+ end
70
+
71
+ def price_asset
72
+ WavesRubyClient::PRICE_ASSET
73
+ end
74
+
75
+ def amount_asset
76
+ WavesRubyClient::AMOUNT_ASSET
77
+ end
78
+
79
+ # query order status
80
+ def refresh_status
81
+ url = "/orderbook/#{amount_asset.url_id}/#{price_asset.url_id}/#{id}"
82
+ response = WavesRubyClient::Api.instance.call_matcher(url, :get)
83
+ self.status = response['status']
84
+ end
85
+
86
+ private
87
+
88
+ # There is no api method to query data of a single order
89
+ # So all orders are retrieved
90
+ def refresh_from_collection
91
+ order = self.class.all.select { |o| o.id == id }.first
92
+ return unless order
93
+ assign_attributes(order.instance_values)
94
+ end
95
+
96
+ def remove(action)
97
+ data = WavesRubyClient::OrderData::Cancel.new(self).data_with_signature
98
+ url = "/orderbook/#{amount_asset.url_id}/#{price_asset.url_id}/#{action}"
99
+ WavesRubyClient::Api.instance.call_matcher(url, :post,
100
+ body: data.to_json,
101
+ headers: JSON_HEADERS)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WavesRubyClient
4
+ # Orderbook for a pair of assets
5
+ class OrderBook
6
+ def self.btc_waves
7
+ new(WavesRubyClient::Asset.waves, WavesRubyClient::Asset.btc)
8
+ end
9
+
10
+ attr_accessor :asset1, :asset2, :bids, :asks
11
+
12
+ def initialize(asset1, asset2)
13
+ self.asset1 = asset1
14
+ self.asset2 = asset2
15
+ self.bids = self.asks = []
16
+ end
17
+
18
+ def refresh
19
+ order_book = WavesRubyClient::Api.instance.call_matcher(
20
+ "/orderbook/#{asset1.url_id}/#{asset2.url_id}"
21
+ )
22
+ self.asks = order_book['asks'].map { |order| scale(order) }
23
+ self.bids = order_book['bids'].map { |order| scale(order) }
24
+ end
25
+
26
+ private
27
+
28
+ def scale(order)
29
+ { price: order['price'].to_f / WavesRubyClient::NUMBER_MULTIPLIKATOR,
30
+ amount: order['amount'].to_f / WavesRubyClient::NUMBER_MULTIPLIKATOR }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WavesRubyClient
4
+ module OrderData
5
+ # Data for cancelling an order
6
+ class Cancel
7
+ include WavesRubyClient::Conversion
8
+
9
+ attr_accessor :order
10
+
11
+ def initialize(order)
12
+ self.order = order
13
+ end
14
+
15
+ def data_with_signature
16
+ { sender: WavesRubyClient::WAVES_PUBLIC_KEY,
17
+ orderId: order.id,
18
+ signature: signature }
19
+ end
20
+
21
+ private
22
+
23
+ def signature
24
+ sign_bytes = Axlsign.sign(
25
+ base58_to_bytes(WavesRubyClient::WAVES_PRIVATE_KEY),
26
+ bytes_to_sign
27
+ )
28
+ bytes_to_base58(sign_bytes)
29
+ end
30
+
31
+ def bytes_to_sign
32
+ base58_to_bytes(WavesRubyClient::WAVES_PUBLIC_KEY) + base58_to_bytes(order.id)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WavesRubyClient
4
+ module OrderData
5
+ # Data for placing an order
6
+ class Place
7
+ include WavesRubyClient::Conversion
8
+
9
+ attr_accessor :order
10
+
11
+ def initialize(order)
12
+ self.order = order
13
+ end
14
+
15
+ def data_with_signature
16
+ data.merge(signature: signature)
17
+ end
18
+
19
+ private
20
+
21
+ def signature
22
+ sign_bytes = Axlsign.sign(
23
+ base58_to_bytes(WavesRubyClient::WAVES_PRIVATE_KEY),
24
+ bytes_to_sign
25
+ )
26
+ bytes_to_base58(sign_bytes)
27
+ end
28
+
29
+ def expiration
30
+ @expiration ||= 1.day.since.to_i * 1000
31
+ end
32
+
33
+ def timestamp
34
+ @timestamp ||= Time.now.to_i * 1000
35
+ end
36
+
37
+ def data
38
+ { orderType: order.type,
39
+ assetPair: { amountAsset: order.amount_asset.id, priceAsset: order.price_asset.id },
40
+ price: (order.price * WavesRubyClient::NUMBER_MULTIPLIKATOR).to_i,
41
+ amount: (order.amount * WavesRubyClient::NUMBER_MULTIPLIKATOR).to_i,
42
+ timestamp: timestamp,
43
+ expiration: expiration,
44
+ matcherFee: WavesRubyClient::MATCHER_FEE * WavesRubyClient::NUMBER_MULTIPLIKATOR,
45
+ matcherPublicKey: WavesRubyClient::MATCHER_PUBLIC_KEY,
46
+ senderPublicKey: WavesRubyClient::WAVES_PUBLIC_KEY }
47
+ end
48
+
49
+ def bytes_to_sign
50
+ order_data = data
51
+ [
52
+ base58_to_bytes(order_data[:senderPublicKey]),
53
+ base58_to_bytes(order_data[:matcherPublicKey]),
54
+ order.amount_asset.to_bytes,
55
+ order.price_asset.to_bytes,
56
+ order.type == :buy ? 0 : 1,
57
+ long_to_bytes(order_data[:price]),
58
+ long_to_bytes(order_data[:amount]),
59
+ long_to_bytes(timestamp),
60
+ long_to_bytes(order_data[:expiration]),
61
+ long_to_bytes(order_data[:matcherFee])
62
+ ].flatten
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module WavesRubyClient
5
+ module OrderData
6
+ # Data for querying user orders
7
+ class UserOrders
8
+ include WavesRubyClient::Conversion
9
+
10
+ def data_with_signature
11
+ { Timestamp: timestamp.to_s,
12
+ Signature: signature }
13
+ end
14
+
15
+ private
16
+
17
+ def signature
18
+ sign_bytes = Axlsign.sign(
19
+ base58_to_bytes(WavesRubyClient::WAVES_PRIVATE_KEY),
20
+ bytes_to_sign
21
+ )
22
+ bytes_to_base58(sign_bytes)
23
+ end
24
+
25
+ def timestamp
26
+ @timestamp ||= Time.now.to_i * 1000
27
+ end
28
+
29
+ def bytes_to_sign
30
+ base58_to_bytes(WavesRubyClient::WAVES_PUBLIC_KEY) + long_to_bytes(timestamp)
31
+ end
32
+ end
33
+ end
34
+ end