yolodice-client 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +7 -0
  3. data/README.md +116 -0
  4. data/lib/yolodice_client.rb +220 -0
  5. 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: []