config.ru

  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