bitcoinrb 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -0
  4. data/README.md +3 -2
  5. data/bitcoinrb.gemspec +2 -2
  6. data/exe/bitcoinrbd +5 -0
  7. data/lib/bitcoin.rb +10 -1
  8. data/lib/bitcoin/bip85_entropy.rb +111 -0
  9. data/lib/bitcoin/block_header.rb +2 -0
  10. data/lib/bitcoin/ext.rb +5 -0
  11. data/lib/bitcoin/ext/json_parser.rb +46 -0
  12. data/lib/bitcoin/ext_key.rb +7 -3
  13. data/lib/bitcoin/key_path.rb +12 -5
  14. data/lib/bitcoin/message.rb +7 -0
  15. data/lib/bitcoin/message/base.rb +1 -0
  16. data/lib/bitcoin/message/cf_parser.rb +16 -0
  17. data/lib/bitcoin/message/cfcheckpt.rb +36 -0
  18. data/lib/bitcoin/message/cfheaders.rb +40 -0
  19. data/lib/bitcoin/message/cfilter.rb +35 -0
  20. data/lib/bitcoin/message/get_cfcheckpt.rb +29 -0
  21. data/lib/bitcoin/message/get_cfheaders.rb +24 -0
  22. data/lib/bitcoin/message/get_cfilters.rb +25 -0
  23. data/lib/bitcoin/message/version.rb +7 -0
  24. data/lib/bitcoin/mnemonic.rb +5 -5
  25. data/lib/bitcoin/network/peer.rb +9 -4
  26. data/lib/bitcoin/network/peer_discovery.rb +3 -1
  27. data/lib/bitcoin/node/cli.rb +14 -10
  28. data/lib/bitcoin/node/spv.rb +1 -1
  29. data/lib/bitcoin/out_point.rb +2 -0
  30. data/lib/bitcoin/payment_code.rb +92 -0
  31. data/lib/bitcoin/psbt/input.rb +5 -14
  32. data/lib/bitcoin/psbt/tx.rb +7 -12
  33. data/lib/bitcoin/rpc/bitcoin_core_client.rb +22 -12
  34. data/lib/bitcoin/rpc/request_handler.rb +1 -1
  35. data/lib/bitcoin/script/script.rb +5 -9
  36. data/lib/bitcoin/script/script_interpreter.rb +2 -3
  37. data/lib/bitcoin/secp256k1.rb +1 -0
  38. data/lib/bitcoin/secp256k1/rfc6979.rb +43 -0
  39. data/lib/bitcoin/secp256k1/ruby.rb +4 -35
  40. data/lib/bitcoin/store.rb +1 -0
  41. data/lib/bitcoin/store/chain_entry.rb +1 -0
  42. data/lib/bitcoin/store/utxo_db.rb +226 -0
  43. data/lib/bitcoin/tx.rb +3 -7
  44. data/lib/bitcoin/tx_in.rb +3 -4
  45. data/lib/bitcoin/util.rb +7 -0
  46. data/lib/bitcoin/version.rb +1 -1
  47. data/lib/bitcoin/wallet.rb +1 -0
  48. data/lib/bitcoin/wallet/account.rb +1 -0
  49. data/lib/bitcoin/wallet/base.rb +2 -2
  50. data/lib/bitcoin/wallet/master_key.rb +1 -0
  51. data/lib/bitcoin/wallet/utxo.rb +37 -0
  52. metadata +34 -20
@@ -1,4 +1,4 @@
1
- require 'rest-client'
1
+ require 'net/http'
2
2
 
3
3
  module Bitcoin
4
4
  module RPC
@@ -53,20 +53,30 @@ module Bitcoin
53
53
  :params => params,
54
54
  :id => 'jsonrpc'
55
55
  }
