bc 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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