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
@@ -0,0 +1,43 @@
|
|
1
|
+
module Dropzone
|
2
|
+
class Communication < MessageBase
|
3
|
+
class NoSymmKey < StandardError; end
|
4
|
+
|
5
|
+
attr_message i: :iv, c: :contents, d: :der, p: :session_pkey
|
6
|
+
|
7
|
+
message_type 'COMMUN'
|
8
|
+
|
9
|
+
attr_accessor :symm_key
|
10
|
+
|
11
|
+
def contents_plain
|
12
|
+
raise NoSymmKey unless symm_key
|
13
|
+
|
14
|
+
aes = OpenSSL::Cipher::Cipher.new Session::CIPHER_ALGORITHM
|
15
|
+
aes.decrypt
|
16
|
+
aes.key = symm_key
|
17
|
+
aes.iv = iv
|
18
|
+
|
19
|
+
aes.update(contents) + aes.final
|
20
|
+
end
|
21
|
+
|
22
|
+
def is_init?; (der && session_pkey); end
|
23
|
+
def is_auth?; !session_pkey.nil?; end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Communication::Validator < ValidatorBase
|
27
|
+
validates :message_type, format: /\ACOMMUN\Z/
|
28
|
+
|
29
|
+
validates_if_present :der, is_string: true
|
30
|
+
validates_if_present :session_pkey, is_string: true
|
31
|
+
validates_if_present :contents, is_string: true
|
32
|
+
validates_if_present :iv, is_string: true
|
33
|
+
|
34
|
+
# Ders always need session_pkey:
|
35
|
+
validates :session_pkey, not_null: true, unless: 'self.der.nil?'
|
36
|
+
|
37
|
+
# Content always needs an iv:
|
38
|
+
validates :iv, not_null: true, unless: 'self.contents.nil?'
|
39
|
+
|
40
|
+
# We should always have either contents or a pkey:
|
41
|
+
validates :contents, not_null: true, if: 'self.session_pkey.nil?'
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
module Dropzone
|
2
|
+
class BitcoinConnection
|
3
|
+
class WalletFundsTooLowOrNoUTXOs < StandardError; end
|
4
|
+
|
5
|
+
SATOSHIS_IN_BTC = 100_000_000
|
6
|
+
|
7
|
+
TXO_DUST = 5430 # 0.0000543 BTC
|
8
|
+
|
9
|
+
PREFIX = 'DZ'
|
10
|
+
|
11
|
+
attr_reader :bitcoin
|
12
|
+
|
13
|
+
def initialize(network, options = {})
|
14
|
+
@network = network
|
15
|
+
@bitcoin = options[:bitcoin] if options.has_key? :bitcoin
|
16
|
+
@is_testing = (/\Atestnet/.match network.to_s) ? true : false
|
17
|
+
@bitcoin ||= BlockrIo.new is_testing?
|
18
|
+
end
|
19
|
+
|
20
|
+
def is_testing?; @is_testing; end
|
21
|
+
|
22
|
+
def privkey_to_addr(key)
|
23
|
+
set_network_mode!
|
24
|
+
Bitcoin::Key.from_base58(key).addr
|
25
|
+
end
|
26
|
+
|
27
|
+
def hash160_to_address(hash160)
|
28
|
+
set_network_mode!
|
29
|
+
Bitcoin.hash160_to_address hash160
|
30
|
+
end
|
31
|
+
|
32
|
+
def hash160_from_address(addr)
|
33
|
+
set_network_mode!
|
34
|
+
Bitcoin.hash160_from_address addr
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_address?(addr)
|
38
|
+
set_network_mode!
|
39
|
+
Bitcoin.valid_address? addr
|
40
|
+
end
|
41
|
+
|
42
|
+
# NOTE:
|
43
|
+
# - This needs to return the messages in Descending order by block
|
44
|
+
# In the case that two transactions are in the same block, it goes by time
|
45
|
+
# - This should only 'valid' return messages. Not transactions
|
46
|
+
def messages_by_addr(addr, options = {})
|
47
|
+
ret = cache.listtransactions addr, true, is_testing? if cache
|
48
|
+
|
49
|
+
unless ret
|
50
|
+
ret = bitcoin.listtransactions addr, true
|
51
|
+
cache.listtransactions! addr, true, ret, is_testing? if cache
|
52
|
+
end
|
53
|
+
|
54
|
+
ret = ret.collect{ |tx_h|
|
55
|
+
begin
|
56
|
+
msg = Dropzone::MessageBase.new_message_from tx_by_id(tx_h['tx'])
|
57
|
+
|
58
|
+
(msg && msg.valid?) ? msg : nil
|
59
|
+
rescue Counterparty::TxDecode::InvalidOutput,
|
60
|
+
Counterparty::TxDecode::MultisigUnsupported
|
61
|
+
next
|
62
|
+
end
|
63
|
+
}.compact
|
64
|
+
|
65
|
+
filter_messages ret, options
|
66
|
+
end
|
67
|
+
|
68
|
+
def messages_in_block(at_height, options = {})
|
69
|
+
ret = bitcoin.getblock(at_height).collect{ |tx_h|
|
70
|
+
|
71
|
+
# This is a speed hack that drastically reduces query times:
|
72
|
+
next if options[:type] == 'ITCRTE' && !tx_h_create?(tx_h)
|
73
|
+
|
74
|
+
begin
|
75
|
+
msg = Dropzone::MessageBase.new_message_from tx_by_id( tx_h['tx'],
|
76
|
+
block_height: at_height )
|
77
|
+
|
78
|
+
(msg && msg.valid?) ? msg : nil
|
79
|
+
rescue Counterparty::TxDecode::InvalidOutput,
|
80
|
+
Counterparty::TxDecode::MultisigUnsupported,
|
81
|
+
Counterparty::TxDecode::UndefinedBehavior,
|
82
|
+
Counterparty::TxDecode::InvalidOpReturn
|
83
|
+
next
|
84
|
+
end
|
85
|
+
}.compact
|
86
|
+
|
87
|
+
filter_messages ret, options
|
88
|
+
end
|
89
|
+
|
90
|
+
def send_value(from_key, to_addr, send_satoshis, tip_satoshis)
|
91
|
+
set_network_mode!
|
92
|
+
|
93
|
+
new_tx = create_tx(from_key, send_satoshis+tip_satoshis){ |tx, allocated|
|
94
|
+
[ [send_satoshis, Bitcoin.hash160_from_address(to_addr)],
|
95
|
+
[(allocated-send_satoshis-tip_satoshis), from_key.hash160]
|
96
|
+
].each do |(amt, to)|
|
97
|
+
tx.add_out Bitcoin::P::TxOut.new( amt,
|
98
|
+
Bitcoin::Script.to_hash160_script(to) )
|
99
|
+
end
|
100
|
+
}
|
101
|
+
|
102
|
+
sign_and_send new_tx, from_key
|
103
|
+
end
|
104
|
+
|
105
|
+
def sign_tx(tx, key)
|
106
|
+
set_network_mode!
|
107
|
+
|
108
|
+
# Sign the transaction:
|
109
|
+
tx.inputs.length.times do |i|
|
110
|
+
# Fetch the previous input:
|
111
|
+
prev_out_tx_hash = tx.inputs[i].prev_out.reverse.unpack('H*').first
|
112
|
+
prev_tx_raw = bitcoin.getrawtransaction(prev_out_tx_hash)['hex']
|
113
|
+
prev_tx = Bitcoin::P::Tx.new [prev_tx_raw].pack('H*')
|
114
|
+
|
115
|
+
# Now we actually sign
|
116
|
+
sig = Bitcoin.sign_data Bitcoin.open_key(key.priv),
|
117
|
+
tx.signature_hash_for_input(i, prev_tx)
|
118
|
+
tx.in[i].script_sig = Bitcoin::Script.to_signature_pubkey_script( sig,
|
119
|
+
[key.pub].pack("H*"))
|
120
|
+
end
|
121
|
+
tx
|
122
|
+
end
|
123
|
+
|
124
|
+
def tx_by_id(id, options = {})
|
125
|
+
set_network_mode!
|
126
|
+
|
127
|
+
ret = cache.tx_by_id id, is_testing? if cache
|
128
|
+
|
129
|
+
unless ret
|
130
|
+
tx_h = bitcoin.getrawtransaction(id)
|
131
|
+
tx = Bitcoin::P::Tx.new [tx_h['hex']].pack('H*')
|
132
|
+
|
133
|
+
if tx_h.has_key? 'blockhash'
|
134
|
+
options[:block_height] = bitcoin.getblockinfo(tx_h['blockhash'])['nb']
|
135
|
+
end
|
136
|
+
|
137
|
+
record = Counterparty::TxDecode.new tx,
|
138
|
+
prefix: Dropzone::BitcoinConnection::PREFIX
|
139
|
+
|
140
|
+
ret = options.merge({ data: record.data,
|
141
|
+
receiver_addr: record.receiver_addr,
|
142
|
+
txid: id,
|
143
|
+
sender_addr: record.sender_addr})
|
144
|
+
|
145
|
+
# NOTE that in the case of a reorg, this might have incorrect block
|
146
|
+
# heights cached. It's probable that we can/should cache these, and
|
147
|
+
# merely set the block height when it's confirmed, and/or set the
|
148
|
+
# height to current_height+1
|
149
|
+
cache.tx_by_id! id, ret, is_testing? if cache && options[:block_height]
|
150
|
+
end
|
151
|
+
|
152
|
+
ret
|
153
|
+
end
|
154
|
+
|
155
|
+
def save!(data, private_key_wif)
|
156
|
+
set_network_mode!
|
157
|
+
|
158
|
+
from_key = Bitcoin::Key.from_base58 private_key_wif
|
159
|
+
|
160
|
+
# We need to know how many transactions we'll have in order to know how
|
161
|
+
# many satoshi's to allocate. We start with 1, since that's the return
|
162
|
+
# address of the allocated input satoshis
|
163
|
+
data_outputs_needed = 1
|
164
|
+
|
165
|
+
bytes_in_output = Counterparty::TxEncode::BYTES_IN_MULTISIG
|
166
|
+
|
167
|
+
# 3 is for the two-byte DZ prefix, and the 1-byte length
|
168
|
+
data_outputs_needed += ((data[:data].length+3) / bytes_in_output).ceil
|
169
|
+
|
170
|
+
# We'll need a P2PSH for the destination if that applies
|
171
|
+
data_outputs_needed += 1 if data.has_key? :receiver_addr
|
172
|
+
tip = data[:tip] || 0
|
173
|
+
|
174
|
+
new_tx = create_tx(from_key,data_outputs_needed * TXO_DUST+tip) do |tx, amount_allocated|
|
175
|
+
outputs = Counterparty::TxEncode.new(
|
176
|
+
[tx.inputs[0].prev_out.reverse_hth].pack('H*'),
|
177
|
+
data[:data], receiver_addr: data[:receiver_addr],
|
178
|
+
sender_pubkey: from_key.pub, prefix: PREFIX).to_opmultisig
|
179
|
+
|
180
|
+
outputs.each_with_index do |output,i|
|
181
|
+
tx.add_out Bitcoin::P::TxOut.new( (i == (outputs.length-1)) ?
|
182
|
+
(amount_allocated - tip - TXO_DUST*(outputs.length - 1)) : TXO_DUST,
|
183
|
+
Bitcoin::Script.binary_from_string(output) )
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
sign_and_send new_tx, from_key
|
188
|
+
end
|
189
|
+
|
190
|
+
def block_height
|
191
|
+
bitcoin.getblockinfo('last')['nb']
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def filter_messages(messages, options = {})
|
197
|
+
messages = messages.find_all{|msg|
|
198
|
+
msg.block_height.nil? || (msg.block_height >= options[:start_block])
|
199
|
+
} if options[:start_block]
|
200
|
+
|
201
|
+
messages = messages.find_all{|msg|
|
202
|
+
msg.block_height.nil? || (msg.block_height <= options[:end_block])
|
203
|
+
} if options[:end_block]
|
204
|
+
|
205
|
+
if messages && options.has_key?(:type)
|
206
|
+
messages = messages.find_all{|msg| msg.message_type == options[:type]}
|
207
|
+
end
|
208
|
+
|
209
|
+
if options.has_key?(:between)
|
210
|
+
messages = messages.find_all{|c|
|
211
|
+
[c.receiver_addr, c.sender_addr].all?{|a| options[:between].include?(a) } }
|
212
|
+
end
|
213
|
+
|
214
|
+
(messages) ? messages : []
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# This is a speed hack which keeps us from traversing through entire blocks
|
219
|
+
# by filtering based on the destination addresses
|
220
|
+
def tx_h_create?(tx_h)
|
221
|
+
address = tx_h['out'][0]['addr'] if [
|
222
|
+
tx_h.has_key?('out'), tx_h['out'][0], tx_h['out'][0]['addr'] ].all?
|
223
|
+
|
224
|
+
(address && Dropzone::Item::HASH_160_PARTS.match(address)) ? true : false
|
225
|
+
end
|
226
|
+
|
227
|
+
# Since the Bitcoin object is a singleton, and we'll be working alongside
|
228
|
+
# testnet, we need to start our methods by setting the correct network mode
|
229
|
+
def set_network_mode!
|
230
|
+
Bitcoin.network = @network
|
231
|
+
end
|
232
|
+
|
233
|
+
def sign_and_send(tx, key)
|
234
|
+
signed_hex = sign_tx(tx, key).to_payload.unpack('H*')[0]
|
235
|
+
|
236
|
+
bitcoin.sendrawtransaction signed_hex
|
237
|
+
end
|
238
|
+
|
239
|
+
def create_tx(key, allocate, &block)
|
240
|
+
# create a new transaction (and sign the inputs)
|
241
|
+
tx = Bitcoin::P::Tx.new(nil)
|
242
|
+
tx.ver, tx.lock_time = 1, 0
|
243
|
+
|
244
|
+
# allocate some inputs here:
|
245
|
+
amount_allocated = 0
|
246
|
+
allocate_inputs_for(key.addr, allocate).each do |unspent|
|
247
|
+
amount_allocated += to_satoshis(unspent['amount'])
|
248
|
+
tx.add_in Bitcoin::P::TxIn.new [unspent['tx'] ].pack('H*').reverse,
|
249
|
+
unspent['n'].to_i
|
250
|
+
end
|
251
|
+
|
252
|
+
block.call(tx, amount_allocated)
|
253
|
+
|
254
|
+
tx
|
255
|
+
end
|
256
|
+
|
257
|
+
# We expect the amount to be in satoshis. As-is this method does not allocate
|
258
|
+
# mempool utxos for a spend.
|
259
|
+
def allocate_inputs_for(addr, amount)
|
260
|
+
allocated = 0
|
261
|
+
|
262
|
+
# NOTE: I think we're issuing more queries here than we should be due to a
|
263
|
+
# fetch for every utxo, instead of one fetch for every transaction.
|
264
|
+
# (Which may have many utxo's per transaction.)
|
265
|
+
mempooled_utxos = bitcoin.listunconfirmed(addr).collect{|tx|
|
266
|
+
# Note that some api's may not retrieve the contents of an unconfirmed
|
267
|
+
# raw transaction.
|
268
|
+
unconfirmed_tx = bitcoin.getrawtransaction(tx['tx'])['hex']
|
269
|
+
|
270
|
+
if unconfirmed_tx
|
271
|
+
tx = Bitcoin::P::Tx.new [unconfirmed_tx].pack('H*')
|
272
|
+
tx.inputs.collect{|input| input.prev_out.reverse.unpack('H*').first}
|
273
|
+
else
|
274
|
+
nil
|
275
|
+
end
|
276
|
+
}.compact.flatten.uniq
|
277
|
+
|
278
|
+
utxos = []
|
279
|
+
bitcoin.listunspent(addr).sort_by{|utxo| utxo['confirmations']}.each{|utxo|
|
280
|
+
next if mempooled_utxos.include? utxo['tx']
|
281
|
+
utxos << utxo
|
282
|
+
allocated += to_satoshis(utxo['amount'])
|
283
|
+
break if allocated >= amount
|
284
|
+
}
|
285
|
+
|
286
|
+
raise WalletFundsTooLowOrNoUTXOs if allocated < amount
|
287
|
+
|
288
|
+
utxos
|
289
|
+
end
|
290
|
+
|
291
|
+
def to_satoshis(string)
|
292
|
+
self.class.to_satoshis string
|
293
|
+
end
|
294
|
+
|
295
|
+
def is_opreturn?(output)
|
296
|
+
/\AOP_RETURN/.match output
|
297
|
+
end
|
298
|
+
|
299
|
+
def self.to_satoshis(string)
|
300
|
+
(BigDecimal.new(string) * SATOSHIS_IN_BTC).to_i
|
301
|
+
end
|
302
|
+
|
303
|
+
def cache
|
304
|
+
self.class.cache
|
305
|
+
end
|
306
|
+
|
307
|
+
class << self
|
308
|
+
# This is a hook to speed up some operations via a local cache
|
309
|
+
attr_accessor :cache
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Dropzone
|
2
|
+
class Invoice < MessageBase
|
3
|
+
attr_message_int p: :amount_due, e: :expiration_in
|
4
|
+
|
5
|
+
message_type 'INCRTE'
|
6
|
+
|
7
|
+
def payments
|
8
|
+
blockchain.messages_by_addr(sender_addr, type: 'INPAID',
|
9
|
+
start_block: block_height).find_all{|p| p.invoice_txid == txid }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Invoice::Validator < ValidatorBase
|
14
|
+
include MessageValidations
|
15
|
+
include BillingValidations
|
16
|
+
|
17
|
+
validates :message_type, format: /\AINCRTE\Z/
|
18
|
+
|
19
|
+
[:amount_due, :expiration_in].each do |attr|
|
20
|
+
validates_if_present attr, integer: true, greater_than_or_equal_to: 0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Dropzone
|
2
|
+
class Item < MessageBase
|
3
|
+
EARTH_RADIUS_IN_METERS = 6_371_000
|
4
|
+
|
5
|
+
HASH_160_PARTS = /\A(?:mfZ|1DZ)([1-9X]{9})([1-9X]{9})([1-9X]{6}).+/
|
6
|
+
|
7
|
+
attr_message d: :description, c: :price_currency, t: :create_txid
|
8
|
+
attr_message_int p: :price_in_units, e: :expiration_in
|
9
|
+
|
10
|
+
# These are receiver address attributes, not message attribs:
|
11
|
+
attr_reader :latitude, :longitude, :radius
|
12
|
+
|
13
|
+
@types_include = ['ITUPDT', 'ITCRTE']
|
14
|
+
|
15
|
+
def latitude
|
16
|
+
(@receiver_addr) ?
|
17
|
+
integer_to_latlon( address_parts(@receiver_addr, 0) ) :
|
18
|
+
@latitude
|
19
|
+
end
|
20
|
+
|
21
|
+
def longitude
|
22
|
+
(@receiver_addr) ?
|
23
|
+
integer_to_latlon( address_parts(@receiver_addr, 1), 180 ) :
|
24
|
+
@longitude
|
25
|
+
end
|
26
|
+
|
27
|
+
def radius
|
28
|
+
(@receiver_addr) ?
|
29
|
+
address_parts(@receiver_addr, 2) :
|
30
|
+
@radius
|
31
|
+
end
|
32
|
+
|
33
|
+
def message_type
|
34
|
+
return @message_type if @message_type
|
35
|
+
|
36
|
+
create_txid ? 'ITUPDT' : 'ITCRTE'
|
37
|
+
end
|
38
|
+
|
39
|
+
# This is an easy guide to what we're doing here:
|
40
|
+
# http://www.reddit.com/r/Bitcoin/comments/2ss3en/calculating_checksum_for_bitcoin_address/
|
41
|
+
#
|
42
|
+
# NOTE: There was a digit off in the reference spec, this radius is a seven
|
43
|
+
# digit number, not an eight digit number.
|
44
|
+
def receiver_addr
|
45
|
+
case
|
46
|
+
when @receiver_addr then @receiver_addr
|
47
|
+
when create_txid then @sender_addr
|
48
|
+
when latitude && longitude && radius
|
49
|
+
receiver_addr_base = ('%s%09d%09d%06d' % [
|
50
|
+
(blockchain.is_testing?) ? 'mfZ' : ('1' + BitcoinConnection::PREFIX),
|
51
|
+
latlon_to_integer(latitude.to_f),
|
52
|
+
latlon_to_integer(longitude.to_f, 180),
|
53
|
+
radius.abs ]).tr('0','X')
|
54
|
+
|
55
|
+
# The x's pad the checksum component for us to ensure the base conversion
|
56
|
+
# produces the correct output. Similarly, we ignore them after the decode:
|
57
|
+
hex_address = Bitcoin.decode_base58(receiver_addr_base+'XXXXXXX')[0...42]
|
58
|
+
|
59
|
+
hash160 = [hex_address].pack('H*')
|
60
|
+
|
61
|
+
# Bitcoin-ruby has a method to do much of this for us, but it is
|
62
|
+
# broken in that it only supports main-net addresses, and not testnet3
|
63
|
+
checksum = Digest::SHA256.digest(Digest::SHA256.digest(hash160))[0,4]
|
64
|
+
|
65
|
+
# Return the checksum'd receiver_addr
|
66
|
+
Bitcoin.encode_base58((hash160 + checksum).unpack('H*').first)
|
67
|
+
else
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.blockchain
|
73
|
+
Dropzone::RecordBase.blockchain
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns all *Items created* since (and including) the provided block
|
77
|
+
# These are items and not listings, so as to query faster.
|
78
|
+
# Items are returned in the order of newest to oldest
|
79
|
+
def self.find_creates_since_block(starting_at, block_depth, &block)
|
80
|
+
starting_at.downto(starting_at-block_depth).collect{|i|
|
81
|
+
blockchain.messages_in_block(i, type: 'ITCRTE').collect do |item|
|
82
|
+
(block_given?) ? block.call(item, i) : item
|
83
|
+
end
|
84
|
+
}.flatten.compact
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.find_in_radius(starting_at, block_depth, lat, long, in_meters, &block)
|
88
|
+
find_creates_since_block(starting_at, block_depth) do |item, nb|
|
89
|
+
if distance_between(item.latitude, item.longitude, lat, long) <= in_meters
|
90
|
+
(block_given?) ? block.call(item, nb) : item
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# haversine formula, pulled from :
|
96
|
+
# http://www.movable-type.co.uk/scripts/latlong.html
|
97
|
+
def self.distance_between(lat1, lon1, lat2, lon2)
|
98
|
+
delta_phi = to_rad(lat2-lat1)
|
99
|
+
delta_lamba = to_rad(lon2-lon1)
|
100
|
+
|
101
|
+
a = Math.sin(delta_phi/2) ** 2 + [ Math.cos(to_rad(lat1)),
|
102
|
+
Math.cos(to_rad(lat2)), Math.sin(delta_lamba/2),
|
103
|
+
Math.sin(delta_lamba/2) ].reduce(:*)
|
104
|
+
|
105
|
+
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
|
106
|
+
|
107
|
+
EARTH_RADIUS_IN_METERS * c
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.to_rad(angle)
|
111
|
+
angle.to_f / 180 * Math::PI
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def address_parts(addr, part)
|
117
|
+
parts = HASH_160_PARTS.match(addr)
|
118
|
+
(parts.length > 0) ? parts[part+1].tr('X','0').to_i : nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def latlon_to_integer(lat_or_lon, unsigned_offset = 90)
|
122
|
+
((lat_or_lon + unsigned_offset) * 1_000_000).floor.abs unless lat_or_lon.nil?
|
123
|
+
end
|
124
|
+
|
125
|
+
def integer_to_latlon(lat_or_lon, unsigned_offset = 90)
|
126
|
+
(BigDecimal.new(lat_or_lon) / 1_000_000 - unsigned_offset).to_f unless lat_or_lon.nil?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Item::Validator < ValidatorBase
|
131
|
+
include MessageValidations
|
132
|
+
|
133
|
+
validates :receiver_addr, equals_attribute: { attribute: :sender_addr },
|
134
|
+
if: "self.sender_addr && self.create_txid"
|
135
|
+
|
136
|
+
validates :latitude, numeric: true, unless: 'create_txid'
|
137
|
+
validates_if_present :latitude, greater_than_or_equal_to: -90, if: 'create_txid.nil?'
|
138
|
+
validates_if_present :latitude, less_than_or_equal_to: 90, if: 'create_txid.nil?'
|
139
|
+
|
140
|
+
validates :longitude, numeric: true, unless: 'create_txid'
|
141
|
+
validates_if_present :longitude, greater_than_or_equal_to: -180, if: 'create_txid.nil?'
|
142
|
+
validates_if_present :longitude, less_than_or_equal_to: 180, if: 'create_txid.nil?'
|
143
|
+
|
144
|
+
validates :radius, integer: true, unless: 'create_txid'
|
145
|
+
validates_if_present :radius, greater_than_or_equal_to: 0, if: 'create_txid.nil?'
|
146
|
+
validates_if_present :radius, less_than: 1000000, if: 'create_txid.nil?'
|
147
|
+
|
148
|
+
validates :message_type, format: /\AIT(?:CRTE|UPDT)\Z/
|
149
|
+
|
150
|
+
validates :price_currency, is_string: {
|
151
|
+
message: 'is required if price is specified' },
|
152
|
+
unless: "price_in_units.nil? || create_txid"
|
153
|
+
|
154
|
+
validates_if_present :description, is_string: true
|
155
|
+
validates_if_present :price_in_units, integer: true,
|
156
|
+
greater_than_or_equal_to: 0
|
157
|
+
validates_if_present :expiration_in, integer: true,
|
158
|
+
greater_than_or_equal_to: 0
|
159
|
+
end
|
160
|
+
end
|