56
- post(server_url, @config[:timeout], @config[:open_timeout], data.to_json, content_type: :json) do |respdata, request, result|
57
- raise result.message if !result.kind_of?(Net::HTTPSuccess) && respdata.empty?
58
- response = JSON.parse(respdata.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*').encode('ISO-8859-1').force_encoding('UTF-8') })
59
- raise response['error'].to_s if response['error']
60
- response['result']
61
- end
56
+ uri = URI.parse(server_url)
57
+ http = Net::HTTP.new(uri.hostname, uri.port)
58
+ http.use_ssl = uri.scheme === "https"
59
+ request = Net::HTTP::Post.new(uri.path.empty? ? '/' : uri.path)
60
+ request.basic_auth(uri.user, uri.password)
61
+ request.content_type = 'application/json'
62
+ request.body = data.to_json
63
+ response = http.request(request)
64
+ body = response.body
65
+ response = Bitcoin::Ext::JsonParser.new(body.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*').encode('ISO-8859-1').force_encoding('UTF-8') }).parse
66
+ raise response['error'].to_s if response['error']
67
+ response['result']
62
68
  end
63
-
64
- def post(url, timeout, open_timeout, payload, headers={}, &block)
65
- RestClient::Request.execute(method: :post, url: url, timeout: timeout,
66
- open_timeout: open_timeout, payload: payload, headers: headers, &block)
69
+
70
+ # Call CLI command on Ruby-like method names.
71
+ # e.g. generate_to_address, send_to_address, get_wallet_info
72
+ def method_missing(name, *args)
73
+ if name.to_s.include?('_')
74
+ send(name.to_s.gsub('_', '').to_sym, args)
75
+ else
76
+ super
77
+ end
67
78
  end
68
79
 
69
80
  end
70
-
71
81
  end
72
82
  end
@@ -42,7 +42,7 @@ module Bitcoin
42
42
  nextblockhash: node.chain.next_hash(block_hash).rhex
43
43
  }
44
44
  else
45
- entry.header.to_payload.bth
45
+ entry.header.to_hex
46
46
  end
47
47
  end
48
48
 
@@ -6,6 +6,7 @@ module Bitcoin
6
6
  # bitcoin script
7
7
  class Script
8
8
  include Bitcoin::Opcodes
9
+ include Bitcoin::HexConverter
9
10
 
10
11
  attr_accessor :chunks
11
12
 
@@ -335,6 +336,7 @@ module Bitcoin
335
336
  when Integer
336
337
  opcode_to_name(c)
337
338
  when String
339
+ return c if c.empty?
338
340
  if c.pushdata?
339
341
  v = Opcodes.opcode_to_small_int(c.ord)
340
342
  if v
@@ -362,7 +364,7 @@ module Bitcoin
362
364
 
363
365
  # generate hash160 hash for payload
364
366
  def to_hash160
365
- Bitcoin.hash160(to_payload.bth)
367
+ Bitcoin.hash160(to_hex)
366
368
  end
367
369
 
368
370
  # script size
@@ -499,7 +501,7 @@ module Bitcoin
499
501
  end
500
502
 
501
503
  def to_h
502
- h = {asm: to_s, hex: to_payload.bth, type: type}
504
+ h = {asm: to_s, hex: to_hex, type: type}
503
505
  addrs = addresses
504
506
  unless addrs.empty?
505
507
  h[:req_sigs] = multisig? ? Bitcoin::Opcodes.opcode_to_small_int(chunks[0].bth.to_i(16)) :addrs.size
@@ -515,12 +517,6 @@ module Bitcoin
515
517
  (size > 0 && op_return?) || size > Bitcoin::MAX_SCRIPT_SIZE
516
518
  end
517
519
 
518
- # convert payload to hex data.
519
- # @return [String] script with hex format.
520
- def to_hex
521
- to_payload.bth
522
- end
523
-
524
520
  private
525
521
 
526
522
  # generate p2pkh address. if script dose not p2pkh, return nil.
@@ -553,7 +549,7 @@ module Bitcoin
553
549
  def bech32_addr
554
550
  segwit_addr = Bech32::SegwitAddr.new
555
551
  segwit_addr.hrp = Bitcoin.chain_params.bech32_hrp
556
- segwit_addr.script_pubkey = to_payload.bth
552
+ segwit_addr.script_pubkey = to_hex
557
553
  segwit_addr.addr
558
554
  end
559
555
 
@@ -61,7 +61,6 @@ module Bitcoin
61
61
  # Additional validation for spend-to-script-hash transactions
62
62
  if flag?(SCRIPT_VERIFY_P2SH) && script_pubkey.p2sh?
63
63
  return set_error(SCRIPT_ERR_SIG_PUSHONLY) unless script_sig.push_only?
64
- tmp = stack
65
64
  @stack = stack_copy
66
65
  raise 'stack cannot be empty.' if stack.empty?
67
66
  begin
@@ -76,7 +75,7 @@ module Bitcoin
76
75
  if flag?(SCRIPT_VERIFY_WITNESS) && redeem_script.witness_program?
77
76
  had_witness = true
78
77
  # The scriptSig must be _exactly_ a single push of the redeemScript. Otherwise we reintroduce malleability.
79
- return set_error(SCRIPT_ERR_WITNESS_MALLEATED_P2SH) unless script_sig == (Bitcoin::Script.new << redeem_script.to_payload.bth)
78
+ return set_error(SCRIPT_ERR_WITNESS_MALLEATED_P2SH) unless script_sig == (Bitcoin::Script.new << redeem_script.to_hex)
80
79
 
81
80
  version, program = redeem_script.witness_data
82
81
  return false unless verify_witness_program(witness, version, program)
@@ -147,7 +146,7 @@ module Bitcoin
147
146
  op_count = 0
148
147
 
149
148
  script.chunks.each_with_index do |c, index|
150
- need_exec = !flow_stack.include?(false)
149
+ need_exec = flow_stack.rindex(false).nil?
151
150
 
152
151
  return set_error(SCRIPT_ERR_PUSH_SIZE) if c.pushdata? && c.pushed_data.bytesize > MAX_SCRIPT_ELEMENT_SIZE
153
152
 
@@ -6,6 +6,7 @@ module Bitcoin
6
6
 
7
7
  autoload :Ruby, 'bitcoin/secp256k1/ruby'
8
8
  autoload :Native, 'bitcoin/secp256k1/native'
9
+ autoload :RFC6979, 'bitcoin/secp256k1/rfc6979'
9
10
 
10
11
  end
11
12
 
@@ -0,0 +1,43 @@
1
+ module Bitcoin
2
+ module Secp256k1
3
+ module RFC6979
4
+
5
+ INITIAL_V = '0101010101010101010101010101010101010101010101010101010101010101'.htb
6
+ INITIAL_K = '0000000000000000000000000000000000000000000000000000000000000000'.htb
7
+ ZERO_B = '00'.htb
8
+ ONE_B = '01'.htb
9
+
10
+ module_function
11
+
12
+ # generate temporary key k to be used when ECDSA sign.
13
+ # https://tools.ietf.org/html/rfc6979#section-3.2
14
+ # @param [String] key_data a data contains private key and message.
15
+ # @param [String] extra_entropy extra entropy with binary format.
16
+ # @return [Integer] a nonce.
17
+ def generate_rfc6979_nonce(key_data, extra_entropy)
18
+ v = INITIAL_V # 3.2.b
19
+ k = INITIAL_K # 3.2.c
20
+ # 3.2.d
21
+ k = Bitcoin.hmac_sha256(k, v + ZERO_B + key_data + extra_entropy)
22
+ # 3.2.e
23
+ v = Bitcoin.hmac_sha256(k, v)
24
+ # 3.2.f
25
+ k = Bitcoin.hmac_sha256(k, v + ONE_B + key_data + extra_entropy)
26
+ # 3.2.g
27
+ v = Bitcoin.hmac_sha256(k, v)
28
+ # 3.2.h
29
+ t = ''
30
+ 10000.times do
31
+ v = Bitcoin.hmac_sha256(k, v)
32
+ t = (t + v)
33
+ t_num = t.bth.to_i(16)
34
+ return t_num if 1 <= t_num && t_num < Bitcoin::Secp256k1::GROUP.order
35
+ k = Bitcoin.hmac_sha256(k, v + '00'.htb)
36
+ v = Bitcoin.hmac_sha256(k, v)
37
+ end
38
+ raise 'A valid nonce was not found.'
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -12,8 +12,8 @@ module Bitcoin
12
12
  private_key = 1 + SecureRandom.random_number(GROUP.order - 1)
13
13
  public_key = GROUP.generator.multiply_by_scalar(private_key)
14
14
  privkey = ECDSA::Format::IntegerOctetString.encode(private_key, 32)
15
- pubkey = ECDSA::Format::PointOctetString.encode(public_key, compression: compressed)
16
- [privkey.bth, pubkey.bth]
15
+ pubkey = public_key.to_hex(compressed)
16
+ [privkey.bth, pubkey]
17
17
  end
18
18
 
19
19
  # generate bitcoin key object
@@ -24,7 +24,7 @@ module Bitcoin
24
24
 
25
25
  def generate_pubkey(privkey, compressed: true)
26
26
  public_key = ECDSA::Group::Secp256k1.generator.multiply_by_scalar(privkey.to_i(16))
27
- ECDSA::Format::PointOctetString.encode(public_key, compression: compressed).bth
27
+ public_key.to_hex(compressed)
28
28
  end
29
29
 
30
30
  # sign data.
@@ -35,7 +35,7 @@ module Bitcoin
35
35
  privkey = privkey.htb
36
36
  private_key = ECDSA::Format::IntegerOctetString.decode(privkey)
37
37
  extra_entropy ||= ''
38
- nonce = generate_rfc6979_nonce(data, privkey, extra_entropy)
38
+ nonce = RFC6979.generate_rfc6979_nonce(privkey + data, extra_entropy)
39
39
 
40
40
  # port form ecdsa gem.
41
41
  r_point = GROUP.new_point(nonce)
@@ -87,37 +87,6 @@ module Bitcoin
87
87
  end
88
88
  end
89
89
 
90
- INITIAL_V = '0101010101010101010101010101010101010101010101010101010101010101'.htb
91
- INITIAL_K = '0000000000000000000000000000000000000000000000000000000000000000'.htb
92
- ZERO_B = '00'.htb
93
- ONE_B = '01'.htb
94
-
95
- # generate temporary key k to be used when ECDSA sign.
96
- # https://tools.ietf.org/html/rfc6979#section-3.2
97
- def generate_rfc6979_nonce(data, privkey, extra_entropy)
98
- v = INITIAL_V # 3.2.b
99
- k = INITIAL_K # 3.2.c
100
- # 3.2.d
101
- k = Bitcoin.hmac_sha256(k, v + ZERO_B + privkey + data + extra_entropy)
102
- # 3.2.e
103
- v = Bitcoin.hmac_sha256(k, v)
104
- # 3.2.f
105
- k = Bitcoin.hmac_sha256(k, v + ONE_B + privkey + data + extra_entropy)
106
- # 3.2.g
107
- v = Bitcoin.hmac_sha256(k, v)
108
- # 3.2.h
109
- t = ''
110
- 10000.times do
111
- v = Bitcoin.hmac_sha256(k, v)
112
- t = (t + v)
113
- t_num = t.bth.to_i(16)
114
- return t_num if 1 <= t_num && t_num < GROUP.order
115
- k = Bitcoin.hmac_sha256(k, v + '00'.htb)
116
- v = Bitcoin.hmac_sha256(k, v)
117
- end
118
- raise 'A valid nonce was not found.'
119
- end
120
90
  end
121
-
122
91
  end
123
92
  end
@@ -6,6 +6,7 @@ module Bitcoin
6
6
  autoload :DB, 'bitcoin/store/db'
7
7
  autoload :SPVChain, 'bitcoin/store/spv_chain'
8
8
  autoload :ChainEntry, 'bitcoin/store/chain_entry'
9
+ autoload :UtxoDB, 'bitcoin/store/utxo_db'
9
10
 
10
11
  end
11
12
  end
@@ -3,6 +3,7 @@ module Bitcoin
3
3
 
4
4
  # wrap a block header object with extra data.
5
5
  class ChainEntry
6
+ include Bitcoin::HexConverter
6
7
 
7
8
  attr_reader :header
8
9
  attr_reader :height
@@ -0,0 +1,226 @@
1
+ require 'leveldb-native'
2
+
3
+ module Bitcoin
4
+ module Store
5
+ class UtxoDB
6
+
7
+ KEY_PREFIX = {
8
+ out_point: 'o', # key: out_point(tx_hash and index), value: Utxo
9
+ script: 's', # key: script_pubkey and out_point(tx_hash and index), value: Utxo
10
+ height: 'h', # key: block_height and out_point, value: Utxo
11
+ tx_hash: 't', # key: tx_hash of transaction, value: [block_height, tx_index]
12
+ block: 'b', # key: block_height and tx_index, value: tx_hash
13
+ tx_payload: 'p', # key: tx_hash, value: Tx
14
+ }
15
+
16
+ attr_reader :level_db, :logger
17
+
18
+ def initialize(path = "#{Bitcoin.base_dir}/db/utxo")
19
+ FileUtils.mkdir_p(path)
20
+ @level_db = ::LevelDBNative::DB.new(path)
21
+ @logger = Bitcoin::Logger.create(:debug)
22
+ end
23
+
24
+ def close
25
+ level_db.close
26
+ end
27
+
28
+ # Save payload of a transaction into db
29
+ #
30
+ # @param [String] tx_hash
31
+ # @param [String] tx_payload
32
+ def save_tx(tx_hash, tx_payload)
33
+ logger.info("UtxoDB#save_tx:#{[tx_hash, tx_payload]}")
34
+ level_db.batch do
35
+ # tx_hash -> [block_height, tx_index]
36
+ key = KEY_PREFIX[:tx_payload] + tx_hash
37
+ level_db.put(key, tx_payload)
38
+ end
39
+ end
40
+
41
+ # Save tx position (block height and index in the block) into db
42
+ # When node receives `header` message, node should call save_tx_position to store block height and its index.
43
+ #
44
+ # @param [String] tx_hash
45
+ # @param [Integer] block_height
46
+ # @param [Integer] tx_index
47
+ def save_tx_position(tx_hash, block_height, tx_index)
48
+ logger.info("UtxoDB#save_tx_position:#{[tx_hash, block_height, tx_index]}")
49
+ level_db.batch do
50
+ # tx_hash -> [block_height, tx_index]
51
+ key = KEY_PREFIX[:tx_hash] + tx_hash
52
+ level_db.put(key, [block_height, tx_index].pack('N2').bth)
53
+
54
+ # block_hash and tx_index -> tx_hash
55
+ key = KEY_PREFIX[:block] + [block_height, tx_index].pack('N2').bth
56
+ level_db.put(key, tx_hash)
57
+ end
58
+ end
59
+
60
+ # Save utxo into db
61
+ #
62
+ # @param [Bitcoin::OutPoint] out_point
63
+ # @param [Double] value
64
+ # @param [Bitcoin::Script] script_pubkey
65
+ # @param [Integer] block_height
66
+ def save_utxo(out_point, value, script_pubkey, block_height=nil)
67
+ logger.info("UtxoDB#save_utxo:#{[out_point, value, script_pubkey, block_height]}")
68
+ level_db.batch do
69
+ utxo = Bitcoin::Wallet::Utxo.new(out_point.tx_hash, out_point.index, value, script_pubkey, block_height)
70
+ payload = utxo.to_payload
71
+
72
+ # out_point
73
+ key = KEY_PREFIX[:out_point] + out_point.to_hex
74
+ return if level_db.contains?(key)
75
+ level_db.put(key, payload)
76
+
77
+ # script_pubkey
78
+ if script_pubkey
79
+ key = KEY_PREFIX[:script] + script_pubkey.to_hex + out_point.to_hex
80
+ level_db.put(key, payload)
81
+ end
82
+
83
+ # height
84
+ unless block_height.nil?
85
+ key = KEY_PREFIX[:height] + [block_height].pack('N').bth + out_point.to_hex
86
+ level_db.put(key, payload)
87
+ end
88
+
89
+ utxo
90
+ end
91
+ end
92
+
93
+ # Get transaction stored via save_tx and save_tx_position
94
+ #
95
+ # @param [string] tx_hash
96
+ # @return [block_height, tx_index, tx_payload]
97
+ def get_tx(tx_hash)
98
+ key = KEY_PREFIX[:tx_hash] + tx_hash
99
+ return [] unless level_db.contains?(key)
100
+ block_height, tx_index = level_db.get(key).htb.unpack('N2')
101
+ key = KEY_PREFIX[:tx_payload] + tx_hash
102
+ tx_payload = level_db.get(key)
103
+ [block_height, tx_index, tx_payload]
104
+ end
105
+
106
+ # Delete utxo from db
107
+ #
108
+ # @param [Bitcoin::Outpoint] out_point
109
+ # @return [Bitcoin::Wallet::Utxo]
110
+ def delete_utxo(out_point)
111
+ level_db.batch do
112
+ # [:out_point]
113
+ key = KEY_PREFIX[:out_point] + out_point.to_hex
114
+ return unless level_db.contains?(key)
115
+ utxo = Bitcoin::Wallet::Utxo.parse_from_payload(level_db.get(key))
116
+ level_db.delete(key)
117
+
118
+ # [:script]
119
+ if utxo.script_pubkey
120
+ key = KEY_PREFIX[:script] + utxo.script_pubkey.to_hex + out_point.to_hex
121
+ level_db.delete(key)
122
+ end
123
+
124
+ if utxo.block_height
125
+ # [:height]
126
+ key = KEY_PREFIX[:height] + [utxo.block_height].pack('N').bth + out_point.to_hex
127
+ level_db.delete(key)
128
+
129
+ # [:block]
130
+ key = KEY_PREFIX[:block] + [utxo.block_height, utxo.index].pack('N2').bth
131
+ level_db.delete(key)
132
+ end
133
+
134
+ # handles both [:tx_hash] and [:tx_payload]
135
+ if utxo.tx_hash
136
+ key = KEY_PREFIX[:tx_hash] + utxo.tx_hash
137
+ level_db.delete(key)
138
+
139
+ key = KEY_PREFIX[:tx_payload] + utxo.tx_hash
140
+ level_db.delete(key)
141
+ end
142
+
143
+ utxo
144
+ end
145
+ end
146
+
147
+ # Get utxo of the specified out point
148
+ #
149
+ # @param [Bitcoin::Outpoint] out_point
150
+ # @return [Bitcoin::Wallet::Utxo]
151
+ def get_utxo(out_point)
152
+ level_db.batch do
153
+ key = KEY_PREFIX[:out_point] + out_point.to_hex
154
+ return unless level_db.contains?(key)
155
+ return Bitcoin::Wallet::Utxo.parse_from_payload(level_db.get(key))
156
+ end
157
+ end
158
+
159
+ # return [Bitcoin::Wallet::Utxo ...]
160
+ def list_unspent(current_block_height: 9999999, min: 0, max: 9999999, addresses: nil)
161
+ if addresses
162
+ list_unspent_by_addresses(current_block_height, min: min, max: max, addresses: addresses)
163
+ else
164
+ list_unspent_by_block_height(current_block_height, min: min, max: max)
165
+ end
166
+ end
167
+
168
+ # @param [Bitcoin::Wallet::Account]
169
+ # return [Bitcoin::Wallet::Utxo ...]
170
+ def list_unspent_in_account(account, current_block_height: 9999999, min: 0, max: 9999999)
171
+ return [] unless account
172
+
173
+ script_pubkeys = case account.purpose
174
+ when Bitcoin::Wallet::Account::PURPOSE_TYPE[:legacy]
175
+ account.watch_targets.map { |t| Bitcoin::Script.to_p2pkh(t).to_hex }
176
+ when Bitcoin::Wallet::Account::PURPOSE_TYPE[:nested_witness]
177
+ account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_p2sh.to_hex }
178
+ when Bitcoin::Wallet::Account::PURPOSE_TYPE[:native_segwit]
179
+ account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_hex }
180
+ end
181
+ list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
182
+ end
183
+
184
+ # @param [Bitcoin::Wallet::Account]
185
+ # return [Bitcoin::Wallet::Utxo ...]
186
+ def get_balance(account, current_block_height: 9999999, min: 0, max: 9999999)
187
+ list_unspent_in_account(account, current_block_height: current_block_height, min: min, max: max).sum { |u| u.value }
188
+ end
189
+
190
+ private
191
+
192
+ def utxos_between(from, to)
193
+ level_db.each(from: from, to: to).map { |k, v| Bitcoin::Wallet::Utxo.parse_from_payload(v) }
194
+ end
195
+
196
+ class ::Array
197
+ def with_height(min, max)
198
+ select { |u| u.block_height >= min && u.block_height <= max }
199
+ end
200
+ end
201
+
202
+ def list_unspent_by_block_height(current_block_height, min: 0, max: 9999999)
203
+ max_height = [current_block_height - min, 0].max
204
+ min_height = [current_block_height - max, 0].max
205
+ from = KEY_PREFIX[:height] + [min_height].pack('N').bth + '000000000000000000000000000000000000000000000000000000000000000000000000'
206
+ to = KEY_PREFIX[:height] + [max_height].pack('N').bth + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
207
+ utxos_between(from, to)
208
+ end
209
+
210
+ def list_unspent_by_addresses(current_block_height, min: 0, max: 9999999, addresses: [])
211
+ script_pubkeys = addresses.map { |a| Bitcoin::Script.parse_from_addr(a).to_hex }
212
+ list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
213
+ end
214
+
215
+ def list_unspent_by_script_pubkeys(current_block_height, min: 0, max: 9999999, script_pubkeys: [])
216
+ max_height = current_block_height - min
217
+ min_height = current_block_height - max
218
+ script_pubkeys.map do |key|
219
+ from = KEY_PREFIX[:script] + key + '000000000000000000000000000000000000000000000000000000000000000000000000'
220
+ to = KEY_PREFIX[:script] + key + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
221
+ utxos_between(from, to).with_height(min_height, max_height)
222
+ end.flatten
223
+ end
224
+ end
225
+ end
226
+ end