config.ru

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