waves_ruby_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Guardfile +72 -0
- data/LICENSE.txt +21 -0
- data/README.md +130 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/btc/base58.rb +109 -0
- data/lib/btc/data.rb +109 -0
- data/lib/sign/axlsign.rb +1154 -0
- data/lib/sign/int32.rb +63 -0
- data/lib/waves_ruby_client.rb +56 -0
- data/lib/waves_ruby_client/api.rb +19 -0
- data/lib/waves_ruby_client/asset.rb +25 -0
- data/lib/waves_ruby_client/conversion.rb +29 -0
- data/lib/waves_ruby_client/order.rb +104 -0
- data/lib/waves_ruby_client/order_book.rb +33 -0
- data/lib/waves_ruby_client/order_data/cancel.rb +36 -0
- data/lib/waves_ruby_client/order_data/place.rb +66 -0
- data/lib/waves_ruby_client/order_data/user_orders.rb +34 -0
- data/lib/waves_ruby_client/transaction.rb +19 -0
- data/lib/waves_ruby_client/version.rb +5 -0
- data/lib/waves_ruby_client/wallet.rb +17 -0
- data/waves_ruby_client.gemspec +34 -0
- metadata +171 -0
data/lib/sign/int32.rb
ADDED
@@ -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
|