Class: Sibit
- Inherits:
-
Object
- Object
- Sibit
- Defined in:
- lib/sibit.rb,
lib/sibit/tx.rb,
lib/sibit/bin.rb,
lib/sibit/key.rb,
lib/sibit/http.rb,
lib/sibit/coins.rb,
lib/sibit/error.rb,
lib/sibit/base58.rb,
lib/sibit/bech32.rb,
lib/sibit/script.rb,
lib/sibit/version.rb,
lib/sibit/httpproxy.rb,
lib/sibit/txbuilder.rb
Overview
Sibit main class.
Defined Under Namespace
Classes: Base58, Bech32, BestOf, Bin, Bitcoinchain, Blockchain, Blockchair, Btc, Cex, Coins, Cryptoapis, Dry, Error, Fake, FirstOf, Http, HttpProxy, Json, Key, NotSupportedError, Script, Sochain, Tx, TxBuilder
Constant Summary collapse
- MIN_SATOSHI_PER_BYTE =
1- VERSION =
'0.34.2'
Instance Method Summary collapse
-
#balance(address, trust: 0) ⇒ Object
Gets the balance of the address, in satoshi.
-
#create(pvt) ⇒ Object
Creates Bitcoin address using the private key in Hash160 format.
-
#fees ⇒ Object
Get recommended fees, in satoshi per byte.
-
#generate ⇒ Object
Generates new Bitcoin private key and returns in Hash160 format.
-
#height(hash) ⇒ Object
Get the height of the block.
-
#initialize(log: Loog::NULL, api: Sibit::Blockchain.new(log: log)) ⇒ Sibit
constructor
Constructor.
-
#latest ⇒ Object
Gets the hash of the latest block.
-
#next_of(hash) ⇒ Object
Get the hash of the next block.
-
#pay(amount, fee, sources, target, change, skip_utxo: [], network: nil, base58: false, price: nil, trust: 0) ⇒ Object
Sends a payment and returns the transaction hash.
-
#price(currency = 'USD') ⇒ Object
Current price of 1 BTC in USD (or another currency), float returned.
-
#scan(start, max: 4) ⇒ Object
You call this method and provide a callback.
Constructor Details
#initialize(log: Loog::NULL, api: Sibit::Blockchain.new(log: 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 succeeds.
42 43 44 45 |
# File 'lib/sibit.rb', line 42 def initialize(log: Loog::NULL, api: Sibit::Blockchain.new(log: log)) @log = log @api = api end |
Instance Method Details
#balance(address, trust: 0) ⇒ Object
Gets the balance of the address, in satoshi. When trust is greater
than zero, only UTXOs with at least that many confirmations are summed,
which requires the underlying API to support utxos.
69 70 71 72 73 74 75 76 |
# File 'lib/sibit.rb', line 69 def balance(address, trust: 0) raise(Error, "Invalid address #{address.inspect}") unless /^[0-9a-zA-Z]+$/.match?(address) unless trust.is_a?(Integer) && !trust.negative? raise(Error, "Trust must be a non-negative Integer, got #{trust.inspect}") end return @api.balance(address) if trust.zero? @api.utxos([address]).sum { |u| (u[:confirmations] || 0) >= trust ? u[:value] : 0 } end |
#create(pvt) ⇒ Object
Creates Bitcoin address using the private key in Hash160 format.
61 62 63 64 |
# File 'lib/sibit.rb', line 61 def create(pvt) raise(Error, 'Invalid private key (must be 64 hex chars)') unless /^[0-9a-f]{64}$/.match?(pvt) Key.new(pvt).bech32 end |
#fees ⇒ Object
Get recommended fees, in satoshi per byte. The method returns a hash: { S: 12, M: 45, L: 100, XL: 200 }
92 93 94 |
# File 'lib/sibit.rb', line 92 def fees @api.fees end |
#generate ⇒ Object
Generates new Bitcoin private key and returns in Hash160 format.
54 55 56 57 58 |
# File 'lib/sibit.rb', line 54 def generate key = Key.generate.priv @log.debug("Bitcoin private key generated: #{key.ellipsized(8)}...") key end |
#height(hash) ⇒ Object
Get the height of the block.
79 80 81 82 |
# File 'lib/sibit.rb', line 79 def height(hash) raise(Error, "Invalid block hash #{hash.inspect}") unless /^[0-9a-f]{64}$/.match?(hash) @api.height(hash) end |
#latest ⇒ Object
Gets the hash of the latest block.
231 232 233 |
# File 'lib/sibit.rb', line 231 def latest @api.latest end |
#next_of(hash) ⇒ Object
Get the hash of the next block.
85 86 87 88 |
# File 'lib/sibit.rb', line 85 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, skip_utxo: [], network: nil, base58: false, price: nil, trust: 0) ⇒ 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. You may retry, but only while no earlier attempt could have reached the network: inputs are selected deterministically, so an immediate retry rebuilds a conflicting transaction that the network rejects as a double-spend. Once an earlier attempt might have confirmed, the wallet holds a fresh set of UTXOs and a blind retry may pay the target twice; check the target address (or the previous transaction hash) before re-sending.
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 list of private bitcoin keys where the coins are now
target: the target address to send to
change: the address where the change has to be sent to
network: optional network override (:mainnet, :testnet, :regtest)
price: optional BTC price in USD (skips API fetch if provided)
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 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 223 224 225 226 227 228 |
# File 'lib/sibit.rb', line 118 def pay( amount, fee, sources, target, change, skip_utxo: [], network: nil, base58: false, price: nil, trust: 0 ) unless amount.is_a?(Integer) || amount.is_a?(String) raise(Error, "The amount #{amount.inspect} must be Integer or String") end raise(Error, 'The amount must be positive') if amount.is_a?(Integer) && amount.negative? raise(Error, 'The sources must be an Array') unless sources.is_a?(Array) raise(Error, 'The target must be a String') unless target.is_a?(String) raise(Error, 'The change must be a String') unless change.is_a?(String) unless trust.is_a?(Integer) && !trust.negative? raise(Error, "Trust must be a non-negative Integer, got #{trust.inspect}") end p = price || price('USD') keys = sources.map do |k| raise(Error, 'Each source private key must be a String') unless k.is_a?(String) wif = /^[5KLc][1-9A-HJ-NP-Za-km-z]{50,51}$/.match?(k) unless /^[0-9a-f]{64}$/i.match?(k) || wif raise(Error, "Invalid private key format: #{k.inspect.ellipsized(8)}") end Key.new(k, network: network) end network = keys.first&.network || :mainnet sources = keys.to_h do |k| pub = if base58 k.base58 else k.bech32 end @log.debug("Private key #{k.priv.ellipsized(8).inspect} is public as #{pub}:") [pub, k.priv] end satoshi = satoshi(amount) builder = TxBuilder.new(network) unspent = 0 size = 100 utxos = @api.utxos(sources.keys).sort_by { |u| [u[:hash], u[:index]] } @log.debug("#{utxos.count} UTXOs found, these will be used (value/confirmations at tx_hash):") utxos.each do |utxo| if skip_utxo.include?(utxo[:hash]) @log.debug("UTXO skipped: #{utxo[:hash]}") next end unless utxo[:confirmations]&.positive? @log.debug("UTXO with no confirmations: #{utxo[:hash]}") next end if utxo[:confirmations] < trust @log.debug("UTXO below trust=#{trust} (#{utxo[:confirmations]} conf): #{utxo[:hash]}") next end unspent += utxo[:value] builder.input do |i| i.prev_out(utxo[:hash]) i.prev_out_index(utxo[:index]) i.prev_out_script = script_hex(utxo[:script]) i.prev_out_value(utxo[:value]) address = Script.new(script_hex(utxo[:script])).address(network) k = sources[address] raise(Error, "UTXO arrived to #{address} is incorrect") unless k i.signature_key(key(k)) end size += 180 @log.debug(" #{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 f = mfee(fee, size) if f.negative? satoshi += f f = -f end 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.output(satoshi, target) tx = builder.tx( input_value: unspent, leave_fee: true, extra_fee: [f, Integer(size * MIN_SATOSHI_PER_BYTE)].max, change_address: change ) @log.debug( [ "A new Bitcoin transaction #{tx.hash} prepared:", "#{tx.in.count} input#{'s' if tx.in.count > 1}:", tx.inputs.map do |i| " in: #{i.prev_out.unpack1('H*')}:#{i.prev_out_index}" end, "#{tx.out.count} output#{'s' if tx.out.count > 1}:", tx.outputs.map do |o| " out: #{o.script_hex} / #{num(o.value, p)}" end, "Min fee: #{num(MIN_SATOSHI_PER_BYTE, p)} /byte", "Fee requested: #{num(f, p)} as \"#{fee}\"", "Fee actually paid: #{num(unspent - tx.outputs.sum(&:value), p)}", "Tx size: #{size} bytes", "Unspent: #{num(unspent, p)}", "Amount: #{num(satoshi, p)}", "Target address: #{target}", ("Change address: #{change}" if tx.out.count > 1) ].flatten.compact.join("\n") ) @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.
48 49 50 51 |
# File 'lib/sibit.rb', line 48 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:
- Bitcoin address of the receiver, 2) transaction hash, 3) amount in satoshi. The callback should return non-false if the transaction found was useful.
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/sibit.rb', line 245 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 json = {} loop do json = @api.block(block) if json[:orphan] steps = 4 @log.debug("Orphan block found at #{block}, moving #{steps} steps back...") steps.times do block = json[:previous] @log.debug("Moved back to #{block}") json = @api.block(block) end next end checked = 0 outputs = 0 json[:txns].each do |t| t[:outputs].each_with_index do |o, i| address = o[:address] outputs += 1 hash = "#{t[:hash]}:#{i}" satoshi = o[:value] if yield(address, hash, satoshi) @log.debug("Bitcoin tx found at #{hash} for #{satoshi} sent to #{address}") end end checked += 1 end count += 1 @log.debug( "Checked #{checked} txns and #{outputs} outputs " \ "in block #{block} (by #{json[:provider]})" ) block = json[:next] begin if block.nil? @log.debug("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.debug("Failed to get the next_of(#{json[:hash]}), quitting: #{e.}") break end if block.nil? @log.debug("The block #{json[:hash]} is definitely the end of Blockchain, we stop.") break end if count >= max @log.debug("Too many blocks (#{count}) in one go, let's get back to it next time") break end end @log.debug("Scanned from #{start} to #{json[:hash]} (#{count} blocks)") json[:hash] end |