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