dropzone_ruby 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|