utxoracle 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e06742e7448c182f3f2399e200c50e0563f3d8672433d942028cbc7c1827f60b
4
+ data.tar.gz: 33f2c9d1d7e496664c84a682e3b0d09a0971f152f7f351b59d7cdeb0baa48bba
5
+ SHA512:
6
+ metadata.gz: b64d8799da84d9ee041e61548adcf889067bd0bcef7b5331d59e43aca4bec7a4075d6bdd0773939132d9b8dec4b428e60a05be9bf9b720ecfec74b40b805e74f
7
+ data.tar.gz: caeb20a9d134903c29cccfd3bd03697a1bcf5b56452762ea383850b9423a4bc056d7ac77e4d18ed6f455d7765e21935421172ccd2ab3ca733298545792a03628
data/.gitignore ADDED
@@ -0,0 +1,59 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
57
+
58
+ /Gemfile.lock
59
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rspec_status ADDED
@@ -0,0 +1,8 @@
1
+ example_id | status | run_time |
2
+ -------------------------------------- | ------ | --------------- |
3
+ ./spec/utxoracle/oracle_spec.rb[1:1:1] | failed | 0.00966 seconds |
4
+ ./spec/utxoracle/rpc_spec.rb[1:1:1] | passed | 0.00025 seconds |
5
+ ./spec/utxoracle/rpc_spec.rb[1:2] | passed | 0.01109 seconds |
6
+ ./spec/utxoracle/rpc_spec.rb[1:3] | passed | 0.00028 seconds |
7
+ ./spec/utxoracle/rpc_spec.rb[1:4] | passed | 0.00214 seconds |
8
+ ./spec/utxoracle_spec.rb[1:1] | passed | 0.00127 seconds |
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Carolina-Bitcoin-Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # UTXOracle.rb
2
+ Ruby implementation of https://utxo.live/oracle/.
3
+
4
+ (Original implementation by [SteveSimple](https://twitter.com/SteveSimple) & [DanielLHinton](https://twitter.com/DanielLHinton) who discovered and built the first offline bitcoin price oracle.)
5
+
6
+ ## Table of contents
7
+
8
+ - [Installation](#installation)
9
+ - [Usage](#usage)
10
+ * [Requiring the gem](#requiring-the-gem)
11
+ * [Fetching Price](#fetching-price)
12
+ - [Development](#development)
13
+ - [Contributing](#contributing)
14
+ - [License](#license)
15
+
16
+
17
+ ## Installation
18
+
19
+ Install the gem and add to the application's Gemfile by executing:
20
+
21
+ $ bundle add utxoracle
22
+
23
+ If bundler is not being used to manage dependencies, install the gem by executing:
24
+
25
+ $ gem install utxoracle
26
+
27
+ ## Usage
28
+
29
+ ### Requiring the gem
30
+
31
+ All examples below assume that the gem has been required.
32
+
33
+ ```ruby
34
+ require 'utxoracle'
35
+ ```
36
+
37
+
38
+ ### Fetching price
39
+
40
+ #### Using a raw bitcoin node
41
+ ```ruby
42
+ provider = Utxoracle::RawBitcoinNode.new("aUser", "aPassword", "127.0.0.1", 8332)
43
+ ```
44
+
45
+ #### Using mempool.space node
46
+ ```ruby
47
+ provider = Utxoracle::MempoolDotSpace.new
48
+ ```
49
+
50
+ ####
51
+ ```ruby
52
+ oracle = Utxoracle::Oracle.new(provider)
53
+ oracle.price("2023-10-30")
54
+ 34840
55
+ ```
56
+
57
+ ## Development
58
+
59
+ After checking out the repo, run `bundle i` to install dependencies.
60
+
61
+ To install this gem onto your local machine, run `bundle exec rake install`.
62
+
63
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
64
+
65
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,
66
+ which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file
67
+ to [rubygems.org](https://rubygems.org).
68
+
69
+ The health and maintainability of the codebase is ensured through a set of
70
+ Rake tasks to test, lint and audit the gem for security vulnerabilities and documentation:
71
+
72
+ ```
73
+ rake build # Build utxoracle-0.0.1.gem into the pkg directory
74
+ rake build:checksum # Generate SHA512 checksum if utxoracle-0.0.1.gem into the checksums directory
75
+ rake clean # Remove any temporary products
76
+ rake clobber # Remove any generated files
77
+ rake install # Build and install utxoracle-0.0.1.gem into system gems
78
+ rake install:local # Build and install utxoracle-0.0.1.gem into system gems without network access
79
+ rake release[remote] # Create tag v0.0.1 and build and push utxoracle-0.0.1.gem to rubygems.org
80
+ rake rubocop # Run RuboCop
81
+ rake rubocop:autocorrect # Autocorrect RuboCop offenses (only when it's safe)
82
+ rake rubocop:autocorrect_all # Autocorrect RuboCop offenses (safe and unsafe)
83
+ rake spec # Run RSpec code examples
84
+ ```
85
+
86
+ ## Contributing
87
+
88
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Carolina-Bitcoin-Project/UTXOracle.
89
+
90
+ ## License
91
+
92
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rubocop/rake_task'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ RuboCop::RakeTask.new(:rubocop) do |t|
7
+ t.options = ['--display-cop-names']
8
+ end
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'utxoracle'
6
+ require 'pry'
7
+
8
+ Pry.start
@@ -0,0 +1,352 @@
1
+ require 'time'
2
+ require_relative 'rpc'
3
+ require_relative 'providers/raw_bitcoin_node'
4
+
5
+ module Utxoracle
6
+ class Oracle
7
+ SECONDS_IN_A_DAY = 60 * 60 * 24
8
+ FOUR_HOURS = 14_400
9
+ MAINNET_PORT = 8332
10
+ TESTNET_PORT = 18_332
11
+ NUMBER_OF_BINS = 2401
12
+
13
+ def initialize(provider, log = false)
14
+ @provider = provider
15
+ @log = log
16
+ @round_usd_stencil = build_round_usd_stencil
17
+ @cache = {}
18
+ end
19
+
20
+ def price(requested_date)
21
+ unless validate_date(requested_date)
22
+ puts "Invalid date.\nEarliest available: 2020-07-26.\nLatest available #{Date.today}.\nFormat: YYYY-MM-DD."
23
+ return
24
+ end
25
+
26
+ if price_estimate = @cache[requested_date]
27
+ puts "price_estimate is #{price_estimate}"
28
+ return price_estimate
29
+ end
30
+
31
+ @requested_date = Time.parse requested_date.tr('\n', '')
32
+ @cache[requested_date] = run
33
+ @cache[requested_date]
34
+ end
35
+
36
+ private
37
+
38
+ # TODO: - Refactor.
39
+ # We need this code to be "plug and play" - so stats wizards can
40
+ # quickly iterate on different models. Or even chat-gpt to cross
41
+ # reference with historical exchange prices.
42
+ def run
43
+ block_count = @provider.getblockcount
44
+ block_hash = @provider.getblockhash(block_count)
45
+ blockheader = @provider.getblockheader(block_hash, true)
46
+
47
+ time_datetime = Time.at(blockheader['time']).utc
48
+ latest_year = time_datetime.year
49
+ latest_month = time_datetime.month
50
+ latest_day = time_datetime.day
51
+ latest_utc_midnight = Time.new(latest_year, latest_month, latest_day).utc
52
+
53
+ latest_time_in_seconds = blockheader['time']
54
+ yesterday_seconds = latest_time_in_seconds - SECONDS_IN_A_DAY
55
+ latest_price_day = Time.at(yesterday_seconds).utc
56
+ latest_price_date = latest_price_day.utc.strftime('%Y-%m-%d')
57
+
58
+ price_day_seconds = @requested_date.to_i - FOUR_HOURS
59
+ price_day_date_utc = @requested_date
60
+
61
+ seconds_since_price_day = latest_time_in_seconds - price_day_seconds
62
+ blocks_ago_estimate = (144 * seconds_since_price_day.to_f / SECONDS_IN_A_DAY.to_f).round
63
+ price_day_block_estimate = (block_count - blocks_ago_estimate).to_i
64
+
65
+ block_hash_b = @provider.getblockhash(price_day_block_estimate)
66
+ block_header = @provider.getblockheader(block_hash_b, true)
67
+ time_in_seconds = block_header['time']
68
+
69
+ seconds_difference = time_in_seconds - price_day_seconds
70
+ block_jump_estimate = (144.0 * seconds_difference / SECONDS_IN_A_DAY).round
71
+
72
+ last_estimate = 0
73
+ last_last_estimate = 0
74
+ while block_jump_estimate > 6 && block_jump_estimate != last_last_estimate
75
+ last_last_estimate = last_estimate
76
+ last_estimate = block_jump_estimate
77
+
78
+ price_day_block_estimate -= block_jump_estimate
79
+ block_hash_b = @provider.getblockhash(price_day_block_estimate)
80
+ block_header = @provider.getblockheader(block_hash_b, true)
81
+
82
+ time_in_seconds = block_header['time']
83
+ seconds_difference = time_in_seconds - price_day_seconds
84
+ block_jump_estimate = (144.0 * seconds_difference / SECONDS_IN_A_DAY).round
85
+ end
86
+
87
+ if time_in_seconds > price_day_seconds
88
+ while time_in_seconds > price_day_seconds
89
+ price_day_block_estimate -= 1
90
+ block_hash_b = @provider.getblockhash(price_day_block_estimate)
91
+ block_header = @provider.getblockheader(block_hash_b, true)
92
+ time_in_seconds = block_header['time']
93
+ end
94
+
95
+ price_day_block_estimate += 1
96
+ elsif time_in_seconds < price_day_seconds
97
+ while time_in_seconds < price_day_seconds
98
+ price_day_block_estimate += 1
99
+ block_hash_b = @provider.getblockhash(price_day_block_estimate)
100
+ block_header = @provider.getblockheader(block_hash_b, true)
101
+ time_in_seconds = block_header['time']
102
+ end
103
+ end
104
+
105
+ price_day_block = price_day_block_estimate
106
+
107
+ first_bin_value = -6
108
+ last_bin_value = 6
109
+ range_bin_values = last_bin_value - first_bin_value
110
+
111
+ output_bell_curve_bins = [0.0]
112
+
113
+ for exponent in -6..5
114
+ for bin_width in 0..199
115
+ bin_value = 10**(exponent + bin_width / 200.0).to_f
116
+ output_bell_curve_bins << bin_value
117
+ end
118
+ end
119
+
120
+ output_bell_curve_bin_counts = []
121
+ for n in 0..NUMBER_OF_BINS - 1
122
+ output_bell_curve_bin_counts << 0.0
123
+ end
124
+
125
+ puts("Reading all blocks on #{price_day_date_utc}...") if @log
126
+ puts('This will take a few minutes (~144 blocks)...') if @log
127
+ puts("Height\tTime(utc)\t Completion %") if @log
128
+
129
+ block_height = price_day_block_estimate
130
+ block_hash_b = @provider.getblockhash(block_height)
131
+ block_b = @provider.getblock(block_hash_b, 2)
132
+
133
+ time = Time.at(block_b['time']).utc
134
+ time_in_seconds = block_b['time']
135
+
136
+ hour = time.hour
137
+ day = time.day
138
+ minute = time.min
139
+
140
+ target = day
141
+ blocks_on_this_day = 0
142
+
143
+ while target == day
144
+ blocks_on_this_day += 1
145
+
146
+ progress_estimate = 100.0 * (hour + minute / 60) / 24.0
147
+ puts("#{block_height}\t#{time.strftime('%H:%M:%S')}\t#{progress_estimate}") if @log
148
+
149
+ for tx in block_b['tx']
150
+ outputs = tx['vout']
151
+ for output in outputs
152
+
153
+ amount = output['value']
154
+ next unless 1e-6 < amount && amount < 1e6
155
+
156
+ amount_log = Math.log10 amount
157
+
158
+ percent_in_range = (amount_log - first_bin_value).to_f / range_bin_values.to_f
159
+ bin_number_est = (percent_in_range * NUMBER_OF_BINS).to_i
160
+
161
+ bin_number_est += 1 while output_bell_curve_bins[bin_number_est] <= amount
162
+ bin_number = bin_number_est - 1
163
+
164
+ output_bell_curve_bin_counts[bin_number] += 1.0
165
+ end
166
+ end
167
+
168
+ block_height += 1
169
+ block_hash_b = @provider.getblockhash(block_height)
170
+ block_b = @provider.getblock(block_hash_b, 2)
171
+
172
+ time = Time.at(block_b['time']).utc
173
+ time_in_seconds = block_b['time']
174
+ hour = time.hour
175
+ day = time.day
176
+ minute = time.min
177
+ end
178
+
179
+ puts "blocks_on_this_day: #{blocks_on_this_day}" if @log
180
+
181
+ for n in 0..201 - 1
182
+ output_bell_curve_bin_counts[n] = 0.0
183
+ end
184
+
185
+ for n in 1601..NUMBER_OF_BINS - 1
186
+ output_bell_curve_bin_counts[n] = 0.0
187
+ end
188
+
189
+ for r in round_btc_bins
190
+ amount_above = output_bell_curve_bin_counts[r + 1]
191
+ amount_below = output_bell_curve_bin_counts[r - 1]
192
+ output_bell_curve_bin_counts[r] = 0.5 * (amount_above + amount_below).to_f
193
+ end
194
+
195
+ curve_sum = 0.0
196
+ for n in 201..1601 - 1
197
+ curve_sum += output_bell_curve_bin_counts[n]
198
+ end
199
+
200
+ for n in 201..1601 - 1
201
+ output_bell_curve_bin_counts[n] /= curve_sum
202
+ output_bell_curve_bin_counts[n] = 0.008 if output_bell_curve_bin_counts[n] > 0.008
203
+ end
204
+
205
+ best_slide = 0
206
+ best_slide_score = 0.0
207
+ total_score = 0.0
208
+ number_of_scores = 0
209
+
210
+ min_slide = -200
211
+ max_slide = 200
212
+
213
+ for slide in min_slide..max_slide - 1
214
+ shifted_curve = output_bell_curve_bin_counts.slice(201 + slide, 1401 + slide)
215
+
216
+ slide_score = 0.0
217
+ for n in 0..shifted_curve.count - 1
218
+ slide_score += shifted_curve[n] * @round_usd_stencil[n + 201]
219
+ end
220
+
221
+ total_score += slide_score
222
+ number_of_scores += 1
223
+
224
+ if slide_score > best_slide_score
225
+ best_slide_score = slide_score
226
+ best_slide = slide
227
+ end
228
+ end
229
+
230
+ usd100_in_btc_best = output_bell_curve_bins[801 + best_slide]
231
+ btc_in_usd_best = 100 / usd100_in_btc_best
232
+
233
+ neighbor_up = output_bell_curve_bin_counts.slice(201 + best_slide + 1, 1401 + best_slide + 1)
234
+ neighbor_up_score = 0.0
235
+ for n in 0..neighbor_up.count - 1
236
+ neighbor_up_score += (neighbor_up[n] * @round_usd_stencil[n + 201]).to_f
237
+ end
238
+
239
+ neighbor_down = output_bell_curve_bin_counts.slice(201 + best_slide - 1, 1401 + best_slide - 1)
240
+ neighbor_down_score = 0.0
241
+ for n in 0..neighbor_down.count - 1
242
+ neighbor_down_score += (neighbor_down[n] * @round_usd_stencil[n + 201]).to_f
243
+ end
244
+
245
+ best_neighbor = +1
246
+ neighbor_score = neighbor_up_score
247
+ if neighbor_down_score > neighbor_up_score
248
+ best_neighbor = -1
249
+ neighbor_score = neighbor_down_score
250
+ end
251
+
252
+ usd100_in_btc_2nd = output_bell_curve_bins[801 + best_slide + best_neighbor]
253
+ btc_in_usd_2nd = 100 / usd100_in_btc_2nd.to_f
254
+
255
+ # weight average the two usd price estimates
256
+ avg_score = total_score / number_of_scores.to_f
257
+ a1 = best_slide_score - avg_score
258
+ a2 = (neighbor_score - avg_score).abs # theoretically possible to be negative
259
+ w1 = a1 / (a1 + a2).to_f
260
+ w2 = a2 / (a1 + a2)
261
+ price_estimate = (w1 * btc_in_usd_best + w2 * btc_in_usd_2nd).to_i
262
+
263
+ puts "price_estimate is #{price_estimate}" if @log
264
+
265
+ price_estimate
266
+ end
267
+
268
+ def build_round_usd_stencil
269
+ round_usd_stencil = []
270
+ for n in 0..NUMBER_OF_BINS
271
+ round_usd_stencil.append(0.0)
272
+ end
273
+
274
+ # fill the round usd stencil with the values found by the process mentioned above
275
+ round_usd_stencil[401] = 0.0005957955691168063 # $1
276
+ round_usd_stencil[402] = 0.0004454790662303128 # (next one for tx/atm fees)
277
+ round_usd_stencil[429] = 0.0001763099393598914 # $1.50
278
+ round_usd_stencil[430] = 0.0001851801497144573
279
+ round_usd_stencil[461] = 0.0006205616481885794 # $2
280
+ round_usd_stencil[462] = 0.0005985696860584984
281
+ round_usd_stencil[496] = 0.0006919505728046619 # $3
282
+ round_usd_stencil[497] = 0.0008912933078342840
283
+ round_usd_stencil[540] = 0.0009372916238804205 # $5
284
+ round_usd_stencil[541] = 0.0017125522985034724 # (larger needed range for fees)
285
+ round_usd_stencil[600] = 0.0021702347223143030
286
+ round_usd_stencil[601] = 0.0037018622326411380 # $10
287
+ round_usd_stencil[602] = 0.0027322168706743802
288
+ round_usd_stencil[603] = 0.0016268322583097678 # (larger needed range for fees)
289
+ round_usd_stencil[604] = 0.0012601953416497664
290
+ round_usd_stencil[661] = 0.0041425242880295460 # $20
291
+ round_usd_stencil[662] = 0.0039247767475640830
292
+ round_usd_stencil[696] = 0.0032399441632017228 # $30
293
+ round_usd_stencil[697] = 0.0037112959007355585
294
+ round_usd_stencil[740] = 0.0049921908828370000 # $50
295
+ round_usd_stencil[741] = 0.0070636869018197105
296
+ round_usd_stencil[801] = 0.0080000000000000000 # $100
297
+ round_usd_stencil[802] = 0.0065431388282424440 # (larger needed range for fees)
298
+ round_usd_stencil[803] = 0.0044279509203361735
299
+ round_usd_stencil[861] = 0.0046132440551747015 # $200
300
+ round_usd_stencil[862] = 0.0043647851395531140
301
+ round_usd_stencil[896] = 0.0031980892880846567 # $300
302
+ round_usd_stencil[897] = 0.0034237641632481910
303
+ round_usd_stencil[939] = 0.0025995335505435034 # $500
304
+ round_usd_stencil[940] = 0.0032631930982226645 # (larger needed range for fees)
305
+ round_usd_stencil[941] = 0.0042753262790881080
306
+ round_usd_stencil[1001] = 0.0037699501474772350 # $1,000
307
+ round_usd_stencil[1002] = 0.0030872891064215764 # (larger needed range for fees)
308
+ round_usd_stencil[1003] = 0.0023237040836798163
309
+ round_usd_stencil[1061] = 0.0023671764210889895 # $2,000
310
+ round_usd_stencil[1062] = 0.0020106877104798474
311
+ round_usd_stencil[1140] = 0.0009099214128654502 # $3,000
312
+ round_usd_stencil[1141] = 0.0012008546799361498
313
+ round_usd_stencil[1201] = 0.0007862586076341524 # $10,000
314
+ round_usd_stencil[1202] = 0.0006900048077192579
315
+
316
+ round_usd_stencil
317
+ end
318
+
319
+ def round_btc_bins
320
+ [
321
+ 201, # 1k sats
322
+ 401, # 10k
323
+ 461, # 20k
324
+ 496, # 30k
325
+ 540, # 50k
326
+ 601, # 100k
327
+ 661, # 200k
328
+ 696, # 300k
329
+ 740, # 500k
330
+ 801, # 0.01 btc
331
+ 861, # 0.02
332
+ 896, # 0.03
333
+ 940, # 0.04
334
+ 1001, # 0.1
335
+ 1061, # 0.2
336
+ 1096, # 0.3
337
+ 1140, # 0.5
338
+ 1201 # 1 btc
339
+ ]
340
+ end
341
+
342
+ def validate_date(date)
343
+ y, m, d = date.split '-'
344
+ valid_format = Date.valid_date? y.to_i, m.to_i, d.to_i
345
+
346
+ valid_range = (Date.parse(date) > Date.parse('2020-7-26')) &&
347
+ (Date.parse(date) < Date.today)
348
+
349
+ valid_format && valid_range
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,25 @@
1
+ module Utxoracle
2
+ class Provider
3
+ def init
4
+ raise NoMethodError
5
+ end
6
+
7
+ def getblockcount
8
+ raise NoMethodError
9
+ end
10
+
11
+ def getblockhash
12
+ raise NoMethodError
13
+ end
14
+
15
+ def getblockheader
16
+ raise NoMethodError
17
+ end
18
+
19
+ def getblock
20
+ raise NoMethodError
21
+ end
22
+
23
+ # TODO: - Build a mechanism allowing for fallbacks; be fault tolerant to node, website, etc.
24
+ end
25
+ end
@@ -0,0 +1,69 @@
1
+ require_relative '../provider'
2
+ require_relative '../request'
3
+
4
+ # Unfortunately Mempool limits the number of tx's you can fetch in bulk,
5
+ # and throttles the number of requests the oracle takes to compute.
6
+ # Running this requires an Enterprise license.
7
+ module Utxoracle
8
+ class MempoolDotSpace < Provider
9
+ def initialize; end
10
+
11
+ def getblockcount
12
+ Request.send('https://mempool.space/api/blocks/tip/height').body.to_i
13
+ end
14
+
15
+ def getblockhash(height)
16
+ Request.send("https://mempool.space/api/block-height/#{height}").body
17
+ end
18
+
19
+ # Oracle.price needs blockheader['time']
20
+ def getblockheader(block_hash, _verbose = true)
21
+ hex_block_header = Request.send("https://mempool.space/api/block/#{block_hash}/header").body
22
+
23
+ # version = hex_to_int(reverse_byte_order(hex_block_header[0..7]))
24
+ # prev_block_hash = reverse_byte_order(hex_block_header[8..71])
25
+ # merkle_root = reverse_byte_order(hex_block_header[72..135])
26
+ timestamp = hex_to_int(reverse_byte_order(hex_block_header[136..143]))
27
+ # bits = reverse_byte_order(hex_block_header[144..151])
28
+ # nonce = hex_to_int(reverse_byte_order(hex_block_header[152..159]))
29
+
30
+ {
31
+ 'time' => timestamp
32
+ }
33
+ end
34
+
35
+ def getblock(block_hash, _verbosity = 2)
36
+ block = JSON.parse Request.send("https://mempool.space/api/block/#{block_hash}").body
37
+
38
+ # mempool API maps 'timestamp' to 'time'
39
+ block['time'] = block['timestamp']
40
+
41
+ # mempool API doesn't return tx. Set to empty array:
42
+ block['tx'] = []
43
+
44
+ # Get all tx_ids in the block
45
+ tx_ids = JSON.parse Request.send("https://mempool.space/api/block/#{block_hash}/txids").body
46
+
47
+ # Append vout to block['tx']
48
+ # ( requires mempool enterprise: 429 Too Many Requests)
49
+ tx_ids.each do |tx_id|
50
+ tx = JSON.parse Request.send("https://mempool.space/api/tx/#{tx_id}").body
51
+ block['tx'] << tx['vout']
52
+ end
53
+
54
+ block
55
+ end
56
+
57
+ private
58
+
59
+ # Reverse byte order in a hexadecimal string.
60
+ def reverse_byte_order(hex_string)
61
+ hex_string.scan(/../).reverse.join
62
+ end
63
+
64
+ # Convert hexadecimal string to an integer.
65
+ def hex_to_int(hex_string)
66
+ hex_string.to_i(16)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,26 @@
1
+ require_relative '../provider'
2
+ require_relative '../rpc'
3
+
4
+ module Utxoracle
5
+ class RawBitcoinNode < Provider
6
+ def initialize(rpcuser, rpcpassword, ip, port)
7
+ @rpc = Rpc.new("http://#{rpcuser}:#{rpcpassword}@#{ip}:#{port}")
8
+ end
9
+
10
+ def getblockcount
11
+ @rpc.getblockcount
12
+ end
13
+
14
+ def getblockhash(height)
15
+ @rpc.getblockhash(height)
16
+ end
17
+
18
+ def getblockheader(block_hash, verbose = true)
19
+ @rpc.getblockheader(block_hash, verbose)
20
+ end
21
+
22
+ def getblock(block_hash, verbosity = 2)
23
+ @rpc.getblock(block_hash, verbosity)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ require 'typhoeus'
2
+
3
+ class Request
4
+ def self.send(domain, method = :get, body = '', params = {}, headers = {})
5
+ request = Typhoeus::Request.new(
6
+ domain,
7
+ method:,
8
+ body:,
9
+ params:,
10
+ headers:
11
+ )
12
+
13
+ request.run
14
+ request.response
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module Utxoracle
6
+ class Rpc
7
+ def initialize(service_url)
8
+ @uri = URI.parse(service_url)
9
+ end
10
+
11
+ def method_missing(name, *args)
12
+ post_body = { 'method' => name, 'params' => args, 'id' => 'jsonrpc' }.to_json
13
+ resp = JSON.parse(http_post_request(post_body))
14
+ raise JSONRPCError, resp['error'] if resp['error']
15
+
16
+ resp['result']
17
+ end
18
+
19
+ def http_post_request(post_body)
20
+ http = Net::HTTP.new(@uri.host, @uri.port)
21
+ request = Net::HTTP::Post.new(@uri.request_uri)
22
+ request.basic_auth @uri.user, @uri.password
23
+ request.content_type = 'application/json'
24
+ request.body = post_body
25
+ http.request(request).body
26
+ end
27
+
28
+ class JSONRPCError < RuntimeError; end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Utxoracle
4
+ VERSION = '0.0.1'
5
+ end
data/lib/utxoracle.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utxoracle/rpc'
4
+ require_relative 'utxoracle/version'
5
+ require_relative 'utxoracle/oracle'
6
+ require_relative 'utxoracle/providers/mempool_dot_space'
7
+ require_relative 'utxoracle/providers/raw_bitcoin_node'
8
+
9
+ module Utxoracle
10
+ end
@@ -0,0 +1,33 @@
1
+ require 'simplecov'
2
+ require 'simplecov-console'
3
+ require 'pry'
4
+
5
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
6
+ [
7
+ SimpleCov::Formatter::HTMLFormatter,
8
+ SimpleCov::Formatter::Console
9
+ ]
10
+ )
11
+
12
+ unless ENV['COVERAGE'] == 'false'
13
+ SimpleCov.start do
14
+ root 'lib'
15
+ coverage_dir "#{Dir.pwd}/coverage"
16
+ end
17
+ end
18
+
19
+ require 'utxoracle'
20
+
21
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
22
+
23
+ RSpec.configure do |config|
24
+ # Enable flags like --only-failures and --next-failure
25
+ config.example_status_persistence_file_path = '.rspec_status'
26
+
27
+ # Disable RSpec exposing methods globally on `Module` and `main`
28
+ config.disable_monkey_patching!
29
+
30
+ config.expect_with :rspec do |c|
31
+ c.syntax = :expect
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # RSpec.describe Utxoracle::Oracle do
6
+ # let(:oracle) do
7
+ # described_class.new('aUser', 'aPassword', '127.0.0.1', '8332')
8
+ # end
9
+ #
10
+ # describe '.new' do
11
+ # it 'initialized an Rpc instance' do
12
+ # expect(Utxoracle::Rpc).to receive(:new)
13
+ # described_class.new('aUser', 'aPassword', '127.0.0.1', '8332')
14
+ # expect(oracle.class).to eq Utxoracle::Oracle
15
+ # end
16
+ # end
17
+ #
18
+ # # TODO: - test cache
19
+ # # TODO - test provider interface
20
+ # # TODO - test `price` return type
21
+ # end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Utxoracle::Rpc do
6
+ let(:rpc) do
7
+ described_class.new('http://foo:bar@127.0.0.1:8332')
8
+ end
9
+ describe '.new' do
10
+ it 'creates an instance of Rpc with given uri' do
11
+ expect(rpc.class).to eq(Utxoracle::Rpc)
12
+ end
13
+ end
14
+
15
+ it 'exposes http request interface' do
16
+ allow(rpc).to receive(:http_post_request).and_return(true)
17
+ expect(rpc.http_post_request('')).to eq true
18
+ end
19
+
20
+ it 'forwards methods over http' do
21
+ allow(rpc).to receive(:http_post_request).and_return("{\"result\":814521,\"error\":null,\"id\":\"jsonrpc\"}\n")
22
+ expect(rpc.getblockcount).to eq 814_521
23
+ end
24
+
25
+ it 'returns error from http endpoint when indicated' do
26
+ expect(rpc).to receive(:http_post_request).and_return("{\"result\":814521,\"error\":\"test error\",\"id\":\"jsonrpc\"}\n")
27
+ expect { rpc.test_rpc_call }.to raise_error(Utxoracle::Rpc::JSONRPCError)
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Utxoracle do
4
+ it 'has a version number' do
5
+ expect(Utxoracle::VERSION).not_to be_nil
6
+ end
7
+ end
data/utxoracle.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $:.unshift lib unless $:.include?(lib)
5
+
6
+ require 'utxoracle/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'utxoracle'
10
+ spec.version = Utxoracle::VERSION
11
+ spec.platform = Gem::Platform::RUBY
12
+ spec.authors = ['Keith Gardner']
13
+
14
+ spec.summary = 'Interface for UTXOracle.'
15
+ spec.description = 'Object oriented design for interacting with UTXOracle.'
16
+ spec.homepage = 'https://github.com/Carolina-Bitcoin-Project/UTXOracle'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = '>= 3.1.0'
19
+
20
+ spec.files = `git ls-files`.split("\n")
21
+ spec.bindir = 'exe'
22
+ spec.require_path = 'lib'
23
+
24
+ spec.add_dependency 'typhoeus'
25
+
26
+ spec.add_development_dependency 'pry'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'rubocop'
30
+ spec.add_development_dependency 'rubocop-rspec'
31
+ spec.add_development_dependency 'simplecov'
32
+ spec.add_development_dependency 'simplecov-console'
33
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: utxoracle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Keith Gardner
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-11-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: typhoeus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov-console
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Object oriented design for interacting with UTXOracle.
126
+ email:
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - ".gitignore"
132
+ - ".rspec"
133
+ - ".rspec_status"
134
+ - ".ruby-version"
135
+ - Gemfile
136
+ - LICENSE
137
+ - README.md
138
+ - Rakefile
139
+ - bin/console
140
+ - lib/utxoracle.rb
141
+ - lib/utxoracle/oracle.rb
142
+ - lib/utxoracle/provider.rb
143
+ - lib/utxoracle/providers/mempool_dot_space.rb
144
+ - lib/utxoracle/providers/raw_bitcoin_node.rb
145
+ - lib/utxoracle/request.rb
146
+ - lib/utxoracle/rpc.rb
147
+ - lib/utxoracle/version.rb
148
+ - spec/spec_helper.rb
149
+ - spec/utxoracle/oracle_spec.rb
150
+ - spec/utxoracle/rpc_spec.rb
151
+ - spec/utxoracle_spec.rb
152
+ - utxoracle.gemspec
153
+ homepage: https://github.com/Carolina-Bitcoin-Project/UTXOracle
154
+ licenses:
155
+ - MIT
156
+ metadata: {}
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: 3.1.0
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubygems_version: 3.4.10
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Interface for UTXOracle.
176
+ test_files: []