1# frozen_string_literal: true
2
3require "braintree"
4require "date"
5require "delegate"
6require "dhall"
7require "forwardable"
8require "pg"
9require "redis"
10require "roda"
11require "uri"
12
13if ENV["RACK_ENV"] == "development"
14 require "pry-rescue"
15 use PryRescue::Rack
16end
17
18require_relative "lib/auto_top_up_repo"
19require_relative "lib/electrum"
20
21require "sentry-ruby"
22Sentry.init do |config|
23 config.traces_sample_rate = 1
24end
25use Sentry::Rack::CaptureExceptions
26
27REDIS = Redis.new
28BRAINTREE_CONFIG = Dhall.load("env:BRAINTREE_CONFIG").sync
29ELECTRUM = Electrum.new(
30 **Dhall::Coder.load("env:ELECTRUM_CONFIG", transform_keys: :to_sym)
31)
32
33DB = PG.connect(dbname: "jmp")
34DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
35DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
36
37class CreditCardGateway
38 def initialize(jid, customer_id=nil)
39 @jid = jid
40 @customer_id = customer_id
41
42 @gateway = Braintree::Gateway.new(
43 environment: BRAINTREE_CONFIG[:environment].to_s,
44 merchant_id: BRAINTREE_CONFIG[:merchant_id].to_s,
45 public_key: BRAINTREE_CONFIG[:public_key].to_s,
46 private_key: BRAINTREE_CONFIG[:private_key].to_s
47 )
48 end
49
50 def check_customer_id(cid)
51 return cid unless ENV["RACK_ENV"] == "production"
52
53 raise "customer_id does not match" unless @customer_id == cid
54
55 cid
56 end
57
58 def customer_id
59 customer_id = REDIS.get(redis_key_jid)
60 return customer_id if check_customer_id(customer_id)
61
62 result = @gateway.customer.create
63 raise "Braintree customer create failed" unless result.success?
64
65 @customer_id = result.customer.id
66 save_customer_id!
67 end
68
69 def save_customer_id!
70 unless REDIS.set(redis_key_jid, @customer_id) == "OK"
71 raise "Saving new jid,customer to redis failed"
72 end
73
74 unless REDIS.set(redis_key_customer_id, @jid) == "OK"
75 raise "Saving new customer,jid to redis failed"
76 end
77
78 @customer_id
79 end
80
81 def client_token
82 @gateway.client_token.generate(customer_id: customer_id)
83 end
84
85 def payment_methods?
86 !@gateway.customer.find(customer_id).payment_methods.empty?
87 end
88
89 def default_payment_method=(nonce)
90 @gateway.payment_method.create(
91 customer_id: customer_id,
92 payment_method_nonce: nonce,
93 options: {
94 make_default: true
95 }
96 )
97 end
98
99protected
100
101 def redis_key_jid
102 "jmp_customer_id-#{@jid}"
103 end
104
105 def redis_key_customer_id
106 "jmp_customer_jid-#{@customer_id}"
107 end
108end
109
110class UnknownTransactions
111 def self.from(customer_id, address, tx_hashes)
112 self.for(
113 customer_id,
114 fetch_rows_for(address, tx_hashes).map { |row|
115 row["transaction_id"]
116 }
117 )
118 end
119
120 def self.fetch_rows_for(address, tx_hashes)
121 values = tx_hashes.map { |tx_hash|
122 "('#{DB.escape_string(tx_hash)}/#{DB.escape_string(address)}')"
123 }
124 return [] if values.empty?
125
126 DB.exec_params(<<-SQL)
127 SELECT transaction_id FROM
128 (VALUES #{values.join(',')}) AS t(transaction_id)
129 LEFT JOIN transactions USING (transaction_id)
130 WHERE transactions.transaction_id IS NULL
131 SQL
132 end
133
134 def self.for(customer_id, transaction_ids)
135 transaction_ids.empty? ? None.new : new(customer_id, transaction_ids)
136 end
137
138 def initialize(customer_id, transaction_ids)
139 @customer_id = customer_id
140 @transaction_ids = transaction_ids
141 end
142
143 def enqueue!
144 REDIS.hset(
145 "pending_btc_transactions",
146 *@transaction_ids.flat_map { |txid| [txid, @customer_id] }
147 )
148 end
149
150 class None
151 def enqueue!; end
152 end
153end
154
155class JmpPay < Roda
156 SENTRY_DSN = ENV["SENTRY_DSN"] && URI(ENV["SENTRY_DSN"])
157 plugin :render, engine: "slim"
158 plugin :common_logger, $stdout
159
160 extend Forwardable
161 def_delegators :request, :params
162
163 def redis_key_btc_addresses
164 "jmp_customer_btc_addresses-#{params['customer_id']}"
165 end
166
167 def verify_address_customer_id(r)
168 return if REDIS.sismember(redis_key_btc_addresses, params["address"])
169
170 warn "Address and customer_id do not match"
171 r.halt([
172 403,
173 { "Content-Type" => "text/plain" },
174 "Address and customer_id do not match"
175 ])
176 end
177
178 route do |r|
179 r.on "electrum_notify" do
180 verify_address_customer_id(r)
181
182 UnknownTransactions.from(
183 params["customer_id"],
184 params["address"],
185 ELECTRUM
186 .getaddresshistory(params["address"])
187 .map { |item| item["tx_hash"] }
188 ).enqueue!
189
190 "OK"
191 end
192
193 r.on :jid do |jid|
194 Sentry.set_user(id: params["customer_id"], jid: jid)
195
196 gateway = CreditCardGateway.new(jid, params["customer_id"])
197 topup = AutoTopUpRepo.new
198
199 r.on "credit_cards" do
200 r.get do
201 view(
202 "credit_cards",
203 locals: {
204 token: gateway.client_token,
205 customer_id: gateway.customer_id,
206 auto_top_up: topup.find(gateway.customer_id) ||
207 (gateway.payment_methods? ? "" : "15")
208 }
209 )
210 end
211
212 r.post do
213 gateway.default_payment_method = params["braintree_nonce"]
214 topup.put(
215 gateway.customer_id,
216 params["auto_top_up_amount"].to_i
217 )
218 "OK"
219 end
220 end
221 end
222 end
223end
224
225run JmpPay.freeze.app