# frozen_string_literal: true
require "braintree"
require "delegate"
require "dhall"
require "pg"
require "redis"
require "roda"
require_relative "lib/electrum"
REDIS = Redis.new
BRAINTREE_CONFIG = Dhall.load("env:BRAINTREE_CONFIG").sync
ELECTRUM = Electrum.new(
**Dhall::Coder.load("env:ELECTRUM_CONFIG", transform_keys: :to_sym)
)
DB = PG.connect(dbname: "jmp")
DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
class CreditCardGateway
def initialize(jid, customer_id=nil)
@jid = jid
@customer_id = customer_id
@gateway = Braintree::Gateway.new(
environment: BRAINTREE_CONFIG[:environment].to_s,
merchant_id: BRAINTREE_CONFIG[:merchant_id].to_s,
public_key: BRAINTREE_CONFIG[:public_key].to_s,
private_key: BRAINTREE_CONFIG[:private_key].to_s
)
end
def check_customer_id(cid)
return cid unless ENV["RACK_ENV"] == "production"
raise "customer_id does not match" unless @customer_id == cid
cid
end
def customer_id
customer_id = REDIS.get(redis_key_jid)
return customer_id if check_customer_id(customer_id)
result = @gateway.customer.create
raise "Braintree customer create failed" unless result.success?
@customer_id = result.customer.id
save_customer_id!
end
def save_customer_id!
unless REDIS.set(redis_key_jid, @customer_id) == "OK"
raise "Saving new jid,customer to redis failed"
end
unless REDIS.set(redis_key_customer_id, @jid) == "OK"
raise "Saving new customer,jid to redis failed"
end
@customer_id
end
def client_token
@gateway.client_token.generate(customer_id: customer_id)
end
def default_payment_method=(nonce)
@gateway.payment_method.create(
customer_id: customer_id,
payment_method_nonce: nonce,
options: {
make_default: true
}
)
end
protected
def redis_key_jid
"jmp_customer_id-#{@jid}"
end
def redis_key_customer_id
"jmp_customer_jid-#{@customer_id}"
end
end
class UnknownTransactions
def self.from(customer_id, address, tx_hashes)
values = tx_hashes.map do |tx_hash|
"('#{DB.escape_string(tx_hash)}/#{DB.escape_string(address)}')"
end
rows = DB.exec_params(<<-SQL)
SELECT transaction_id FROM
(VALUES #{values.join(',')}) AS t(transaction_id)
LEFT JOIN transactions USING (transaction_id)
WHERE transactions.transaction_id IS NULL
SQL
new(customer_id, rows.map { |row| row["transaction_id"] })
end
def initialize(customer_id, transaction_ids)
@customer_id = customer_id
@transaction_ids = transaction_ids
end
def enqueue!
REDIS.hset(
"pending_btc_transactions",
*@transaction_ids.flat_map { |txid| [txid, @customer_id] }
)
end
end
class JmpPay < Roda
plugin :render, engine: "slim"
plugin :common_logger, $stdout
def redis_key_btc_addresses
"jmp_customer_btc_addresses-#{request.params['customer_id']}"
end
def verify_address_customer_id(r)
return if REDIS.sismember(redis_key_btc_addresses, request.params["address"])
warn "Address and customer_id do not match"
r.halt(
403,
{"Content-Type" => "text/plain"},
"Address and customer_id do not match"
)
end
route do |r|
r.on "electrum_notify" do
verify_address_customer_id(r)
UnknownTransactions.from(
request.params["customer_id"],
request.params["address"],
ELECTRUM
.getaddresshistory(request.params["address"])
.map { |item| item["tx_hash"] }
).enqueue!
"OK"
end
r.on :jid do |jid|
r.on "credit_cards" do
gateway = CreditCardGateway.new(
jid,
request.params["customer_id"]
)
r.get do
view(
"credit_cards",
locals: {
token: gateway.client_token,
customer_id: gateway.customer_id
}
)
end
r.post do
gateway.default_payment_method = request.params["braintree_nonce"]
"OK"
end
end
end
end
end
run JmpPay.freeze.app