bc 0.1
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.
- data.tar.gz.sig +1 -0
- data/README.md +8 -0
- data/UNLICENSE +24 -0
- data/lib/bc.rb +820 -0
- metadata +100 -0
- metadata.gz.sig +2 -0
data.tar.gz.sig
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
(s����$�y/�駇��DXQn�"YzIӚ>��`����4��ҽ���揫���D�
|
data/README.md
ADDED
data/UNLICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
data/lib/bc.rb
ADDED
@@ -0,0 +1,820 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'jr'
|
3
|
+
|
4
|
+
module Bitcoin
|
5
|
+
# Errors that the bitcoind is expected to raise should derive from this class.
|
6
|
+
class Error < Exception
|
7
|
+
end
|
8
|
+
|
9
|
+
# InvalidAddress is raised when a bitcoin address is invalid.
|
10
|
+
class InvalidAddress < Error
|
11
|
+
# This is the address that was invalid.
|
12
|
+
attr_reader :address
|
13
|
+
|
14
|
+
def initialize(address)
|
15
|
+
@address = address.freeze
|
16
|
+
super("Invalid bitcoin address: #{@address.inspect}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# UnknownPrivateKey is raised when we need, but don't have, the private key
|
21
|
+
# associated with a bitcoin address.
|
22
|
+
class UnknownPrivateKey < InvalidAddress
|
23
|
+
end
|
24
|
+
|
25
|
+
# UnknownBlock is raised when bitcoind doesn't know about a given block.
|
26
|
+
class UnknownBlock < Error
|
27
|
+
# This is the block ID that bitcoind didn't know about.
|
28
|
+
attr_reader :block_id
|
29
|
+
|
30
|
+
def initialize(block_id)
|
31
|
+
@block_id = block_id.freeze
|
32
|
+
super("Unknown block ID: #{@block_id.inspect}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# UnknownTransaction is raised when bitcoind doesn't know about a given
|
37
|
+
# transaction.
|
38
|
+
class UnknownTransaction < Error
|
39
|
+
# This is the transaction ID that bitcoind didn't know about.
|
40
|
+
attr_reader :transaction_id
|
41
|
+
|
42
|
+
def initialize(transaction_id)
|
43
|
+
@transaction_id = transaction_id.freeze
|
44
|
+
super("Unknown transaction ID: #{@transaction_id.inspect}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# InsufficientFunds is raised when an account (or the bitcoind as a whole)
|
49
|
+
# doesn't have enough Bitcoin to perform an action.
|
50
|
+
class InsufficientFunds < Error
|
51
|
+
# This Float is the amount we would need to perform the action.
|
52
|
+
attr_reader :required
|
53
|
+
|
54
|
+
# This Float is the actual funds we have available to perform the action.
|
55
|
+
attr_reader :available
|
56
|
+
|
57
|
+
def initialize(required, available)
|
58
|
+
super("฿#{required} required, but only #{available} available")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# LockedWallet is raised when bitcoind refuses to send funds because the
|
63
|
+
# wallet is locked.
|
64
|
+
class LockedWallet < Error
|
65
|
+
def initialize()
|
66
|
+
super('You must unlock your wallet prior to sending funds.')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# InvalidPassphrase is raised when we attempt to decrypt the wallet with an
|
71
|
+
# invalid passphrase.
|
72
|
+
class InvalidPassphrase < Error
|
73
|
+
# This is the password we tried (unsuccessfully) to authenticate with.
|
74
|
+
attr_reader :bad_password
|
75
|
+
|
76
|
+
def initialize(bad_password)
|
77
|
+
@bad_password = bad_password
|
78
|
+
super("Invalid password: #{@bad_password.inspect}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# This class represents a block in the Bitcoin block chain. (c.f.
|
83
|
+
# https://en.bitcoin.it/wiki/Block and
|
84
|
+
# https://en.bitcoin.it/wiki/Block_hashing_algorithm)
|
85
|
+
class Block
|
86
|
+
# This is the Bitcoin::Client instance we are connected to.
|
87
|
+
attr_reader :bc
|
88
|
+
|
89
|
+
# This is the unique ID of the block. It is a String beginning with a number
|
90
|
+
# of '0's.
|
91
|
+
attr_reader :block_id
|
92
|
+
|
93
|
+
# This is the order of this block in the block chain, i.e. the number of
|
94
|
+
# blocks that came before it.
|
95
|
+
attr_reader :height
|
96
|
+
|
97
|
+
# This is the block version. Currently, it should be +1+.
|
98
|
+
attr_reader :version
|
99
|
+
|
100
|
+
# This is a base 16 String representation of a SHA-256 hash generated from
|
101
|
+
# all the transactions in the block.
|
102
|
+
attr_reader :merkle_root
|
103
|
+
|
104
|
+
# This is a 32-bit Fixnum used in order to get a @block_id matching @target.
|
105
|
+
attr_reader :nonce
|
106
|
+
|
107
|
+
# This is a Float representing how difficult it was to generate the block.
|
108
|
+
# It increases logarithmically in proportion to difficulty.
|
109
|
+
attr_reader :difficulty
|
110
|
+
|
111
|
+
# This is an Array of every Transaction that is part of the block.
|
112
|
+
attr_reader :transactions
|
113
|
+
|
114
|
+
# This is a compact representation of the prefix this block was attempting
|
115
|
+
# to match.
|
116
|
+
attr_reader :target
|
117
|
+
|
118
|
+
# +bc+ is a Bitcoin::Client instance. block_id is our unique (String) block
|
119
|
+
# ID. We raise UnknownBitcoinBlock if the bitcoind doesn't know about a
|
120
|
+
# block with that ID.
|
121
|
+
def initialize(bc, block_id)
|
122
|
+
@bc = bc
|
123
|
+
|
124
|
+
unless @bc.is_a?(Bitcoin::Client)
|
125
|
+
raise TypeError, "bc must be a Bitcoin::Client (#{@bc.class} given)"
|
126
|
+
end
|
127
|
+
|
128
|
+
unless block_id.is_a?(String)
|
129
|
+
raise TypeError, "block_id must be a String (#{block_id.class} given)"
|
130
|
+
end
|
131
|
+
|
132
|
+
begin
|
133
|
+
block_data = @bc.jr.getblock(block_id)
|
134
|
+
rescue Jr::ServerError => ex
|
135
|
+
if ex.code == -5
|
136
|
+
raise UnknownBlock, block_id
|
137
|
+
else
|
138
|
+
raise
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
{
|
143
|
+
block_id: :hash,
|
144
|
+
height: :height,
|
145
|
+
version: :version,
|
146
|
+
merkle_root: :merkleroot,
|
147
|
+
created_at_unix_time: :time,
|
148
|
+
nonce: :nonce,
|
149
|
+
difficulty: :difficulty,
|
150
|
+
transaction_ids: :tx,
|
151
|
+
previous_block_id: :nextblockhash,
|
152
|
+
next_block_id: :previoushblockhash
|
153
|
+
}.each do |our_attr, block_data_key|
|
154
|
+
instance_variable_set(
|
155
|
+
"@#{our_attr}",
|
156
|
+
block_data[block_data_key.to_s].freeze
|
157
|
+
)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# This is the Block created immediately after this one, or +nil+ if this is
|
162
|
+
# the most recent block.
|
163
|
+
def next_block
|
164
|
+
@bc.get_block(@next_block_id)
|
165
|
+
end
|
166
|
+
|
167
|
+
# This is the Block created prior to this one, or +nil+ if this is the
|
168
|
+
# origin block.
|
169
|
+
def previous_block
|
170
|
+
@bc.get_block(@previous_block_id)
|
171
|
+
end
|
172
|
+
|
173
|
+
# This is the Time the block was created at.
|
174
|
+
def created_at
|
175
|
+
@created_at ||= Time.at(@created_at_unix_time).utc.freeze
|
176
|
+
end
|
177
|
+
|
178
|
+
def inspect
|
179
|
+
"#<Bitcoin::Block #{@block_id}>"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# This represents a single Bitcoin transaction.
|
184
|
+
class Transaction
|
185
|
+
# This is the Bitcoin::Client instance we're connected to.
|
186
|
+
attr_reader :bc
|
187
|
+
|
188
|
+
# This (String) is a unique identifier assigned to this transaction.
|
189
|
+
attr_reader :transaction_id
|
190
|
+
|
191
|
+
# This is a Hash whose keys are the (String) bitcoin addresses involved in
|
192
|
+
# the transaction (whose private keys bitcoind has) and whose values are the
|
193
|
+
# (Float) amounts that the corresponding address gained (in which case the
|
194
|
+
# value would be positive) or lost (in which case the value would be
|
195
|
+
# negative).
|
196
|
+
attr_reader :amounts
|
197
|
+
|
198
|
+
# This is a Hash similar to @amounts, except that the values are the amounts
|
199
|
+
# each address paid in transaction fees. (c.f.
|
200
|
+
# https://en.bitcoin.it/wiki/Transaction_fees)
|
201
|
+
attr_reader :fees
|
202
|
+
|
203
|
+
# +bc+ is a Bitcoin::Client. (The String) +transaction_id+ is our unique
|
204
|
+
# transaction ID. If bitcoind doesn't know about a transaction with that ID,
|
205
|
+
# we raise UnknownTransaction. Note that bitcoind only knows about
|
206
|
+
# transactions involving private keys in our wallet even though information
|
207
|
+
# about other transactions is in the block chain.
|
208
|
+
def initialize(bc, transaction_id)
|
209
|
+
@bc = bc
|
210
|
+
@transaction_id = transaction_id.freeze
|
211
|
+
|
212
|
+
unless @bc.is_a?(Bitcoin::Client)
|
213
|
+
raise TypeError, "bc must be a Bitcoin::Client (#{@bc.class} given)"
|
214
|
+
end
|
215
|
+
|
216
|
+
begin
|
217
|
+
info = @bc.jr.gettransaction(transaction_id)
|
218
|
+
rescue Jr::ServerError => ex
|
219
|
+
if info.code == -5
|
220
|
+
raise UnknownTransaction, transaction_id
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
@unix_time = info.fetch('time')
|
225
|
+
|
226
|
+
@fees = Hash.new
|
227
|
+
@amounts = Hash.new
|
228
|
+
|
229
|
+
info.fetch('details').each do |detail|
|
230
|
+
address = detail.fetch('address').freeze
|
231
|
+
|
232
|
+
@amounts[address] = detail.fetch('amount').freeze
|
233
|
+
|
234
|
+
if detail['fee']
|
235
|
+
@fees[address] = detail['fee'].freeze
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
@fees.freeze
|
240
|
+
@amounts.freeze
|
241
|
+
end
|
242
|
+
|
243
|
+
# This is the Time the transaction was made.
|
244
|
+
def time
|
245
|
+
@time ||= Time.at(@unix_time).utc.freeze
|
246
|
+
end
|
247
|
+
|
248
|
+
# Does this transaction include +address+? (Note that this only works for
|
249
|
+
# addresses which we control.)
|
250
|
+
def include?(address)
|
251
|
+
address = address.to_s if address.is_a?(Address)
|
252
|
+
@fees.keys.include?(address) or @amounts.keys.include?(address)
|
253
|
+
end
|
254
|
+
|
255
|
+
def inspect
|
256
|
+
"#<Bitcoin::Transaction #{@transaction_id}>"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# This is a bitcoind account. (c.f.
|
261
|
+
# https://en.bitcoin.it/wiki/Accounts_explained)
|
262
|
+
class Account
|
263
|
+
# This is the Bitcoin::Client instance we are connected to.
|
264
|
+
attr_reader :bc
|
265
|
+
|
266
|
+
# This (String) is our account designation.
|
267
|
+
attr_reader :name
|
268
|
+
|
269
|
+
# +bc+ is a Bitcoin::Client instance. +account_name+ is the (String) name of
|
270
|
+
# the account we're associated with. +account_name+ may be +""+, in which
|
271
|
+
# case we represent the default account.
|
272
|
+
def initialize(bc, account_name)
|
273
|
+
@bc = bc
|
274
|
+
@name = account_name.freeze
|
275
|
+
|
276
|
+
unless @name.is_a?(String)
|
277
|
+
raise TypeError, "account_name must be a String (#{@name.class} given)"
|
278
|
+
end
|
279
|
+
|
280
|
+
unless @bc.is_a?(Bitcoin::Client)
|
281
|
+
raise TypeError, "bc must be a Bitcoin::Client (#{@bc.class} given)"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Get every Transaction associated with this account.
|
286
|
+
def transactions
|
287
|
+
grab = 20
|
288
|
+
position = 0
|
289
|
+
transactions = Array.new
|
290
|
+
|
291
|
+
loop do
|
292
|
+
new_transactions = @bc.jr.listtransactions(@name, grab, position)
|
293
|
+
transactions += new_transactions.map{|tx| tx.fetch('txid')}
|
294
|
+
|
295
|
+
if new_transactions.length < grab
|
296
|
+
break
|
297
|
+
else
|
298
|
+
position += grab
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
transactions.uniq.map do |tx|
|
303
|
+
Transaction.new(@bc, tx)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Fetch the balance of this account. Only deposits with at least
|
308
|
+
# +minimum_confirmations+ will be included in this total.
|
309
|
+
def balance(minimum_confirmations=1)
|
310
|
+
@bc.jr.getbalance(@name, minimum_confirmations)
|
311
|
+
end
|
312
|
+
|
313
|
+
# This is an Array of every Address associated with this account.
|
314
|
+
def addresses
|
315
|
+
@bc.jr.getaddressesbyaccount(@name).map(&@bc.method(:get_address))
|
316
|
+
end
|
317
|
+
|
318
|
+
# Get an unused Address associated with this account, or create one if one
|
319
|
+
# doesn't already exist.
|
320
|
+
def unused_address
|
321
|
+
@bc.get_address(@bc.jr.getaccountaddress(@name))
|
322
|
+
end
|
323
|
+
|
324
|
+
# Get a new Address associated with this account.
|
325
|
+
def new_address
|
326
|
+
@bc.get_address(@bc.jr.getnewaddress(@name))
|
327
|
+
end
|
328
|
+
|
329
|
+
# Send +amount+ Bitcoin to +dest+. +amount+ should be a positive real
|
330
|
+
# number; +dest+ can either be a (String) bitcoin address, or an Address
|
331
|
+
# instance. We return a Transaction.
|
332
|
+
def send(dest, amount)
|
333
|
+
dest = dest.to_s
|
334
|
+
|
335
|
+
begin
|
336
|
+
txid = @bc.jr.sendfrom(@name, dest, amount)
|
337
|
+
rescue Jr::ServerError => ex
|
338
|
+
case ex.code
|
339
|
+
when -13
|
340
|
+
raise LockedWallet
|
341
|
+
|
342
|
+
when -6
|
343
|
+
raise InsufficientFunds.new(amount, balance)
|
344
|
+
|
345
|
+
when -5
|
346
|
+
raise InvalidAddress, dest
|
347
|
+
|
348
|
+
else
|
349
|
+
raise
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
@bc.get_transaction(txid)
|
354
|
+
end
|
355
|
+
|
356
|
+
# +dests+ is a Hash whose keys are Address or String instances and whose
|
357
|
+
# values are positive real numbers. Each key is sent the amount of Bitcoin
|
358
|
+
# specified by its value. We return a Transaction.
|
359
|
+
def send_to_many(dests)
|
360
|
+
dests = Hash[
|
361
|
+
dests.map do |dest, amount|
|
362
|
+
[dest.to_s, amount]
|
363
|
+
end
|
364
|
+
]
|
365
|
+
|
366
|
+
begin
|
367
|
+
txid = @bc.jr.sendmany(@name, dest, amount)
|
368
|
+
rescue Jr::ServerError => ex
|
369
|
+
case ex.code
|
370
|
+
when -13
|
371
|
+
raise LockedWallet
|
372
|
+
|
373
|
+
when -6
|
374
|
+
raise InsufficientFunds.new(amount.values.reduce(&:+), balance)
|
375
|
+
|
376
|
+
when -5
|
377
|
+
raise InvalidAddress, ex.split(':').fetch(1)
|
378
|
+
|
379
|
+
else
|
380
|
+
raise
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
@bc.get_transaction(txid)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Donate +amount+ to katmagic (bc's author).
|
388
|
+
def donate(amount)
|
389
|
+
tx = send('1LzDffumxiCSh8wEpxWE8fUozb2LUTcL8L', amount)
|
390
|
+
|
391
|
+
if STDOUT.tty?
|
392
|
+
puts('katmagic l♥ves y♥u ♥♥dles!')
|
393
|
+
end
|
394
|
+
|
395
|
+
tx
|
396
|
+
end
|
397
|
+
|
398
|
+
def to_s # :nodoc:
|
399
|
+
@name
|
400
|
+
end
|
401
|
+
|
402
|
+
def inspect # :nodoc:
|
403
|
+
"#<Bitcoin::Account #{@name.inspect}>"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# This class represents a Bitcoin address.
|
408
|
+
class Address
|
409
|
+
# This is the Bitcoin::Client instance we are connected to.
|
410
|
+
attr_reader :bc
|
411
|
+
|
412
|
+
# This is our actual (String) Bitcoin address.
|
413
|
+
attr_reader :address
|
414
|
+
|
415
|
+
# +bc+ is a Bitcoin::Client instance. +address+ is a (String) Bitcoin
|
416
|
+
# address that we have the private key for.
|
417
|
+
def initialize(bc, address)
|
418
|
+
@bc = bc
|
419
|
+
@address = address
|
420
|
+
|
421
|
+
unless @bc.is_a?(Bitcoin::Client)
|
422
|
+
raise TypeError, "bc must be a Bitcoin::Client (#{@bc.class} given)"
|
423
|
+
end
|
424
|
+
|
425
|
+
unless valid?
|
426
|
+
raise InvalidAddress, @address
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# Return an Array of every Transaction associated with us.
|
431
|
+
def transactions
|
432
|
+
account.transactions.find_all do |tx|
|
433
|
+
tx.include?(self)
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
# Are we a valid Bitcoin address?
|
438
|
+
def valid?
|
439
|
+
@bc.is_valid?(@address)
|
440
|
+
end
|
441
|
+
|
442
|
+
# This is the (String) private key associated with this address. If this
|
443
|
+
# account's address is invalid (or from the wrong bitcoin network), we raise
|
444
|
+
# InvalidAddress. If we don't have the private key associated with this
|
445
|
+
# address (or our wallet is locked), we return +nil+.
|
446
|
+
def private_key
|
447
|
+
begin
|
448
|
+
@private_key ||= @bc.jr.dumpprivkey(@address)
|
449
|
+
rescue Jr::ServerError => ex
|
450
|
+
$ex = ex.code
|
451
|
+
case ex.code
|
452
|
+
when -5
|
453
|
+
raise(InvalidAddress, @address)
|
454
|
+
when -4
|
455
|
+
nil
|
456
|
+
else
|
457
|
+
raise
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Get the Account we're associated with.
|
463
|
+
def account
|
464
|
+
@bc.get_account(@bc.jr.getaccount(@address))
|
465
|
+
end
|
466
|
+
|
467
|
+
# Associate us with +account+, which can either be a String or an Account.
|
468
|
+
# Either way, we will return an Account.
|
469
|
+
def account=(account)
|
470
|
+
@bc.jr.setaccount(@address, account.to_s)
|
471
|
+
@bc.get_account(account.to_s)
|
472
|
+
end
|
473
|
+
|
474
|
+
# Sign the (String) message +msg+. We return a detached base-64 encoded
|
475
|
+
# signature (String). In order to verify the message, you will need both the
|
476
|
+
# signature and +msg+. If we don't know private key, we raise
|
477
|
+
# UnknownPrivateKey. (c.f. Client.verify())
|
478
|
+
def sign(msg)
|
479
|
+
begin
|
480
|
+
@bc.jr.signmessage(@address, msg)
|
481
|
+
rescue Jr::ServerError => ex
|
482
|
+
case ex.code
|
483
|
+
when -13
|
484
|
+
raise LockedWallet
|
485
|
+
|
486
|
+
when -4
|
487
|
+
raise UnknownPrivateKey, @address
|
488
|
+
|
489
|
+
else
|
490
|
+
raise
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# Verify the signature +sig+ of a message +msg+ signed by our private key.
|
496
|
+
# We return +true+ if the signature is valid, or +false+ if it is not.
|
497
|
+
def verify(msg, sig)
|
498
|
+
@bc.jr.verifymessage(@address, msg, sig)
|
499
|
+
end
|
500
|
+
|
501
|
+
def to_s # :nodoc:
|
502
|
+
@address
|
503
|
+
end
|
504
|
+
|
505
|
+
def inspect # :nodoc:
|
506
|
+
"#<Bitcoin::Address #{@address}>"
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
class Client
|
511
|
+
attr_reader :user, :password, :host, :port, :ssl
|
512
|
+
# @jr is a Jr::Jr instance connected to the bitcoind.
|
513
|
+
attr_reader :jr
|
514
|
+
|
515
|
+
def initialize(user, password, host='127.0.0.1', port=8331, ssl=false)
|
516
|
+
%w{user password host port ssl}.each do |var|
|
517
|
+
instance_variable_set("@#{var}", eval(var))
|
518
|
+
end
|
519
|
+
|
520
|
+
@jr = Jr::Jr.new(@host, @port, @user, @password, ssl)
|
521
|
+
|
522
|
+
@accounts = Hash.new
|
523
|
+
@addresses = Hash.new
|
524
|
+
@blocks = Hash.new
|
525
|
+
@transactions = Hash.new
|
526
|
+
end
|
527
|
+
|
528
|
+
# Get an Array of every Address we have.
|
529
|
+
def addresses
|
530
|
+
@jr.listreceivedbyaddress(0, true).map do |addr_info|
|
531
|
+
get_address(addr_info.fetch('address'))
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
# Get an Array of every Account we have.
|
536
|
+
def accounts
|
537
|
+
@jr.listreceivedbyaccount(0, true).map do |acct_info|
|
538
|
+
get_account(acct_info.fetch('account'))
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# Get the Address +addr+. If +addr+ is invalid, we raise InvalidAddress. The
|
543
|
+
# result of this function is cached.
|
544
|
+
def get_address(addr)
|
545
|
+
@addresses[addr] ||= Address.new(self, addr)
|
546
|
+
end
|
547
|
+
|
548
|
+
# Get the account associated with the String +label+, or create it if it
|
549
|
+
# doesn't already exist. The result of this function is cached.
|
550
|
+
def get_account(label)
|
551
|
+
@accounts[label] ||= Account.new(self, label)
|
552
|
+
end
|
553
|
+
alias_method :[], :get_account
|
554
|
+
|
555
|
+
# Does +acct+ have any associated addresses?
|
556
|
+
def has_account?(acct)
|
557
|
+
!get_account(acct).addresses.empty?
|
558
|
+
end
|
559
|
+
|
560
|
+
# Get the Block with a hash of +block_id+, or if +block_id+ is a Fixnum, the
|
561
|
+
# Block with a height of +block_id+. If +block_id+ is an unknown block ID,
|
562
|
+
# we raise UnknownBlock+; if +block_id+ is a Fixnum and there is no
|
563
|
+
# associated block, we raise RangeError. The result of this function is
|
564
|
+
# cached.
|
565
|
+
def get_block(block_id)
|
566
|
+
if block_id.is_a?(Fixnum)
|
567
|
+
begin
|
568
|
+
block_id = @jr.getblockhash(block_id)
|
569
|
+
rescue Jr::ServerError => ex
|
570
|
+
if ex.code == -1
|
571
|
+
raise RangeError, "block_id #{block_id.inspect} is out of range."
|
572
|
+
else
|
573
|
+
raise
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
@blocks[block_id] ||= Block.new(self, block_id)
|
579
|
+
end
|
580
|
+
|
581
|
+
# Get the Transaction with the ID +transaction_id+. We raise
|
582
|
+
# UnknownTransaction if we don't know about a Transaction with that ID.
|
583
|
+
# The result of this function is cached.
|
584
|
+
def get_transaction(transaction_id)
|
585
|
+
if transaction_id.is_a?(Fixnum)
|
586
|
+
end
|
587
|
+
|
588
|
+
@transactions[transaction_id] ||= Transaction.new(self, transaction_id)
|
589
|
+
end
|
590
|
+
|
591
|
+
# Get an Array of every Transaction that has occurred since +block+. If
|
592
|
+
# +block+ is not a Block it will be converted into one with get_block().
|
593
|
+
def get_transactions_since(block)
|
594
|
+
unless block.is_a?(Block)
|
595
|
+
block = get_block(block)
|
596
|
+
end
|
597
|
+
|
598
|
+
@jr.listsinceblock(block.block_id).fetch('transactions').map do |tx|
|
599
|
+
tx.fetch('txid')
|
600
|
+
end.uniq.map(&method(:get_transaction))
|
601
|
+
end
|
602
|
+
|
603
|
+
# Get an Array of every Transaction involving one of our addresses.
|
604
|
+
def transactions
|
605
|
+
get_transactions_since(0)
|
606
|
+
end
|
607
|
+
|
608
|
+
# This is the latest Block we've processed.
|
609
|
+
def latest_block
|
610
|
+
get_block( @jr.getinfo().fetch('blocks') )
|
611
|
+
end
|
612
|
+
|
613
|
+
# Send +amount+ Bitcoin to +dest+. +amount+ should be a positive real
|
614
|
+
# number; +dest+ can either be a String bitcoin address, or an Address
|
615
|
+
# instance. We return a Transaction.
|
616
|
+
def send(dest, amount)
|
617
|
+
dest = dest.to_s
|
618
|
+
|
619
|
+
begin
|
620
|
+
txid = @jr.sendtoaddress(dest, amount)
|
621
|
+
rescue Jr::ServerError => ex
|
622
|
+
case ex.code
|
623
|
+
when -13
|
624
|
+
raise LockedWallet
|
625
|
+
|
626
|
+
when -6
|
627
|
+
raise InsufficientFunds.new(amount, balance)
|
628
|
+
|
629
|
+
when -5
|
630
|
+
raise InvalidAddress, dest
|
631
|
+
|
632
|
+
else
|
633
|
+
raise
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
get_transaction(txid)
|
638
|
+
end
|
639
|
+
|
640
|
+
# Donate +amount+ to katmagic (bc's author).
|
641
|
+
def donate(amount)
|
642
|
+
tx = send('1LzDffumxiCSh8wEpxWE8fUozb2LUTcL8L', amount)
|
643
|
+
|
644
|
+
if STDOUT.tty?
|
645
|
+
puts('katmagic l♥ves y♥u ♥♥dles!')
|
646
|
+
end
|
647
|
+
|
648
|
+
tx
|
649
|
+
end
|
650
|
+
|
651
|
+
# Safely copies our wallet to destination at +path+. If +path+ is a
|
652
|
+
# directory, we will copy our wallet to +path+/wallet.dat.
|
653
|
+
def backup_wallet(path)
|
654
|
+
@jr.backupwallet(path)
|
655
|
+
end
|
656
|
+
|
657
|
+
# Encrypt the wallet with +passwd+ and stop bitcoind.
|
658
|
+
def encrypt_wallet(passwd)
|
659
|
+
@jr.encryptwallet(passwd)
|
660
|
+
end
|
661
|
+
|
662
|
+
# Change the wallet's passphrase from +old_passwd+ to +new_passwd+.
|
663
|
+
def change_wallet_passwd(old_passwd, new_passwd)
|
664
|
+
begin
|
665
|
+
@jr.walletpassphrasechange(old_passwd, new_passwd)
|
666
|
+
rescue Jr::ServerError => ex
|
667
|
+
if ex.code == -14
|
668
|
+
raise InvalidPassphrase, passwd
|
669
|
+
else
|
670
|
+
raise
|
671
|
+
end
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
# Unlock the wallet with +passwd+ for +timeout+ seconds.
|
676
|
+
def unlock_wallet(passwd, timeout=300)
|
677
|
+
begin
|
678
|
+
@jr.walletpassphrase(passwd, timeout)
|
679
|
+
rescue Jr::ServerError => ex
|
680
|
+
if ex.code == -14
|
681
|
+
raise InvalidPassphrase, passwd
|
682
|
+
else
|
683
|
+
raise
|
684
|
+
end
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
# Lock the wallet.
|
689
|
+
def lock_wallet()
|
690
|
+
@jr.walletlock()
|
691
|
+
end
|
692
|
+
|
693
|
+
# Get the total balance of all our accounts. (This includes all transactions
|
694
|
+
# with at least 1 confirmation.)
|
695
|
+
def balance
|
696
|
+
@jr.getbalance()
|
697
|
+
end
|
698
|
+
|
699
|
+
# How many blocks are there (that we know about) in the block chain?
|
700
|
+
def block_count
|
701
|
+
@jr.getblockcount()
|
702
|
+
end
|
703
|
+
|
704
|
+
# How many peers are we connected to?
|
705
|
+
def connection_count
|
706
|
+
@jr.getconnectioncount()
|
707
|
+
end
|
708
|
+
|
709
|
+
# This is a Float representing the difficulty associated with finding the
|
710
|
+
# next block. The higher the number, the more difficult it is. (c.f.
|
711
|
+
# https://en.bitcoin.it/wiki/Difficulty)
|
712
|
+
def difficulty
|
713
|
+
@jr.getdifficulty()
|
714
|
+
end
|
715
|
+
|
716
|
+
# Are we trying to generate a block?
|
717
|
+
def generate?
|
718
|
+
@jr.getgenerate()
|
719
|
+
end
|
720
|
+
|
721
|
+
alias_method :generate, :generate?
|
722
|
+
|
723
|
+
# If +should_generate+ is +true+, we instruct bitcoind to begin (or
|
724
|
+
# continue) to generate a block. If it is +false+, we do the opposite.
|
725
|
+
def generate=(should_generate)
|
726
|
+
@jr.setgenerate(should_generate)
|
727
|
+
should_generate
|
728
|
+
end
|
729
|
+
|
730
|
+
# How many blocks are we hashing per second? This will be zero unless we're
|
731
|
+
# trying to generate a block.
|
732
|
+
def hashes_per_second
|
733
|
+
@jr.gethashespersec()
|
734
|
+
end
|
735
|
+
|
736
|
+
# This [Fixnum] is the version of bitcoind we're connecting to.
|
737
|
+
def bitcoind_version
|
738
|
+
@jr.getinfo().fetch('version')
|
739
|
+
end
|
740
|
+
|
741
|
+
# This (Fixnum) is the version of the Bitcoin RPC protocol we're
|
742
|
+
# communicating in.
|
743
|
+
def protocol_version
|
744
|
+
@jr.getinfo().fetch('protocolversion')
|
745
|
+
end
|
746
|
+
|
747
|
+
# This is the proxy bitcoind is using, or +nil+ if we're not using a proxy.
|
748
|
+
def proxy
|
749
|
+
@jr.getinfo().fetch('proxy')
|
750
|
+
end
|
751
|
+
|
752
|
+
# Is bitcoind using a proxy?
|
753
|
+
def proxy?
|
754
|
+
!!proxy
|
755
|
+
end
|
756
|
+
|
757
|
+
# Are we on the testnet?
|
758
|
+
def testnet?
|
759
|
+
@jr.getinfo().fetch('testnet')
|
760
|
+
end
|
761
|
+
|
762
|
+
# This is how much we're configured to use as our transaction fee. (c.f.
|
763
|
+
# https://en.bitcoin.it/wiki/Transaction_fees)
|
764
|
+
def transaction_fee
|
765
|
+
@jr.getinfo().fetch('paytxfee')
|
766
|
+
end
|
767
|
+
|
768
|
+
# This is the Time the oldest key in our key pool was created. (c.f.
|
769
|
+
# key_pool_size())
|
770
|
+
def oldest_key
|
771
|
+
Time.at(@jr.getinfo().fetch('keypoololdest'))
|
772
|
+
end
|
773
|
+
|
774
|
+
# This is the (Fixnum) size of our key pool. The key pool is a pre-generated
|
775
|
+
# set of Bitcoin keys which are then allocated through the other address
|
776
|
+
# allocation mechanisms. It exists so that backups will (hopefully) contain
|
777
|
+
# all the private keys we actually used.
|
778
|
+
def key_pool_size
|
779
|
+
@jr.getinfo().fetch('keypoolsize')
|
780
|
+
end
|
781
|
+
|
782
|
+
# This is a Hash containing data about the next block that will be generated
|
783
|
+
# (c.f. the documentation regarding the getmemorypool API call in
|
784
|
+
# https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list)
|
785
|
+
def memory_pool
|
786
|
+
@jr.getmemorypool()
|
787
|
+
end
|
788
|
+
|
789
|
+
# Import a Bitcoin private key. We will fail if the key already exists in
|
790
|
+
# our wallet. We return an Address. (c.f. get_private_key())
|
791
|
+
def import_private_key(key, label=nil)
|
792
|
+
label ||= ''
|
793
|
+
@jr.get_address( @jr.importprivkey(key, label) )
|
794
|
+
end
|
795
|
+
|
796
|
+
# Refill our key pool. (c.f. key_pool_size())
|
797
|
+
def refill_key_pool()
|
798
|
+
@jr.keypoolrefill()
|
799
|
+
end
|
800
|
+
|
801
|
+
# Set the transaction fee to +fee+. (c.f.
|
802
|
+
# https://en.bitcoin.it/wiki/Transaction_fees)
|
803
|
+
def transaction_fee=(fee)
|
804
|
+
fee = fee.to_f
|
805
|
+
@jr.settxfee(fee)
|
806
|
+
fee
|
807
|
+
end
|
808
|
+
|
809
|
+
# Stop bitcoind.
|
810
|
+
def stop()
|
811
|
+
@jr.stop()
|
812
|
+
end
|
813
|
+
|
814
|
+
# Is +addr+ a valid bitcoin address? If we're using the testnet, normal
|
815
|
+
# addresses won't be valid; if we're not, testnet addresses won't be valid.
|
816
|
+
def is_valid?(addr)
|
817
|
+
@jr.validateaddress(addr.to_s)['isvalid']
|
818
|
+
end
|
819
|
+
end
|
820
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- katmagic
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain:
|
12
|
+
- ! '-----BEGIN CERTIFICATE-----
|
13
|
+
|
14
|
+
MIIDQDCCAiigAwIBAgIBADANBgkqhkiG9w0BAQUFADBGMRgwFgYDVQQDDA90aGUu
|
15
|
+
|
16
|
+
bWFnaWNhbC5rYXQxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixk
|
17
|
+
|
18
|
+
ARkWA2NvbTAeFw0xMTA4MjEyMjMyMDFaFw0xMjA4MjAyMjMyMDFaMEYxGDAWBgNV
|
19
|
+
|
20
|
+
BAMMD3RoZS5tYWdpY2FsLmthdDEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYK
|
21
|
+
|
22
|
+
CZImiZPyLGQBGRYDY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
23
|
+
|
24
|
+
pBt20nwjs5W03djpRN6FAbpiio286NHMTk6HhmjV6GZKOi5ZUX5onTnKUg2Vc35z
|
25
|
+
|
26
|
+
/nK+aIPReyRfBgIcfSjhoXh1A1Dp+2laNgTtU/3eMupruatgORAPCSaG9Ns+HSyR
|
27
|
+
|
28
|
+
vySbz1QUrwvlvF0qkhhApNQ6dsLl2LMOV3QcluY+Y3CVccOWOSHdQcnAbPuzM9Hf
|
29
|
+
|
30
|
+
4ChI4OGL7+DwLA5OK2S5uewRAa2iLkJSN0WugnQlJqMT59GRaqTDOtnYQpiyKEBy
|
31
|
+
|
32
|
+
QjVPO4LNk7iDsJP22YBrveIzm8/YYRBTU4LTHMEMOyCszrYqD2S1Lwp2rtCJzQCl
|
33
|
+
|
34
|
+
BA0LtBKrZl5mwZm7qyj+TwIDAQABozkwNzAJBgNVHRMEAjAAMB0GA1UdDgQWBBSm
|
35
|
+
|
36
|
+
s5arhjp61kmGl6wsmLYkqerdqDALBgNVHQ8EBAMCBLAwDQYJKoZIhvcNAQEFBQAD
|
37
|
+
|
38
|
+
ggEBAA6cQNQMOPRy4yrj7Nh5Mb9qq8t/8ho/JQvjzVof9qRd+kfKrOoOhXfEO+Rm
|
39
|
+
|
40
|
+
sWcaOnBCVC4DnZuNDSLygVhCDtMnHjg/JsfO/GBF/QlNTJOO1jkoQiS6w0KARlBm
|
41
|
+
|
42
|
+
cpXaWg/oMtXJ2PaUga6WkNeXYf9Mad36P4yuGQScjs+WkUUy7DNZvTGReIcCWOR8
|
43
|
+
|
44
|
+
jteSvvCMobQKGr2DfFOU9Jiddh2FPpz/KOM2ijzwsVNUMUr7R58LoCnQZrZ/YaRW
|
45
|
+
|
46
|
+
ob6QnVgwqu5SUAKQxlFJ/aKlPMj735z8EogaZC1ZHgg3vkgGGyu57N/8BDDG0TzC
|
47
|
+
|
48
|
+
Zn3u2leVae/fJ03zYGArhuJKPgc=
|
49
|
+
|
50
|
+
-----END CERTIFICATE-----
|
51
|
+
|
52
|
+
'
|
53
|
+
date: 2012-03-05 00:00:00.000000000 Z
|
54
|
+
dependencies:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: jr
|
57
|
+
requirement: &76283090 !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: *76283090
|
66
|
+
description: bc is a Ruby interface to bitcoind.
|
67
|
+
email: the.magical.kat@gmail.com
|
68
|
+
executables: []
|
69
|
+
extensions: []
|
70
|
+
extra_rdoc_files: []
|
71
|
+
files:
|
72
|
+
- lib/bc.rb
|
73
|
+
- README.md
|
74
|
+
- UNLICENSE
|
75
|
+
homepage: https://github.com/katmagic/bc
|
76
|
+
licenses: []
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.8.10
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: Interface with bitcoid.
|
99
|
+
test_files: []
|
100
|
+
has_rdoc:
|
metadata.gz.sig
ADDED