yolodice-client 0.1.1
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/LICENSE.md +7 -0
- data/README.md +116 -0
- data/lib/yolodice_client.rb +220 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a3f9c18f4f343eb90b4b79ac9ea29947aa37e8cc
|
4
|
+
data.tar.gz: 79d786b072ff58a346805ce4c6ac57bf9af8f3f8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4cd29705d3668b1fc29489fdc255efeb7b364085325cf482eb9e641aa95b7c242be4b7f90a36e30423029c120ce32658a1d6632999852975f21968f2b9b554db
|
7
|
+
data.tar.gz: 5af05d6cb39c7693ff7df81040a936cdad27820ef5a82161e1257c22b23d152d05055bd658ee951f233f762a384ea822d1799a7206c3957cb051ffdf43642878
|
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2017 Ethan White (YOLOdice.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
YOLOdice API Client for Ruby
|
2
|
+
============================
|
3
|
+
|
4
|
+
[YOLOdice](https://yolodice.com) is a simple online Bitcoin game you can play against the house. The game relies on a pseudorandom number generator that returns bet results used to determine if bets placed by players win or lose.
|
5
|
+
|
6
|
+
This Ruby library contains a simple client that connects to the YOLOdice API endpoint:
|
7
|
+
|
8
|
+
* client connects over secure SSL over TCP transport layer,
|
9
|
+
* supports authentication,
|
10
|
+
* handles any server-side methods via `method_missing` mechanism.
|
11
|
+
|
12
|
+
Additional resources:
|
13
|
+
|
14
|
+
* [dev.yolodice.com](https://dev.yolodice.com) Official YOLOdice API documentation,
|
15
|
+
* [yolodice.com](https://yolodice.com) The YOLOdice site (take a look at [FAQ](https://yolodice.com/#faq)).
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
gem install yolodice-client
|
20
|
+
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
Require the gem in your scripts:
|
25
|
+
|
26
|
+
require 'yolodice_client'
|
27
|
+
|
28
|
+
|
29
|
+
## Generating API keys
|
30
|
+
|
31
|
+
YOLOdice API requires authentication for most of it's methods. A Bitcoin key/address will be required to setup the API key and authenticate. Here is one way to do this:
|
32
|
+
|
33
|
+
1. Generate an Bitcoin public and private key:
|
34
|
+
|
35
|
+
require 'bitcoin'
|
36
|
+
|
37
|
+
btc_key = Bitcoin::Key.generate
|
38
|
+
auth_key = btc_key.to_base58 # this is your secret code, store it in a secure place
|
39
|
+
auth_addr = btc_key.addr # paste this in your YD settings as a new key
|
40
|
+
|
41
|
+
2. Go to [YOLOdice account Settings](https://yolodice.com/#settings), create a new key and paste the `auth_addr` generated above. Set permissions as you wish.
|
42
|
+
3. Use `auth_key` in your code to authenticate.
|
43
|
+
|
44
|
+
Just a quick note — this address is used ONLY to authenticate. No coins will be ever sent to it.
|
45
|
+
|
46
|
+
|
47
|
+
## Connecting
|
48
|
+
|
49
|
+
yd = YolodiceClient.new
|
50
|
+
yd.connect
|
51
|
+
yd.authenticate auth_key
|
52
|
+
|
53
|
+
It's important to authenticate immediately after connecting. Otherwise the connection will be closed by the server.
|
54
|
+
|
55
|
+
The client automatically sends a `ping` requests to the server every 30 seconds to prevent the connection from timing out.
|
56
|
+
|
57
|
+
|
58
|
+
## Logging and debugging
|
59
|
+
|
60
|
+
To preview the actuall messages sent back and forth you could provide your own logger object and set log level to `DEBUG` like this:
|
61
|
+
|
62
|
+
yd = YolodiceClient.new
|
63
|
+
logger = Logger.new STDERR
|
64
|
+
logger.level = Logger::DEBUG
|
65
|
+
yd.logger = logger
|
66
|
+
|
67
|
+
yd.connect
|
68
|
+
yd.authenticate auth_key
|
69
|
+
|
70
|
+
This would result in the output similar to this:
|
71
|
+
|
72
|
+
DEBUG -- : Connecting to api.yolodice.dev:4444 over SSL
|
73
|
+
INFO -- : Connected to api.yolodice.dev:4444
|
74
|
+
DEBUG -- : Listening thread started
|
75
|
+
DEBUG -- : Pinging thread started
|
76
|
+
DEBUG -- : Calling remote method generate_auth_challenge()
|
77
|
+
DEBUG -- : >>> {"id":1,"method":"generate_auth_challenge"}
|
78
|
+
DEBUG -- : <<< {"id":1,"result":"yd_login_26vEyvUUdgUy"}
|
79
|
+
|
80
|
+
DEBUG -- : Calling remote method auth_by_address({:address=>"n3kmufwdR3Zzgk3k6NYeeLBxB9SpHKe5Tc", :signature=>"IB5ITZHQZoApdXhUMGFFZ9AG4OtTw85jdaPMSVYNpOayEAG5LK9bsPhtCjwPEjDy/YDHqKk6gf1+aLzg0B63Qfk="})
|
81
|
+
DEBUG -- : >>> {"id":2,"method":"auth_by_address","params":{"address":"n3kmufwdR3Zzgk3k6NYeeLBxB9SpHKe5Tc","signature":"IB5ITZHQZoApdXhUMGFFZ9AG4OtTw85jdaPMSVYNpOayEAG5LK9bsPhtCjwPEjDy/YDHqKk6gf1+aLzg0B63Qfk="}}
|
82
|
+
DEBUG -- : <<< {"id":2,"result":{"id":1,"name":"sdafasfuiafu","created_at":1470085899.0356,"roles":["admin"]}}
|
83
|
+
|
84
|
+
## Conventions, return values, errors
|
85
|
+
|
86
|
+
By default whenever any Bitcoin amount is sent or received from the server, it is passed as an Integer with the amount of satoshis. Using this convention 1 BTC would be represented as an integer value or `100_000_000`.
|
87
|
+
|
88
|
+
Whenever server responds with an Object, this client returns a Hash with keys being Strings.
|
89
|
+
|
90
|
+
There are two error classes:
|
91
|
+
|
92
|
+
* `YolodiceClient::Error` that inherits from `StandardError` and is used for errors thrown by the client itself,
|
93
|
+
* `YolodiceClient::RemoteError` that inherits from `StandardError` that is used to pass errors from the remote server,
|
94
|
+
* any errors from underlaying `TCPSocket` or `SSLSocket` are passed through.
|
95
|
+
|
96
|
+
`RemoteError` has two extra attributes: `code` and `data` that are mapped to values in the error object returned from the server.
|
97
|
+
|
98
|
+
## Another example
|
99
|
+
|
100
|
+
Here is a script that connects to the server, authenticates, fetches user data, rolls a few 50% chance bets and reads user data again (make sure to use your own credential):
|
101
|
+
|
102
|
+
require 'yolodice_client'
|
103
|
+
require 'pp'
|
104
|
+
|
105
|
+
auth_key = 'cPFVHENWNjs5UKNXXynDSWiRkBEph8hcrjHKkXK5SW9QHxx7i4jC'
|
106
|
+
yd = YolodiceClient.new
|
107
|
+
yd.connect
|
108
|
+
user = yd.authenticate auth_key
|
109
|
+
user_data = yd.read_user_data selector: {id: user['id']}
|
110
|
+
puts "Your account balance is: #{user_data['balance']} satoshis."
|
111
|
+
10.times do
|
112
|
+
b = yd.create_bet attrs: {amount: 100, range: 'lo', target: 500000}
|
113
|
+
puts "Bet profit: #{b['profit']}"
|
114
|
+
end
|
115
|
+
user_data = yd.read_user_data selector: {id: user['id']}
|
116
|
+
puts "Your account balance is: #{user_data['balance']} satoshis."
|
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
require 'thread'
|
4
|
+
require 'logger'
|
5
|
+
require 'json'
|
6
|
+
require 'bitcoin'
|
7
|
+
|
8
|
+
##
|
9
|
+
# YolodiceClient is a simple JSON-RPC 2.0 client that connects to YOLOdice.com.
|
10
|
+
|
11
|
+
class YolodiceClient
|
12
|
+
|
13
|
+
# <tt>OpenSSL::SSL::SSLSocket</tt> object created by the <tt>connect</tt> method.
|
14
|
+
attr_reader :connection
|
15
|
+
|
16
|
+
##
|
17
|
+
# Initializes the client object. The method accepts an option hash with the following keys:
|
18
|
+
# * <tt>:host</tt> -- defaults to <tt>api.yolodice.com</tt>
|
19
|
+
# * <tt>:port</tt> -- defaults to <tt>4444</tt>
|
20
|
+
# * <tt>:ssl</tt> -- if SSL should be used, defaults to <tt>true</tt>
|
21
|
+
|
22
|
+
def initialize opts={}
|
23
|
+
@opts = {
|
24
|
+
host: 'api.yolodice.com',
|
25
|
+
port: 4444,
|
26
|
+
ssl: true
|
27
|
+
}.merge opts
|
28
|
+
|
29
|
+
@req_id_seq = 0
|
30
|
+
@current_requests = {}
|
31
|
+
@receive_queues = {}
|
32
|
+
@thread_semaphore = Mutex.new
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
##
|
37
|
+
# Sets a logger for the object.
|
38
|
+
|
39
|
+
def logger=(logger)
|
40
|
+
@log = logger
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
##
|
45
|
+
# Connects to the host.
|
46
|
+
|
47
|
+
def connect
|
48
|
+
@connection = if @opts[:ssl]
|
49
|
+
log.debug "Connecting to #{@opts[:host]}:#{@opts[:port]} over SSL"
|
50
|
+
socket = TCPSocket.open @opts[:host], @opts[:port]
|
51
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new socket
|
52
|
+
ssl_socket.sync_close = true
|
53
|
+
ssl_socket.connect
|
54
|
+
ssl_socket
|
55
|
+
else
|
56
|
+
log.debug "Connecting to #{@opts[:host]}:#{@opts[:port]}"
|
57
|
+
TCPSocket.open @opts[:host], @opts[:port]
|
58
|
+
end
|
59
|
+
|
60
|
+
log.info "Connected to #{@opts[:host]}:#{@opts[:port]}"
|
61
|
+
# Start a thread that keeps listening on the socket
|
62
|
+
@listening_thread = Thread.new do
|
63
|
+
log.debug 'Listening thread started'
|
64
|
+
loop do
|
65
|
+
msg = @connection.gets
|
66
|
+
log.debug{ "<<< #{msg}" }
|
67
|
+
message = JSON.parse msg
|
68
|
+
if message['id'] && (message.has_key?('result') || message.has_key?('error'))
|
69
|
+
# definitealy a response
|
70
|
+
callback = @thread_semaphore.synchronize{ @current_requests.delete message['id'] }
|
71
|
+
raise Error, "Unknown id in response" unless callback
|
72
|
+
if callback.is_a? Integer
|
73
|
+
# it's a thread
|
74
|
+
@receive_queues[callback] << message
|
75
|
+
elsif callback.is_a? Proc
|
76
|
+
# A thread pool would be better.
|
77
|
+
Thread.new do
|
78
|
+
callback.call message
|
79
|
+
end
|
80
|
+
end
|
81
|
+
else
|
82
|
+
if message['id']
|
83
|
+
# It must be a request from the server. We do not support it yet.
|
84
|
+
else
|
85
|
+
# No id, it must be a notification then. This can be implemented later.
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# Start a thread that pings the server
|
91
|
+
@pinging_thread = Thread.new do
|
92
|
+
log.debug 'Pinging thread started'
|
93
|
+
loop do
|
94
|
+
sleep 30
|
95
|
+
call :ping
|
96
|
+
end
|
97
|
+
end
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
##
|
103
|
+
# Closes connection to the host.
|
104
|
+
|
105
|
+
def close
|
106
|
+
log.debug "Closing connection"
|
107
|
+
# Stop threads
|
108
|
+
@connection.close
|
109
|
+
@listening_thread.exit
|
110
|
+
@pinging_thread.exit
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
##
|
116
|
+
# Authenticates the connection by requesting a challenge message, signing it and sending the response back.
|
117
|
+
#
|
118
|
+
# Parameters:
|
119
|
+
# * <tt>auth_key</tt> -- Base58 encoded private key for the API key
|
120
|
+
#
|
121
|
+
# Returns
|
122
|
+
# * <tt>false</tt> if authentication fails,
|
123
|
+
# * user object (Hash with public user attributes) when authentication succeeds.
|
124
|
+
|
125
|
+
def authenticate auth_key
|
126
|
+
auth_key = Bitcoin::Key.from_base58(auth_key) unless auth_key.is_a?(Bitcoin::Key)
|
127
|
+
challenge = generate_auth_challenge
|
128
|
+
user = auth_by_address address: auth_key.addr, signature: auth_key.sign_message(challenge)
|
129
|
+
raise Error, "Authentication failed" unless user
|
130
|
+
log.debug "Authenticated as user #{user['name']}(#{user['id']})"
|
131
|
+
user
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
##
|
136
|
+
# Calls an arbitrary method on the server.
|
137
|
+
#
|
138
|
+
# Parameters:
|
139
|
+
# * <tt>method</tt> -- method name,
|
140
|
+
# * <tt>*arguments</tt> -- any arguments for the method, will be passed as the <tt>params</tt> object (optional),
|
141
|
+
# * <tt>&blk</tt> -- a callback (optional) to be called upon receiving a response for async calls. The callback will receive the response object.
|
142
|
+
|
143
|
+
def call method, *arguments, &blk
|
144
|
+
raise Error, "Not connected" unless @connection && !@connection.closed?
|
145
|
+
params = if arguments.count == 0
|
146
|
+
nil
|
147
|
+
elsif arguments.is_a?(Array) && arguments[0].is_a?(Hash)
|
148
|
+
arguments[0]
|
149
|
+
else
|
150
|
+
arguments
|
151
|
+
end
|
152
|
+
id = @thread_semaphore.synchronize{ @req_id_seq += 1 }
|
153
|
+
request = {
|
154
|
+
id: id,
|
155
|
+
method: method
|
156
|
+
}
|
157
|
+
request[:params] = params if params != nil
|
158
|
+
if blk
|
159
|
+
@thread_semaphore.synchronize{ @current_requests[id] = blk }
|
160
|
+
log.debug{ "Calling remote method #{method}(#{params.inspect if params != nil}) with an async callback" }
|
161
|
+
log.debug{ ">>> #{request.to_json}" }
|
162
|
+
@connection.puts request.to_json
|
163
|
+
nil
|
164
|
+
else
|
165
|
+
# a regular blocking request
|
166
|
+
@thread_semaphore.synchronize{ @current_requests[id] = Thread.current.object_id }
|
167
|
+
queue = (@receive_queues[Thread.current.object_id] ||= Queue.new)
|
168
|
+
log.debug{ "Calling remote method #{method}(#{params.inspect if params != nil})" }
|
169
|
+
log.debug{ ">>> #{request.to_json}" }
|
170
|
+
@connection.puts request.to_json
|
171
|
+
response = queue.pop
|
172
|
+
if response.has_key? 'result'
|
173
|
+
response['result']
|
174
|
+
elsif response['error']
|
175
|
+
raise RemoteError.new response['error']
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
##
|
182
|
+
# Overloading the <tt>method_missing</tt> gives a convenience way to call server-side methods. This method calls the <tt>call</tt> with the same set of arguments.
|
183
|
+
|
184
|
+
def method_missing method, *args, &blk
|
185
|
+
call method, *args, &blk
|
186
|
+
end
|
187
|
+
|
188
|
+
def log
|
189
|
+
# no logging by default
|
190
|
+
@log ||= Logger.new '/dev/null'
|
191
|
+
end
|
192
|
+
|
193
|
+
private :log
|
194
|
+
|
195
|
+
|
196
|
+
##
|
197
|
+
# Thrown whenever an error in the client occurs.
|
198
|
+
|
199
|
+
class Error < StandardError; end
|
200
|
+
|
201
|
+
|
202
|
+
##
|
203
|
+
# Thrown when an error is received from the server. <tt>RemoteError</tt> has two extra attributes: <tt>code</tt> and <tt>data</tt> that correspond to the values returned in the error object in server response.
|
204
|
+
|
205
|
+
class RemoteError < StandardError
|
206
|
+
|
207
|
+
# Error code, as returned from the server.
|
208
|
+
attr_accessor :code
|
209
|
+
|
210
|
+
# Optional data object, if returned by the server.
|
211
|
+
attr_accessor :data
|
212
|
+
|
213
|
+
def initialize(error_obj = {'code' => -1, 'message' => "RPC Error"})
|
214
|
+
@code = error_obj['code'] || -1
|
215
|
+
@data = error_obj['data'] if error_obj['data']
|
216
|
+
super "#{@code}: #{error_obj['message']}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yolodice-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ethan_nx
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-02-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bitcoin-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.0.8
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.0.8
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: hashie
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.5'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 3.5.1
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.5'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.5.1
|
47
|
+
description: A simple JSON-RPC2 client dedicated for YOLOdice.com API.
|
48
|
+
email:
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- LICENSE.md
|
54
|
+
- README.md
|
55
|
+
- lib/yolodice_client.rb
|
56
|
+
homepage: https://github.com/ethan-nx/yolodice-client
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata: {}
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 2.5.1
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: Ruby API client for YOLOdice.com
|
80
|
+
test_files: []
|