waves_ruby_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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