sibit 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85eea5a6116c95e319c46e4bb3bcaa9a5578d0d0e96395e4a9395cc4b7546e7b
4
- data.tar.gz: 0de27c41f5e7ea62ef116f06504839e3bfa45da2cc01ac9c87352853e95fb41a
3
+ metadata.gz: a297184fe1eeb6405104af89ad5eb25288fc99e1fc0635596f0ed61649f3c248
4
+ data.tar.gz: b386b08577c46b662de0b78737e364411cea207f203b6c78196caa545407bf0b
5
5
  SHA512:
6
- metadata.gz: e656fa1439cc031cf41c69e8ec14fcb929a92bbb839657ee78ef2d52f0b715f9dd4939ee4a0567b21aaa7c8dd6da5d91c0bd2fd148847de37db8e79689b873d9
7
- data.tar.gz: d0cdb4e5c6a3b4bf1cfa5c08aba92d77cc1920aaf45033ca6845d57e7b2c26782b9a0587794db11a7bf615cce51328bcb2327d29b20765c844346d59d63ebd15
6
+ metadata.gz: 0b088ebce459844e583f03a160490cec3a56299bf021c0e953597e6323f2d84377a47ca1f069810bc7db04d54bc4761929b706cc10f32a0c349753ecd826eb17
7
+ data.tar.gz: 8de79264a592d6bdf387223a4443d7950441a06c553b926640ac4f9c91f9fc61df0f97ed14d878a76de2be02e0eccbf238dcb7f139f26ed4467b1fc545f1e176
data/.rubocop.yml CHANGED
@@ -5,6 +5,8 @@ AllCops:
5
5
  DisplayCopNames: true
6
6
  TargetRubyVersion: 2.3
7
7
 
8
+ Metrics/LineLength:
9
+ Max: 100
8
10
  Layout/EndOfLine:
9
11
  EnforcedStyle: lf
10
12
  Metrics/CyclomaticComplexity:
@@ -13,6 +15,10 @@ Metrics/MethodLength:
13
15
  Enabled: false
14
16
  Layout/MultilineMethodCallIndentation:
15
17
  Enabled: false
18
+ Metrics/ParameterLists:
19
+ Max: 6
20
+ Metrics/ClassLength:
21
+ Max: 120
16
22
  Metrics/AbcSize:
17
23
  Enabled: false
18
24
  Metrics/BlockLength:
