dropzone_ruby 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Drop Zone - An Anonymous Peer-To-Peer Local Contraband Marketplace.pdf +0 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +69 -0
  6. data/README.md +62 -0
  7. data/bin/dropzone +487 -0
  8. data/dropzone-screenshot.jpg +0 -0
  9. data/dropzone_ruby.gemspec +31 -0
  10. data/lib/blockrio_ext.rb +52 -0
  11. data/lib/dropzone/buyer.rb +21 -0
  12. data/lib/dropzone/command.rb +488 -0
  13. data/lib/dropzone/communication.rb +43 -0
  14. data/lib/dropzone/connection.rb +312 -0
  15. data/lib/dropzone/invoice.rb +23 -0
  16. data/lib/dropzone/item.rb +160 -0
  17. data/lib/dropzone/listing.rb +64 -0
  18. data/lib/dropzone/message_base.rb +178 -0
  19. data/lib/dropzone/payment.rb +36 -0
  20. data/lib/dropzone/profile.rb +86 -0
  21. data/lib/dropzone/record_base.rb +34 -0
  22. data/lib/dropzone/seller.rb +21 -0
  23. data/lib/dropzone/session.rb +161 -0
  24. data/lib/dropzone/state_accumulator.rb +39 -0
  25. data/lib/dropzone/version.rb +4 -0
  26. data/lib/dropzone_ruby.rb +14 -0
  27. data/lib/veto_checks.rb +74 -0
  28. data/spec/bitcoin_spec.rb +115 -0
  29. data/spec/buyer_profile_spec.rb +279 -0
  30. data/spec/buyer_spec.rb +109 -0
  31. data/spec/command_spec.rb +353 -0
  32. data/spec/config.yml +5 -0
  33. data/spec/invoice_spec.rb +129 -0
  34. data/spec/item_spec.rb +294 -0
  35. data/spec/lib/fake_connection.rb +97 -0
  36. data/spec/listing_spec.rb +150 -0
  37. data/spec/payment_spec.rb +152 -0
  38. data/spec/seller_profile_spec.rb +290 -0
  39. data/spec/seller_spec.rb +120 -0
  40. data/spec/session_spec.rb +303 -0
  41. data/spec/sham/buyer.rb +5 -0
  42. data/spec/sham/invoice.rb +5 -0
  43. data/spec/sham/item.rb +8 -0
  44. data/spec/sham/payment.rb +13 -0
  45. data/spec/sham/seller.rb +7 -0
  46. data/spec/spec_helper.rb +49 -0
  47. metadata +267 -0
