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.
- 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
|