utxoracle 0.0.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 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: []