Binary file
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/dropzone/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Miracle Max"]
6
+ gem.email = ["17Q4MX2hmktmpuUKHFuoRmS5MfB5XPbhod@mail2tor.com"]
7
+ gem.summary = "An Anonymous Peer-To-Peer Local Contraband Marketplace"
8
+ gem.description = 'Drop Zone is a solution to the problem of restricted sales in censored markets. The proposal is for the design of a protocol and reference client that encodes the location and a brief description of a good onto The Blockchain. Those wishing to purchase the good can search for items within a user-requested radius. Sellers list a good as available within a geographic region, subject to some degree of precision, for the purpose of obfuscating their precise location. Goods are announced next to an expiration, a hashtag, and if space permits, a description. Once a buyer finds a good in a defined relative proximity, a secure communication channel is opened between the parties on the Bitcoin test network ("testnet"). Once negotiations are complete, the buyer sends payment to the seller via the address listed on the Bitcoin mainnet. This spend action establishes reputation for the buyer, and potentially for the seller. Once paid, the seller is to furnish the exact GPS coordinates of the good to the buyer (alongside a small note such as "Check in the crevice of the tree"). When the buyer successfully picks up the item at the specified location, the buyer then issues a receipt with a note by spending flake to the address of the original post. In this way, sellers receive a reputation score. The solution is akin to that of Craigslist.org or Uber, but is distributed and as such provides nearly risk-free terms to contraband sellers, and drastically reduced risk to contraband buyers.'
9
+ gem.homepage = "https://github.com/17Q4MX2hmktmpuUKHFuoRmS5MfB5XPbhod/dropzone_ruby"
10
+ gem.files = `git ls-files`.split(/\n/m)
11
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ gem.test_files = gem.files.grep(%r{^(spec)/})
13
+ gem.name = "dropzone_ruby"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = Dropzone::VERSION
16
+ gem.required_ruby_version = '>= 1.9'
17
+ gem.license = 'LGPL'
18
+
19
+ gem.add_runtime_dependency 'counterparty_ruby', '~> 1.2'
20
+ gem.add_runtime_dependency 'commander', '~> 4.3'
21
+ gem.add_runtime_dependency 'sequel', '~> 4.21'
22
+ gem.add_runtime_dependency 'sqlite3', '~> 1.3'
23
+ gem.add_runtime_dependency 'veto', '~> 1.0'
24
+ gem.add_runtime_dependency 'socksify', '~> 1.7'
25
+
26
+ gem.add_development_dependency 'rspec', '~> 3.2'
27
+ gem.add_development_dependency 'rake', '~> 10.4'
28
+ gem.add_development_dependency 'rdoc', '~> 4.2'
29
+ gem.add_development_dependency 'sham', '~> 1.1'
30
+ end
31
+
@@ -0,0 +1,52 @@
1
+ require 'time'
2
+
3
+ class BlockrIo
4
+ # NOTE: The blockr Api only shows the most recent 200 transactions on an
5
+ # address. Either this method should use the non-API query mode, or an
6
+ # alternate API driver will be needed to properly support this method.
7
+ #
8
+ # Additionally, this api call returns the block times, but not the relay times
9
+ # A better api would sort these by relay times.
10
+ def listtransactions(addr, include_unconfirmed = false)
11
+ # Confirmed Transactions:
12
+ ret = json_get('address', 'txs', addr)['data']['txs'].sort_by{|t|
13
+ t['confirmations'] }
14
+
15
+ if include_unconfirmed
16
+ unconfirmed = json_get('address', 'unconfirmed', addr)['data']['unconfirmed']
17
+ # We don't need every output listed, just the tx:
18
+ unconfirmed.uniq!{|tx_h| tx_h['tx']}
19
+ ret.unshift(*unconfirmed.sort_by{|t| Time.parse t['time_utc']}.reverse)
20
+ end
21
+
22
+ ret
23
+ end
24
+
25
+ # The blockr.io block/txs method appears to omit some transactions. As such,
26
+ # we'll be using blockchain.info for this query.
27
+ # Ideally, this would work: json_get('block', 'txs', number.to_s)['data']['txs']
28
+ def getblock(number)
29
+ block_hash = getblockinfo(number)['hash']
30
+
31
+ resp = RestClient::Resource.new( [ 'https://blockchain.info', 'block-index',
32
+ block_hash ].join('/')+'?format=json' ).get(content_type: 'json')
33
+
34
+ raise ResponseError if resp.code != 200
35
+
36
+ JSON.parse(resp)['tx'].collect do |tx, i|
37
+ {'tx' => tx['hash'], 'out' => tx['out']}
38
+ end
39
+ end
40
+
41
+ def getbalance(addr)
42
+ json_get('address', 'balance', addr)['data']['balance']
43
+ end
44
+
45
+ def getrawtransaction(tx_id)
46
+ json_get('tx', 'raw', tx_id.to_s)['data']['tx']
47
+ end
48
+
49
+ def getblockinfo(hash)
50
+ json_get('block', 'info', hash)['data']
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ module Dropzone
2
+ class Buyer < MessageBase
3
+ attr_message d: :description, a: :alias
4
+
5
+ attr_message_pkey t: :transfer_pkey
6
+
7
+ message_type 'BYUPDT'
8
+ end
9
+
10
+ class Buyer::Validator < ValidatorBase
11
+ include MessageValidations
12
+ include ProfileValidations
13
+
14
+ validates :message_type, format: /\ABYUPDT\Z/
15
+
16
+ validates_if_present :description, is_string: true
17
+ validates_if_present :alias, is_string: true
18
+
19
+ validates_if_present :transfer_pkey, is_pkey: true
20
+ end
21
+ end
@@ -0,0 +1,488 @@
1
+ require 'sequel'
2
+
3
+ # This speeds up the CLI considerably, without any opportunity cost, so it's
4
+ # included for the sake of convenience
5
+ class LocalConnectionCache
6
+ def initialize
7
+ config_dir = File.join(Dir.home, ".dropzone")
8
+ Dir.mkdir config_dir, 0700 unless Dir.exists? config_dir
9
+
10
+ @persistence = Sequel.connect 'sqlite://%s/cache.db' % config_dir
11
+
12
+ @persistence.create_table :transactions do
13
+ primary_key :id
14
+ String :txid
15
+ String :receiver_addr
16
+ String :sender_addr
17
+ File :data
18
+ Boolean :is_testing
19
+ Integer :block_height
20
+ end unless @persistence.table_exists?(:transactions)
21
+
22
+ @transactions = @persistence[:transactions]
23
+ @listtransactions = {}
24
+ end
25
+
26
+ def tx_by_id(id, is_testing)
27
+ record_to_tx transactions.where(txid: id, is_testing: is_testing).first
28
+ end
29
+
30
+ def tx_by_id!(id, hash, is_testing)
31
+ transactions.insert(hash.tap{ |et|
32
+ et[:data] = Sequel.blob et[:data]
33
+ et[:txid] = id
34
+ et[:is_testing] = is_testing
35
+ }).to_s
36
+ end
37
+
38
+ def listtransactions(*key)
39
+ @listtransactions.has_key?(key) ? @listtransactions[key] : nil
40
+ end
41
+
42
+ def listtransactions!(addr, is_confirmed, value, is_testing)
43
+ @listtransactions[ [addr, is_confirmed, is_testing] ] = value
44
+ end
45
+
46
+ def invalidate_listtransactions!
47
+ @listtransactions = {}
48
+ end
49
+
50
+ private
51
+
52
+ def record_to_tx(record)
53
+ record.tap{|r| r.delete(:id) } if record
54
+ end
55
+
56
+ def transactions
57
+ @transactions
58
+ end
59
+ end
60
+
61
+ # This is mostly intended for use by the CLI client, though it's conceivable
62
+ # that others may find it useful in some contexts.
63
+ class DropZoneCommand
64
+ MAX_TABLE_WIDTH = 80
65
+
66
+ class << self
67
+ def create_command(action, klass, label, *attributes, &block)
68
+ define_method(action) do |args, options|
69
+ privkey = privkey_from args
70
+
71
+ params = parameterize options, *attributes
72
+
73
+ block.call privkey, args, params if block_given?
74
+
75
+ message = klass.new params
76
+
77
+ txid = message.save! privkey.to_base58
78
+
79
+ puts_object '%s: %s' % [label, message.receiver_addr], 'Tx: %s' % txid, attributes,
80
+ message
81
+ end
82
+ end
83
+
84
+ def show_command(action, klass, label, finder, *attributes, &block)
85
+ define_method(action) do |args, options|
86
+ id = self.send finder, args
87
+
88
+ record = (block_given?) ? block.call(klass, id) : klass.new(id)
89
+
90
+ if record.respond_to?(:found?) and !record.found?
91
+ puts "%s Not Found" % label
92
+ else
93
+ params = attributes.collect{|attr| [attr,record.send(attr)]}.to_h
94
+
95
+ puts_object '%s: %s' % [label, id], nil, attributes, record
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ LISTING_ATTRS = [ :latitude, :longitude, :radius, :price_currency,
102
+ :price_in_units, :description, :expiration_in ]
103
+ PROFILE_ATTRS = [ :alias, :description ]
104
+ INVOICE_ATTRS = [ :amount_due, :expiration_in ]
105
+ PAYMENT_ATTRS = [:description, :delivery_quality, :product_quality,
106
+ :communications_quality, :invoice_txid ]
107
+
108
+ ADDRESS_TO_SELF = lambda{|privkey, args, params|
109
+ params.merge!(receiver_addr: privkey.addr) }
110
+
111
+ RECORD_BY_FIND = lambda{|klass, id| klass.find id }
112
+
113
+ show_command :listing_show, Dropzone::Listing, 'Listing', :by_txid,
114
+ *LISTING_ATTRS+[:addr]
115
+
116
+ show_command :profile_buyer_show, Dropzone::BuyerProfile, 'Buyer', :by_addr,
117
+ *PROFILE_ATTRS
118
+
119
+ show_command :profile_seller_show, Dropzone::SellerProfile, 'Seller',:by_addr,
120
+ *(PROFILE_ATTRS+[:communications_pkey] )
121
+
122
+ show_command :invoice_show, Dropzone::Invoice, 'Invoice', :by_txid,
123
+ *INVOICE_ATTRS+[:sender_addr, :receiver_addr], &RECORD_BY_FIND
124
+
125
+ show_command :review_show, Dropzone::Payment, 'Review',:by_txid,
126
+ *PAYMENT_ATTRS+[:sender_addr, :receiver_addr], &RECORD_BY_FIND
127
+
128
+ create_command :profile_buyer_create, Dropzone::Buyer, 'Buyer',
129
+ *(PROFILE_ATTRS+[:transfer_pkey] ), &ADDRESS_TO_SELF
130
+
131
+ create_command :profile_seller_create, Dropzone::Seller, 'Seller',
132
+ *(PROFILE_ATTRS+[:transfer_pkey, :communications_pkey] ), &ADDRESS_TO_SELF
133
+
134
+ create_command(:invoice_create, Dropzone::Invoice, 'Invoice',
135
+ *INVOICE_ATTRS) do |privkey, args, params|
136
+ receiver_addr = args[1] if args.length > 1
137
+
138
+ raise OptionParser::MissingArgument, 'receiver_addr' unless (
139
+ receiver_addr && Bitcoin.valid_address?(receiver_addr) )
140
+
141
+ params.merge! receiver_addr: receiver_addr
142
+ end
143
+
144
+ create_command(:review_create, Dropzone::Payment, 'Review',
145
+ *PAYMENT_ATTRS ) do |privkey, args, params|
146
+ invoice_txid = args[1] if args.length > 1
147
+
148
+ raise OptionParser::MissingArgument, 'invoice_txid' unless invoice_txid
149
+
150
+ invoice = Dropzone::Invoice.find invoice_txid
151
+
152
+ raise 'Invoice Not Found' unless invoice
153
+
154
+ params.merge! receiver_addr: invoice.sender_addr, invoice_txid: invoice_txid
155
+ end
156
+
157
+ create_command( :listing_create, Dropzone::Item, 'Listing',
158
+ *LISTING_ATTRS) do |privkey, args, params|
159
+ %w(longitude latitude radius).each do |attr|
160
+ raise OptionParser::MissingArgument, attr unless params[attr.to_sym]
161
+ end
162
+ end
163
+
164
+ create_command( :listing_update, Dropzone::Item, 'Listing',
165
+ :description, :price_currency, :price_in_units,
166
+ :expiration_in ) do |privkey, args, params|
167
+
168
+ create_txid = args[1] if args.length > 1
169
+
170
+ raise OptionParser::MissingArgument, 'create_txid' unless create_txid
171
+
172
+ params.merge! receiver_addr: privkey.addr, create_txid: create_txid
173
+ end
174
+
175
+ def initialize(is_spec = false)
176
+ @is_spec = is_spec
177
+ network! Bitcoin.network_name
178
+ end
179
+
180
+ attr_reader :connection
181
+
182
+ def network!(network_name)
183
+ unless @is_spec
184
+ Bitcoin.network = network_name
185
+ @connection = Dropzone::BitcoinConnection.new network_name
186
+ Dropzone::RecordBase.blockchain = @connection
187
+ end
188
+ end
189
+
190
+ def communication_new(args, options)
191
+ network! :testnet3
192
+
193
+ privkey = privkey_from args, :testnet3
194
+
195
+ receiver_addr = args[1] if args.length > 1
196
+
197
+ raise OptionParser::MissingArgument, 'addr' unless (
198
+ receiver_addr && Bitcoin.valid_address?(receiver_addr) )
199
+
200
+ session = Dropzone::Session.new privkey.to_base58,
201
+ secret_for(privkey.addr, receiver_addr),
202
+ receiver_addr: receiver_addr
203
+
204
+ txid = session.authenticate!
205
+
206
+ puts_table '%s: %s' % ['Session', txid], nil, [
207
+ [:sender_addr, privkey.addr], [:receiver_addr, receiver_addr] ]
208
+ end
209
+
210
+ def communication_list(args, options)
211
+ network! :testnet3
212
+
213
+ privkey = privkey_from args, :testnet3
214
+
215
+ Dropzone::Session.all(privkey.addr).each do |session|
216
+ puts_table '%s: %s' % ['Session', session.txid], nil, [
217
+ [:sender_addr, session.sender_addr],
218
+ [:receiver_addr, session.receiver_addr] ]
219
+ end
220
+ end
221
+
222
+ def communication_say(args, options)
223
+ network! :testnet3
224
+
225
+ privkey = privkey_from args, :testnet3
226
+
227
+ txid = args[1] if args.length > 1
228
+ message = args[2] if args.length > 2
229
+
230
+ raise OptionParser::MissingArgument, 'txid' unless txid
231
+ raise OptionParser::MissingArgument, 'message' unless message
232
+
233
+ comm_init = Dropzone::Communication.find txid
234
+
235
+ raise "Invalid Session" unless comm_init && comm_init.is_init?
236
+
237
+ session = session_for privkey, comm_init
238
+
239
+ if !session.authenticated?
240
+ if comm_init.sender_addr == privkey.addr
241
+ raise "The receiver has not yet authenticated your request"
242
+ else
243
+ session.authenticate!
244
+
245
+ # This allows us to re-query the the session communications, and
246
+ # retrive the auth_message we just created.
247
+ # NOTE: We nonetheless fail (sometimes) with a
248
+ # Dropzone::Session::Unauthenticated message as it takes a bit of time to
249
+ # populate the authentication relay into the mempool:
250
+ if Dropzone::BitcoinConnection.cache
251
+ Dropzone::BitcoinConnection.cache.invalidate_listtransactions!
252
+
253
+ puts "Waiting 10 seconds for authorization to propagate to the mempool..."
254
+ sleep 10
255
+ end
256
+ end
257
+ end
258
+
259
+ comm_txid = session << message
260
+
261
+ puts_table '%s: %s' % ['Communication', comm_txid], nil, [
262
+ ['Session', txid],
263
+ [:sender_addr, privkey.addr],
264
+ [:message, message] ]
265
+ end
266
+
267
+ def communication_show(args, options)
268
+ network! :testnet3
269
+
270
+ privkey = privkey_from args, :testnet3
271
+
272
+ txid = args[1] if args.length > 1
273
+
274
+ raise OptionParser::MissingArgument, 'txid' unless txid
275
+
276
+ comm_init = Dropzone::Communication.find txid
277
+
278
+ raise "Invalid Session" unless comm_init && comm_init.is_init?
279
+
280
+ session = session_for privkey, comm_init
281
+
282
+ puts_table '%s: %s' % ['Communication', txid], nil,
283
+ session.communications.collect{|comm|
284
+ [comm.sender_addr, comm.contents_plain] }
285
+ end
286
+
287
+ def listing_find(args, options)
288
+ block_depth = args[0] if args.length > 0
289
+
290
+ raise OptionParser::MissingArgument, "block_depth" unless block_depth
291
+
292
+ block_depth = block_depth.to_i
293
+
294
+ raise OptionParser::InvalidArgument, "block_depth" unless block_depth >= 0
295
+
296
+ location_attrs = [:latitude, :longitude, :radius]
297
+
298
+ params = parameterize options, *location_attrs+[:start_at]
299
+
300
+ via_location = location_attrs.collect{|k| params[k]}
301
+
302
+ raise "missing one or more of: latitude, longitude, or radius." if (
303
+ via_location.any? && !via_location.all? )
304
+
305
+ start_at = params[:start_at].to_i || connection.block_height
306
+
307
+ finder_method = (via_location.all?) ?
308
+ [ :find_in_radius, start_at, block_depth, *via_location] :
309
+ [ :find_creates_since_block, start_at, block_depth]
310
+
311
+ Dropzone::Item.send(*finder_method) do |item|
312
+ puts_object '%s: %s' % ['Listing', item.txid], nil, LISTING_ATTRS, item
313
+ end
314
+ end
315
+
316
+ def send_value(args, options)
317
+ dest_addr = args[1] if args.length > 1
318
+
319
+ raise OptionParser::MissingArgument, "dest_addr" unless dest_addr
320
+
321
+ network! (/\A1/.match dest_addr) ? :bitcoin : :testnet3
322
+
323
+ raise OptionParser::InvalidArgument, "dest_addr" unless Bitcoin.valid_address? dest_addr
324
+
325
+ amnt_btc = args[2] if args.length > 2
326
+
327
+ raise OptionParser::MissingArgument, "amnt_btc" unless amnt_btc
328
+
329
+ amnt_btc = BigDecimal.new amnt_btc
330
+
331
+ raise OptionParser::InvalidArgument, "amnt_btc" unless amnt_btc > 0
332
+
333
+ amnt_satoshis = (amnt_btc * Dropzone::BitcoinConnection::SATOSHIS_IN_BTC).to_i
334
+
335
+ privkey = privkey_from args, Bitcoin.network_name
336
+
337
+ txid = connection.send_value privkey, dest_addr, amnt_satoshis,
338
+ Dropzone::MessageBase.default_tip
339
+
340
+ puts_table '%s: %s' % ['Transaction', txid], nil, [
341
+ ['From', privkey.addr ] ,
342
+ ['To', dest_addr ] ,
343
+ ['Amount (BTC)', amnt_btc.to_s('F').to_s ]
344
+ ]
345
+ end
346
+
347
+ def balance(args, options)
348
+ addr = args.first
349
+
350
+ raise OptionParser::MissingArgument, "addr" unless addr
351
+
352
+ network! (/\A1/.match addr) ? :bitcoin : :testnet3
353
+
354
+ raise OptionParser::InvalidArgument, "addr" unless Bitcoin.valid_address? addr
355
+
356
+ balance = connection.bitcoin.getbalance addr
357
+
358
+ puts_table '%s: %s' % ['Address', addr], nil, [
359
+ ['Balance', balance ] ]
360
+ end
361
+
362
+ private
363
+
364
+ def secret_for(sender_addr, receiver_addr)
365
+ record = local_persistence[:communication_keys].where(
366
+ Sequel.expr(receiver_addr: receiver_addr) &
367
+ Sequel.expr(sender_addr: sender_addr) ).first
368
+
369
+ if record
370
+ record[:secret]
371
+ else
372
+ secret = SecureRandom.random_bytes(128).unpack('H*').first
373
+
374
+ local_persistence[:communication_keys].insert sender_addr: sender_addr,
375
+ receiver_addr: receiver_addr, secret: secret
376
+
377
+ secret
378
+ end
379
+ end
380
+
381
+ def local_persistence
382
+ unless @local_persistence
383
+ config_dir = File.join(Dir.home, ".dropzone")
384
+ Dir.mkdir config_dir, 0700 unless Dir.exists? config_dir
385
+
386
+ @local_persistence = Sequel.connect 'sqlite://%s/dropzone.db' % config_dir
387
+
388
+ @local_persistence.create_table :communication_keys do
389
+ primary_key :id
390
+ String :sender_addr
391
+ String :receiver_addr
392
+ String :secret
393
+ end unless @local_persistence.table_exists? :communication_keys
394
+ end
395
+
396
+ @local_persistence
397
+ end
398
+
399
+ def session_for(privkey, comm_init)
400
+ receiver_addr = (comm_init.receiver_addr == privkey.addr) ?
401
+ comm_init.sender_addr : comm_init.receiver_addr
402
+
403
+ Dropzone::Session.new privkey.to_base58,
404
+ secret_for(privkey.addr, receiver_addr),
405
+ (comm_init.sender_addr == privkey.addr) ?
406
+ {receiver_addr: receiver_addr} : {with: comm_init}
407
+ end
408
+
409
+ def parameterize(options,*valid_params)
410
+ options.__hash__.find_all{|(k,v)| valid_params.include? k}.to_h
411
+ end
412
+
413
+ def privkey_from(args, network = nil)
414
+ start_network = Bitcoin.network_name if network
415
+
416
+ privkey = args.first
417
+
418
+ raise OptionParser::MissingArgument, "private_key" unless privkey
419
+
420
+ begin
421
+ Bitcoin.network = network if network
422
+ Bitcoin::Key.from_base58 privkey
423
+ rescue
424
+ raise OptionParser::InvalidArgument, "private_key"
425
+ ensure
426
+ Bitcoin.network = start_network if start_network
427
+ end
428
+ end
429
+
430
+ def by_txid(args)
431
+ txid = args.first
432
+
433
+ raise OptionParser::MissingArgument, "txid" unless txid
434
+
435
+ txid
436
+ end
437
+
438
+ def by_addr(args)
439
+ addr = args.first
440
+ raise OptionParser::MissingArgument, "addr" unless addr
441
+
442
+ raise OptionParser::InvalidArgument, "addr" unless Bitcoin.valid_address? addr
443
+
444
+ addr
445
+ end
446
+
447
+ def puts_object(header, footer, attributes, object)
448
+ puts_table header, footer, attributes.collect{|attr|
449
+ value = object.send(attr)
450
+ [attr, value] if value
451
+ }.compact
452
+ end
453
+
454
+ def puts_table(header, footer, pairs)
455
+ pairs_h = pairs.to_h
456
+ widest_key = pairs_h.keys.sort_by{|k| k.to_s.length}.last.to_s.length
457
+ widest_value = pairs_h.values.sort_by{|v| v.to_s.length}.last.to_s.length
458
+
459
+ widest_header = (footer.nil? || (header.length > footer.length) ) ?
460
+ header.length : footer.length
461
+
462
+ content_width = ((widest_key+widest_value) > widest_header) ?
463
+ (widest_key+widest_value + 2) : widest_header
464
+
465
+ content_width = MAX_TABLE_WIDTH if content_width > MAX_TABLE_WIDTH
466
+
467
+ endcap = "+%s+" % [ '-'*(content_width+2)]
468
+
469
+ puts [ endcap, "| %-#{content_width}s |" % [header], endcap,
470
+ pairs.collect{|k,v|
471
+ if (k.to_s.length+v.to_s.length+2) > content_width
472
+ data_width = content_width-widest_key-3
473
+ sentence_splitter = /(.{1,#{data_width}})( +|$\n?)|(.{1,#{data_width}})/m
474
+ v.scan(sentence_splitter).each_with_index.collect{|l,i|
475
+ "| %-#{content_width}s |" % [
476
+ ("%-#{widest_key}s: %s" % [(i == 0) ? k : '',l.first]) ]
477
+ }
478
+ else
479
+ "| %-#{content_width}s |" % ["%-#{widest_key}s: %s" % [k,v]]
480
+ end
481
+ },
482
+ endcap,
483
+ (footer) ? ["| %-#{content_width}s |" % [footer], endcap] : nil
484
+ ].flatten.join("\n")
485
+ end
486
+
487
+ end
488
+