data/README.md CHANGED
@@ -16,20 +16,18 @@ this [short video](https://www.youtube.com/watch?v=IV9pRBq5A4g).
16
16
  This is a simple Bitcoin client, to use from the command line
17
17
  or from your Ruby app. You don't need to run any Bitcoin software,
18
18
  no need to install anything, and so on. All you need is just a command line
19
- and [Ruby](https://www.ruby-lang.org/en/) 2.3+.
19
+ and [Ruby](https://www.ruby-lang.org/en/) 2.3+. The purpose of this
20
+ client is to simplify most typical operations with Bitcoin. If you need
21
+ something more complex, I would recommend using
22
+ [bitcoin-ruby](https://github.com/lian/bitcoin-ruby) for Ruby and
23
+ [Electrum](https://electrum.org/) as a GUI client.
20
24
 
21
- This is Ruby gem, install it first:
25
+ This is a Ruby gem, install it first:
22
26
 
23
27
  ```bash
24
28
  $ gem install sibit
25
29
  ```
26
30
 
27
- Run it and read its output:
28
-
29
- ```bash
30
- $ sibit --help
31
- ```
32
-
33
31
  Then, you generate a [private key](https://en.bitcoin.it/wiki/Private_key):
34
32
 
35
33
  ```bash
@@ -56,8 +54,8 @@ $ sibit balance 1CC3X2gu58d6wXUWMffpuzN9JAfTUWu4Kj
56
54
  To send a payment from a few addresses to a new address:
57
55
 
58
56
  ```
59
- $ sibit pay KEY AMOUNT FEE FROM1,FROM2,... TO
60
- 1CC3X2gu58d6wXUWjslPuzN9JAfTUWu4Kg
57
+ $ sibit pay KEY AMOUNT FEE SOURCE1,SOURCE2,... TARGET CHANGE
58
+ e87f138c9ebf5986151667719825c28458a28cc66f69fed4f1032a93b399fdf8
61
59
  ```
62
60
 
63
61
  Here,
@@ -65,9 +63,12 @@ Here,
65
63
  `AMOUNT` is the amount of [satoshi](https://en.bitcoin.it/wiki/Satoshi_%28unit%29) you are sending,
66
64
  `FEE` is the [miner fee](https://en.bitcoin.it/wiki/Miner_fees) you are ready to spend to make this transaction delivered
67
65
  (you can say `S`, `M`, `L`, or `XL` if you want it to be calculated automatically),
68
- `FROM1,FROM2,...` is a comma-separated list of addresses you are sending your coins from,
69
- `TO` is the address you are sending to.
70
- The address returned will contain the residual coins after the transaction is made.
66
+ `SOURCE1,SOURCE2,...` is a comma-separated list of addresses you are sending your coins from,
67
+ `TARGET` is the address you are sending to,
68
+ `CHANGE` is the address where the change will be sent to.
69
+ The transaction hash will be returned.
70
+ Not all [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output)
71
+ will be used, but only the necessary amount of them.
71
72
 
72
73
  All operations are performed through the
73
74
  [Blockchain API](https://www.blockchain.com/api/blockchain_api).
@@ -84,7 +85,9 @@ sibit = Sibit.new
84
85
  pkey = sibit.generate
85
86
  address = sibit.create(pkey)
86
87
  balance = sibit.balance(address)
87
- out = sibit.pay(pkey, 10_000_000, 'XL', address, target)
88
+ target = sibit.create(pkey) # where to send coins to
89
+ change = sibit.create(pkey) # where the change will sent to
90
+ tx = sibit.pay(pkey, 10_000_000, 'XL', [address], target, change)
88
91
  ```
89
92
 
90
93
  Should work.
data/bin/sibit CHANGED
@@ -24,11 +24,18 @@ STDOUT.sync = true
24
24
  require 'slop'
25
25
  require 'backtrace'
26
26
  require_relative '../lib/sibit'
27
+ require_relative '../lib/sibit/version'
27
28
 
28
29
  begin
29
30
  begin
30
31
  opts = Slop.parse(ARGV, strict: true, help: true) do |o|
31
- o.banner = "Usage (#{Sibit::VERSION}): sibit [options]"
32
+ o.banner = "Usage (#{Sibit::VERSION}): sibit [options] command [args]
33
+ Commands are:
34
+ generate: Generate a new private key
35
+ create: Create a public Bitcoin address from the key
36
+ balance: Check the balance of the Bitcoin address
37
+ pay: Send a new Bitcoin transaction
38
+ Options are:"
32
39
  o.bool '--help', 'Read this: https://github.com/yegor256/sibit' do
33
40
  puts o
34
41
  exit
@@ -37,6 +44,7 @@ begin
37
44
  rescue Slop::Error => ex
38
45
  raise "#{ex.message}"
39
46
  end
47
+ raise 'Try --help' if opts.arguments.empty?
40
48
  sibit = Sibit.new
41
49
  case opts.arguments[0]
42
50
  when 'generate'
@@ -60,7 +68,9 @@ begin
60
68
  raise 'Addresses argument is required' if sources.nil?
61
69
  target = opts.arguments[5]
62
70
  raise 'Target argument is required' if target.nil?
63
- puts sibit.pay(pvt, amount, fee, sources.split(','), target)
71
+ change = opts.arguments[6]
72
+ raise 'Change argument is required' if change.nil?
73
+ puts sibit.pay(pvt, amount, fee, sources.split(','), target, change)
64
74
  else
65
75
  raise "Command #{opts.arguments[0]} is not supported"
66
76
  end
@@ -56,9 +56,7 @@ When(%r{^I run bin/sibit with "([^"]*)"$}) do |arg|
56
56
  end
57
57
 
58
58
  Then(/^Stdout contains "([^"]*)"$/) do |txt|
59
- unless @stdout.include?(txt)
60
- raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}"
61
- end
59
+ raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}" unless @stdout.include?(txt)
62
60
  end
63
61
 
64
62
  Then(/^Stdout is empty$/) do
data/lib/sibit/version.rb CHANGED
@@ -26,5 +26,5 @@
26
26
  # License:: MIT
27
27
  class Sibit
28
28
  # Current version of the library.
29
- VERSION = '0.0.1'
29
+ VERSION = '0.1.0'
30
30
  end
data/lib/sibit.rb CHANGED
@@ -25,38 +25,142 @@ require 'typhoeus'
25
25
  require 'json'
26
26
 
27
27
  # Sibit main class.
28
+ #
29
+ # It works through the Blockchain API at the moment:
30
+ # https://www.blockchain.com/api/blockchain_api
31
+ #
28
32
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
29
33
  # Copyright:: Copyright (c) 2019 Yegor Bugayenko
30
34
  # License:: MIT
31
35
  class Sibit
32
- # Generate new Bitcon private key.
36
+ # Constructor.
37
+ def initialize(log: STDOUT)
38
+ @log = log
39
+ end
40
+
41
+ # Generate new Bitcon private key and returns in Hash160 format.
33
42
  def generate
34
- key = Bitcoin::Key.generate
35
- key.priv
43
+ Bitcoin::Key.generate.priv
36
44
  end
37
45
 
38
- # Create Bitcon address using the private key.
46
+ # Create Bitcon address using the private key in Hash160 format.
39
47
  def create(pvt)
40
- key = Bitcoin::Key.new
41
- key.priv = pvt
42
- key.addr
48
+ key(pvt).addr
43
49
  end
44
50
 
45
51
  # Get the balance of the address, in satoshi.
46
52
  def balance(address)
47
- request = Typhoeus::Request.new(
48
- "https://blockchain.info/rawaddr/#{address}",
49
- method: :get,
50
- headers: {}
53
+ get_json("https://blockchain.info/rawaddr/#{address}")['final_balance']
54
+ end
55
+
56
+ # Send a payment and return the transaction hash.
57
+ #
58
+ # +pvt+: the private key as a Hash160 string
59
+ # +amount+: the amount either in satoshis or ending with 'BTC', like '0.7BTC'
60
+ # +fee+: the miners fee in satoshis (as integer) or S/M/X/XL as a string
61
+ # +sources+: the array of bitcoin addresses where the coins are now
62
+ # +target+: the target address to send to
63
+ # +change+: the address where the change has to be sent to
64
+ def pay(pvt, amount, fee, sources, target, change)
65
+ satoshi = satoshi(amount)
66
+ builder = Bitcoin::Builder::TxBuilder.new
67
+ unspent = 0
68
+ size = 100
69
+ utxos(sources).each do |utxo|
70
+ unspent += utxo['value']
71
+ builder.input do |i|
72
+ i.prev_out(utxo['tx_hash_big_endian'])
73
+ i.prev_out_index(utxo['tx_output_n'])
74
+ i.prev_out_script = [utxo['script']].pack('H*')
75
+ i.signature_key(key(pvt))
76
+ end
77
+ size += 180
78
+ break if unspent > satoshi
79
+ end
80
+ raise "Not enough funds to send #{amount}, only #{unspent} left" if unspent < satoshi
81
+ builder.output(satoshi, target)
82
+ tx = builder.tx(
83
+ input_value: unspent,
84
+ leave_fee: mfee(fee, size),
85
+ change_address: change
51
86
  )
87
+ post_tx(tx.to_payload.bth)
88
+ tx.hash
89
+ end
90
+
91
+ private
92
+
93
+ # Retrieve all unspent outputs of the given list of
94
+ # addresses.
95
+ def utxos(sources)
96
+ offset = 0
97
+ txns = []
98
+ loop do
99
+ uri = [
100
+ 'https://blockchain.info/unspent?',
101
+ "active=#{sources.join('|')}",
102
+ offset.positive? ? "&offset=#{offset}" : ''
103
+ ].join
104
+ list = get_json(uri)['unspent_outputs']
105
+ txns += list
106
+ break if list.empty?
107
+ offset += list.count
108
+ end
109
+ txns
110
+ end
111
+
112
+ # Convert text to amount.
113
+ def satoshi(amount)
114
+ return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC')
115
+ amount.to_i
116
+ end
117
+
118
+ def mfee(fee, size)
119
+ return fee.to_i if fee.is_a?(Integer) || /^[0-9]+$/.match?(fee)
120
+ case fee
121
+ when 'S'
122
+ return 10 * size
123
+ when 'M'
124
+ return 50 * size
125
+ when 'L'
126
+ return 100 * size
127
+ when 'XL'
128
+ return 250 * size
129
+ else
130
+ raise "Can't understand the fee: #{fee.inspect}"
131
+ end
132
+ end
133
+
134
+ # Make key from private key string in Hash160.
135
+ def key(hash160)
136
+ key = Bitcoin::Key.new
137
+ key.priv = hash160
138
+ key
139
+ end
140
+
141
+ def post_tx(body)
142
+ uri = 'https://blockchain.info/pushtx'
143
+ request = Typhoeus::Request.new(uri, method: :post, body: { tx: body })
144
+ request.run
145
+ response = request.response
146
+ raise "Invalid response at #{uri}: #{response.code}" unless response.code == 200
147
+ debug("POST #{uri}: #{response.code}")
148
+ end
149
+
150
+ def get_json(uri)
151
+ request = Typhoeus::Request.new(uri, method: :get, headers: {})
52
152
  request.run
53
153
  response = request.response
54
- json = JSON.parse(response.body)
55
- json['final_balance']
154
+ raise "Invalid response at #{uri}: #{response.code}" unless response.code == 200
155
+ debug("GET #{uri}: #{response.code}")
156
+ JSON.parse(response.body)
56
157
  end
57
158
 
58
- # Send a payment.
59
- def pay(_pvt, _amount, _fee, _sources, _target)
60
- raise 'Not implemented yet'
159
+ def debug(msg)
160
+ if @log.respond_to?(:debug)
161
+ @log.debug(msg)
162
+ elsif @log.respond_to?(:puts)
163
+ @log.puts(msg)
164
+ end
61
165
  end
62
166
  end
data/sibit.gemspec CHANGED
@@ -55,10 +55,12 @@ and Ruby 2.3+.'
55
55
  s.add_runtime_dependency 'typhoeus', '1.3.1'
56
56
  s.add_development_dependency 'codecov', '0.1.10'
57
57
  s.add_development_dependency 'cucumber', '1.3.17'
58
+ s.add_development_dependency 'debase', '0.2.2'
58
59
  s.add_development_dependency 'minitest', '5.5.0'
59
60
  s.add_development_dependency 'rake', '12.0.0'
60
- # s.add_development_dependency 'rdoc', '4.2.0'
61
61
  s.add_development_dependency 'rspec-rails', '3.1.0'
62
62
  s.add_development_dependency 'rubocop', '0.61.0'
63
63
  s.add_development_dependency 'rubocop-rspec', '1.31.0'
64
+ s.add_development_dependency 'ruby-debug-ide', '0.6.1'
65
+ s.add_development_dependency 'webmock', '3.4.2'
64
66
  end
data/test/test_sibit.rb CHANGED
@@ -21,6 +21,8 @@
21
21
  # SOFTWARE.
22
22
 
23
23
  require 'minitest/autorun'
24
+ require 'webmock/minitest'
25
+ require 'json'
24
26
  require_relative '../lib/sibit'
25
27
 
26
28
  # Sibit.
@@ -38,14 +40,55 @@ class TestSibit < Minitest::Test
38
40
  def test_create_address
39
41
  sibit = Sibit.new
40
42
  pkey = sibit.generate
43
+ puts "key: #{pkey}"
41
44
  address = sibit.create(pkey)
45
+ puts "address: #{address}"
42
46
  assert(!address.nil?)
43
47
  assert(/^1[0-9a-zA-Z]+$/.match?(address))
44
48
  end
45
49
 
46
50
  def test_gets_balance
51
+ stub_request(
52
+ :get,
53
+ 'https://blockchain.info/rawaddr/1MZT1fa6y8H9UmbZV6HqKF4UY41o9MGT5f'
54
+ ).to_return(status: 200, body: '{"final_balance": 100}')
47
55
  sibit = Sibit.new
48
56
  balance = sibit.balance('1MZT1fa6y8H9UmbZV6HqKF4UY41o9MGT5f')
49
57
  assert(balance.is_a?(Integer))
58
+ assert_equal(100, balance)
59
+ end
60
+
61
+ def test_sends_payment
62
+ json = {
63
+ unspent_outputs: [
64
+ {
65
+ tx_hash: 'fc8fb1a526aef220b54a66bbb3e0549bf34db4f25e1aebc3feb87e86d341e65d',
66
+ tx_hash_big_endian: '5de641d3867eb8fec3eb1a5ef2b44df39b54e0b3bb664ab520f2ae26a5b18ffc',
67
+ tx_output_n: 0,
68
+ script: '76a914c48a1737b35a9f9d9e3b624a910f1e22f7e80bbc88ac',
69
+ value: 100_000
70
+ }
71
+ ]
72
+ }
73
+ stub_request(
74
+ :get,
75
+ 'https://blockchain.info/unspent?active=1JvCsJtLmCxEk7ddZFnVkGXpr9uhxZPmJi'
76
+ ).to_return(status: 200, body: JSON.pretty_generate(json))
77
+ stub_request(
78
+ :get,
79
+ 'https://blockchain.info/unspent?active=1JvCsJtLmCxEk7ddZFnVkGXpr9uhxZPmJi&offset=1'
80
+ ).to_return(status: 200, body: '{"unspent_outputs": []}')
81
+ stub_request(:post, 'https://blockchain.info/pushtx').to_return(status: 200)
82
+ sibit = Sibit.new
83
+ target = sibit.create(sibit.generate)
84
+ change = sibit.create(sibit.generate)
85
+ tx = sibit.pay(
86
+ 'fd2333686f49d8647e1ce8d5ef39c304520b08f3c756b67068b30a3db217dcb2',
87
+ '0.0001BTC', 'S',
88
+ ['1JvCsJtLmCxEk7ddZFnVkGXpr9uhxZPmJi'],
89
+ target, change
90
+ )
91
+ assert(!tx.nil?)
92
+ assert(tx.length > 30, tx)
50
93
  end
51
94
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sibit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-08 00:00:00.000000000 Z
11
+ date: 2019-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backtrace
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - '='
109
109
  - !ruby/object:Gem::Version
110
110
  version: 1.3.17
111
+ - !ruby/object:Gem::Dependency
112
+ name: debase
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 0.2.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '='
123
+ - !ruby/object:Gem::Version
124
+ version: 0.2.2
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: minitest
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +192,34 @@ dependencies:
178
192
  - - '='
179
193
  - !ruby/object:Gem::Version
180
194
  version: 1.31.0
195
+ - !ruby/object:Gem::Dependency
196
+ name: ruby-debug-ide
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - '='
200
+ - !ruby/object:Gem::Version
201
+ version: 0.6.1
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - '='
207
+ - !ruby/object:Gem::Version
208
+ version: 0.6.1
209
+ - !ruby/object:Gem::Dependency
210
+ name: webmock
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - '='
214
+ - !ruby/object:Gem::Version
215
+ version: 3.4.2
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - '='
221
+ - !ruby/object:Gem::Version
222
+ version: 3.4.2
181
223
  description: |-
182
224
  This is a simple Bitcoin client, to use from command line \
183
225
  or from your Ruby app. You don't need to run any Bitcoin software, \