Class: Sibit

Inherits:
Object
  • Object
show all
Defined in:
lib/sibit.rb,
lib/sibit/btc.rb,
lib/sibit/cex.rb,
lib/sibit/log.rb,
lib/sibit/earn.rb,
lib/sibit/fake.rb,
lib/sibit/http.rb,
lib/sibit/json.rb,
lib/sibit/error.rb,
lib/sibit/bestof.rb,
lib/sibit/firstof.rb,
lib/sibit/version.rb,
lib/sibit/blockchain.rb,
lib/sibit/blockchair.rb,
lib/sibit/cryptoapis.rb,
lib/sibit/bitcoinchain.rb

Overview

Bitcoinchain.com API.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2019-2022 Yegor Bugayenko

License

MIT

Defined Under Namespace

Classes: BestOf, Bitcoinchain, Blockchain, Blockchair, Btc, Cex, Cryptoapis, Earn, Error, Fake, FirstOf, Http, HttpProxy, Json, Log, NotSupportedError

Constant Summary collapse

VERSION =

Current version of the library.

'0.22.0'

Instance Method Summary collapse

Constructor Details

#initialize(log: $stdout, api: Sibit::Blockchain.new(log: Sibit::Log.new(log))) ⇒ 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.

It is recommended to wrap the API in a RetriableProxy from retriable_proxy gem and to configure it to retry on Sibit::Error:

RetriableProxy.for_object(api, on: Sibit::Error)

This will help you avoid some temporary network issues.

The api argument can be an object or an array of objects. If an array is provided, we will make an attempt to try them one by one, until one of them succeedes.



50
51
52
53
# File 'lib/sibit.rb', line 50

def initialize(log: $stdout, api: Sibit::Blockchain.new(log: Sibit::Log.new(log)))
  @log = Sibit::Log.new(log)
  @api = api
end

Instance Method Details

#balance(address) ⇒ Object

Gets the balance of the address, in satoshi.

Raises:



76
77
78
79
# File 'lib/sibit.rb', line 76

def balance(address)
  raise Error, "Invalid address #{address.inspect}" unless /^[0-9a-zA-Z]+$/.match?(address)
  @api.balance(address)
end

#create(pvt) ⇒ Object

Creates Bitcon address using the private key in Hash160 format.



69
70
71
72
73
# File 'lib/sibit.rb', line 69

def create(pvt)
  key = Bitcoin::Key.new
  key.priv = pvt
  key.addr
end

#feesObject

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



95
96
97
# File 'lib/sibit.rb', line 95

def fees
  @api.fees
end

#generateObject

Generates new Bitcon private key and returns in Hash160 format.



62
63
64
65
66
# File 'lib/sibit.rb', line 62

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

#height(hash) ⇒ Object

Get the height of the block.

Raises:



82
83
84
85
# File 'lib/sibit.rb', line 82

def height(hash)
  raise Error, "Invalid block hash #{hash.inspect}" unless /^[0-9a-f]{64}$/.match?(hash)
  @api.height(hash)
end

#latestObject

Gets the hash of the latest block.



173
174
175
# File 'lib/sibit.rb', line 173

def latest
  @api.latest
end

#next_of(hash) ⇒ Object

Get the hash of the next block.

Raises:



88
89
90
91
# File 'lib/sibit.rb', line 88

def next_of(hash)
  raise Error, "Invalid block hash #{hash.inspect}" unless /^[0-9a-f]{64}$/.match?(hash)
  @api.next_of(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:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/sibit.rb', line 116

def pay(amount, fee, sources, target, change)
  p = price('USD')
  satoshi = satoshi(amount)
  builder = Bitcoin::Builder::TxBuilder.new
  unspent = 0
  size = 100
  utxos = @api.utxos(sources.keys)
  @log.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[:hash])
      i.prev_out_index(utxo[:index])
      i.prev_out_script = utxo[:script]
      address = Bitcoin::Script.new(utxo[:script]).get_address
      i.signature_key(key(sources[address]))
    end
    size += 180
    @log.info(
      "  #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}"
    )
    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)
  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?
  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(&:+)
  @log.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 actually paid: #{num(left, p)}
Tx size: #{size} bytes
Unspent: #{num(unspent, p)}
Amount: #{num(satoshi, p)}
Target address: #{target}
Change address is #{change}")
  @api.push(tx.to_payload.bth)
  tx.hash
end

#price(currency = 'USD') ⇒ Object

Current price of 1 BTC in USD (or another currency), float returned.

Raises:



56
57
58
59
# File 'lib/sibit.rb', line 56

def price(currency = 'USD')
  raise Error, "Invalid currency #{currency.inspect}" unless /^[A-Z]{3}$/.match?(currency)
  @api.price(currency)
end

#scan(start, max: 4) ⇒ Object

You call this method and provide a callback. You provide the hash of the starting block. The method will go through the Blockchain, fetch blocks and find transactions, one by one, passing them to the callback provided. When finished, the method returns the hash of a new block, not scanned yet or NIL if it’s the end of Blockchain.

The callback will be called with three arguments: 1) Bitcoin address of the receiver, 2) transaction hash, 3) amount in satoshi. The callback should return non-false if the transaction found was useful.

Raises:



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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/sibit.rb', line 187

def scan(start, max: 4)
  raise Error, "Invalid block hash #{start.inspect}" unless /^[0-9a-f]{64}$/.match?(start)
  raise Error, "The max number must be above zero: #{max}" if max < 1
  block = start
  count = 0
  wrong = []
  json = {}
  loop do
    json = @api.block(block)
    if json[:orphan]
      steps = 4
      @log.info("Orphan block found at #{block}, moving #{steps} steps back...")
      wrong << block
      steps.times do
        block = json[:previous]
        wrong << block
        @log.info("Moved back to #{block}")
        json = @api.block(block)
      end
      next
    end
    checked = 0
    checked_outputs = 0
    json[:txns].each do |t|
      t[:outputs].each_with_index do |o, i|
        address = o[:address]
        checked_outputs += 1
        hash = "#{t[:hash]}:#{i}"
        satoshi = o[:value]
        if yield(address, hash, satoshi)
          @log.info("Bitcoin tx found at #{hash} for #{satoshi} sent to #{address}")
        end
      end
      checked += 1
    end
    count += 1
    @log.info("We checked #{checked} txns and #{checked_outputs} outputs \
in block #{block} (by #{json[:provider]})")
    block = json[:next]
    begin
      if block.nil?
        @log.info("The next_block is empty in #{json[:hash]}, this may be the end...")
        block = @api.next_of(json[:hash])
      end
    rescue Sibit::Error => e
      @log.info("Failed to get the next_of(#{json[:hash]}), quitting: #{e.message}")
      break
    end
    if block.nil?
      @log.info("The block #{json[:hash]} is definitely the end of Blockchain, we stop.")
      break
    end
    if count > max
      @log.info("Too many blocks (#{count}) in one go, let's get back to it next time")
      break
    end
  end
  @log.info("Scanned from #{start} to #{json[:hash]} (#{count} blocks)")
  json[:hash]
end