Class: Sibit

Inherits:
Object
  • Object
show all
Defined in:
lib/sibit.rb,
lib/sibit/version.rb

Overview

Sibit main class.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2019 Yegor Bugayenko

License

MIT

Defined Under Namespace

Classes: Error, Fake

Constant Summary collapse

VERSION =

Current version of the library.

'0.12.5'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(log: STDOUT, http: Sibit.default_http, dry: false, attempts: 1) ⇒ Sibit

Constructor.

You may provide the log you want to see the messages in. If you don’t provide anything, the console will be used. The object you provide has to respond to the method info or puts in order to receive logging messages.



98
99
100
101
102
103
# File 'lib/sibit.rb', line 98

def initialize(log: STDOUT, http: Sibit.default_http, dry: false, attempts: 1)
  @log = log
  @http = http
  @dry = dry
  @attempts = attempts
end

Class Method Details

.default_httpObject

This HTTP client will be used by default.



78
79
80
81
82
# File 'lib/sibit.rb', line 78

def self.default_http
  http = Net::HTTP.new('blockchain.info', 443)
  http.use_ssl = true
  http
end

.proxy_http(addr) ⇒ Object

This HTTP client with proxy.



85
86
87
88
89
90
# File 'lib/sibit.rb', line 85

def self.proxy_http(addr)
  host, port = addr.split(':')
  http = Net::HTTP.new('blockchain.info', 443, host, port.to_i)
  http.use_ssl = true
  http
end

Instance Method Details

#balance(address) ⇒ Object

Gets the balance of the address, in satoshi.



125
126
127
128
129
130
# File 'lib/sibit.rb', line 125

def balance(address)
  json = get_json("/rawaddr/#{address}")
  info("Total transactions: #{json['n_tx']}")
  info("Received/sent: #{json['total_received']}/#{json['total_sent']}")
  json['final_balance']
end

#create(pvt) ⇒ Object

Creates Bitcon address using the private key in Hash160 format.



120
121
122
# File 'lib/sibit.rb', line 120

def create(pvt)
  key(pvt).addr
end

#feesObject

Get recommended fees, in satoshi per byte. The method returns a hash: { S: 12, M: 45, L: 100, XL: 200 }



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/sibit.rb', line 134

def fees
  json = JSON.parse(
    Net::HTTP.get(
      URI('https://bitcoinfees.earn.com/api/v1/fees/recommended')
    )
  )
  info("Current recommended Bitcoin fees: \
#{json['hourFee']}/#{json['halfHourFee']}/#{json['fastestFee']} sat/byte")
  {
    S: json['hourFee'] / 3,
    M: json['hourFee'],
    L: json['halfHourFee'],
    XL: json['fastestFee']
  }
end

#generateObject

Generates new Bitcon private key and returns in Hash160 format.



113
114
115
116
117
# File 'lib/sibit.rb', line 113

def generate
  key = Bitcoin::Key.generate.priv
  info("Bitcoin private key generated: #{key[0..8]}...")
  key
end

#get_json(uri) ⇒ Object

Send GET request to the Blockchain API and return JSON response. This method will also log the process and will validate the response for correctness.



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/sibit.rb', line 232

def get_json(uri)
  start = Time.now
  attempt = 0
  begin
    res = @http.get(
      uri,
      'Accept' => 'text/plain',
      'User-Agent' => user_agent,
      'Accept-Encoding' => ''
    )
    raise Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}" unless res.code == '200'
    info("GET #{uri}: #{res.code}/#{res.body.length}b in #{age(start)}")
    JSON.parse(res.body)
  rescue StandardError => e
    attempt += 1
    raise e if attempt >= @attempts
    retry
  end
end

#latestObject

Gets the hash of the latest block.



225
226
227
# File 'lib/sibit.rb', line 225

def latest
  get_json('/latestblock')['hash']
end

#pay(amount, fee, sources, target, change) ⇒ Object

Sends a payment and returns the transaction hash.

If the payment can’t be signed (the key is wrong, for example) or the previous transaction is not found, or there is a network error, or any other reason, you will get an exception. In this case, just try again. It’s safe to try as many times as you need. Don’t worry about duplicating your transaction, the Bitcoin network will filter duplicates out.

If there are more than 1000 UTXOs in the address where you are trying to send bitcoins from, this method won’t be helpful.

amount: the amount either in satoshis or ending with ‘BTC’, like ‘0.7BTC’ fee: the miners fee in satoshis (as integer) or S/M/X/XL as a string sources: the hashmap of bitcoin addresses where the coins are now, with their addresses as keys and private keys as values target: the target address to send to change: the address where the change has to be sent to

Raises:



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/sibit.rb', line 167

def pay(amount, fee, sources, target, change)
  p = price
  satoshi = satoshi(amount)
  f = mfee(fee, size_of(amount, sources))
  satoshi += f if f.negative?
  raise Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero?
  raise Error, "The fee #{f.abs} is bigger than the amount #{satoshi}" if satoshi.negative?
  builder = Bitcoin::Builder::TxBuilder.new
  unspent = 0
  size = 100
  utxos = get_json(
    "/unspent?active=#{sources.keys.join('|')}&limit=1000"
  )['unspent_outputs']
  info("#{utxos.count} UTXOs found, these will be used \
(value/confirmations at tx_hash):")
  utxos.each do |utxo|
    unspent += utxo['value']
    builder.input do |i|
      i.prev_out(utxo['tx_hash_big_endian'])
      i.prev_out_index(utxo['tx_output_n'])
      i.prev_out_script = [utxo['script']].pack('H*')
      address = Bitcoin::Script.new([utxo['script']].pack('H*')).get_address
      i.signature_key(key(sources[address]))
    end
    size += 180
    info("  #{num(utxo['value'], p)}/#{utxo['confirmations']} at #{utxo['tx_hash_big_endian']}")
    break if unspent > satoshi
  end
  if unspent < satoshi
    raise Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left"
  end
  builder.output(satoshi, target)
  f = mfee(fee, size)
  tx = builder.tx(
    input_value: unspent,
    leave_fee: true,
    extra_fee: [f, Bitcoin.network[:min_tx_fee]].max,
    change_address: change
  )
  left = unspent - tx.outputs.map(&:value).inject(&:+)
  info("A new Bitcoin transaction #{tx.hash} prepared:
#{tx.in.count} input#{tx.in.count > 1 ? 's' : ''}:
  #{tx.inputs.map { |i| " in: #{i.prev_out.bth}:#{i.prev_out_index}" }.join("\n    ")}
#{tx.out.count} output#{tx.out.count > 1 ? 's' : ''}:
  #{tx.outputs.map { |o| "out: #{o.script.bth} / #{num(o.value, p)}" }.join("\n    ")}
Min tx fee: #{num(Bitcoin.network[:min_tx_fee], p)}
Fee requested: #{num(f, p)} as \"#{fee}\"
Fee left: #{num(left, p)}
Tx size: #{size} bytes
Unspent: #{num(unspent, p)}
Amount: #{num(satoshi, p)}
Target address: #{target}
Change address is #{change}")
  post_tx(tx.to_payload.bth) unless @dry
  tx.hash
end

#price(cur = 'USD') ⇒ Object

Current price of 1 BTC.

Raises:



106
107
108
109
110
# File 'lib/sibit.rb', line 106

def price(cur = 'USD')
  h = get_json('/ticker')[cur.upcase]
  raise Error, "Unrecognized currency #{cur}" if h.nil?
  h['15m']
end