dropzone_ruby 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Drop Zone - An Anonymous Peer-To-Peer Local Contraband Marketplace.pdf +0 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +69 -0
- data/README.md +62 -0
- data/bin/dropzone +487 -0
- data/dropzone-screenshot.jpg +0 -0
- data/dropzone_ruby.gemspec +31 -0
- data/lib/blockrio_ext.rb +52 -0
- data/lib/dropzone/buyer.rb +21 -0
- data/lib/dropzone/command.rb +488 -0
- data/lib/dropzone/communication.rb +43 -0
- data/lib/dropzone/connection.rb +312 -0
- data/lib/dropzone/invoice.rb +23 -0
- data/lib/dropzone/item.rb +160 -0
- data/lib/dropzone/listing.rb +64 -0
- data/lib/dropzone/message_base.rb +178 -0
- data/lib/dropzone/payment.rb +36 -0
- data/lib/dropzone/profile.rb +86 -0
- data/lib/dropzone/record_base.rb +34 -0
- data/lib/dropzone/seller.rb +21 -0
- data/lib/dropzone/session.rb +161 -0
- data/lib/dropzone/state_accumulator.rb +39 -0
- data/lib/dropzone/version.rb +4 -0
- data/lib/dropzone_ruby.rb +14 -0
- data/lib/veto_checks.rb +74 -0
- data/spec/bitcoin_spec.rb +115 -0
- data/spec/buyer_profile_spec.rb +279 -0
- data/spec/buyer_spec.rb +109 -0
- data/spec/command_spec.rb +353 -0
- data/spec/config.yml +5 -0
- data/spec/invoice_spec.rb +129 -0
- data/spec/item_spec.rb +294 -0
- data/spec/lib/fake_connection.rb +97 -0
- data/spec/listing_spec.rb +150 -0
- data/spec/payment_spec.rb +152 -0
- data/spec/seller_profile_spec.rb +290 -0
- data/spec/seller_spec.rb +120 -0
- data/spec/session_spec.rb +303 -0
- data/spec/sham/buyer.rb +5 -0
- data/spec/sham/invoice.rb +5 -0
- data/spec/sham/item.rb +8 -0
- data/spec/sham/payment.rb +13 -0
- data/spec/sham/seller.rb +7 -0
- data/spec/spec_helper.rb +49 -0
- 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
|
+
|
data/lib/blockrio_ext.rb
ADDED
@@ -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
|
+
|