M .builds/debian-stable.yml => .builds/debian-stable.yml +1 -0
@@ 6,6 6,7 @@ packages:
- ruby-dev
- bundler
- libxml2-dev
+- libpq-dev
- rubocop
environment:
LANG: C.UTF-8
M .gitignore => .gitignore +1 -1
@@ 1,4 1,4 @@
.bundle
.gems
Gemfile.lock
-braintree.dhall>
\ No newline at end of file
+*.dhall<
\ No newline at end of file
A .gitmodules => .gitmodules +3 -0
@@ 0,0 1,3 @@
+[submodule "schemas"]
+ path = schemas
+ url = https://git.singpolyma.net/jmp-schemas
M .rubocop.yml => .rubocop.yml +4 -0
@@ 1,6 1,10 @@
Metrics/LineLength:
Max: 80
+Metrics/BlockLength:
+ ExcludedMethods:
+ - route
+
Layout/Tab:
Enabled: false
M Gemfile => Gemfile +1 -0
@@ 4,6 4,7 @@ source "https://rubygems.org"
gem "braintree"
gem "dhall"
+gem "pg"
gem "redis"
gem "roda"
gem "slim"
M config.ru => config.ru +66 -0
@@ 3,11 3,21 @@
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)
@@ 77,11 87,67 @@ protected
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(
A lib/electrum.rb => lib/electrum.rb +41 -0
@@ 0,0 1,41 @@
+# frozen_string_literal: true
+
+require "json"
+require "net/http"
+require "securerandom"
+
+class Electrum
+ def initialize(rpc_uri:, rpc_username:, rpc_password:)
+ @rpc_uri = URI(rpc_uri)
+ @rpc_username = rpc_username
+ @rpc_password = rpc_password
+ end
+
+ def getaddresshistory(address)
+ rpc_call(:getaddresshistory, address: address)["result"]
+ end
+
+protected
+
+ def rpc_call(method, params)
+ JSON.parse(post_json(
+ jsonrpc: "2.0",
+ id: SecureRandom.hex,
+ method: method.to_s,
+ params: params
+ ).body)
+ end
+
+ def post_json(data)
+ req = Net::HTTP::Post.new(@rpc_uri, "Content-Type" => "application/json")
+ req.basic_auth(@rpc_username, @rpc_password)
+ req.body = data.to_json
+ Net::HTTP.start(
+ @rpc_uri.hostname,
+ @rpc_uri.port,
+ use_ssl: @rpc_uri.scheme == "https"
+ ) do |http|
+ http.request(req)
+ end
+ end
+end
A schemas => schemas +1 -0
@@ 0,0 1,1 @@
+Subproject commit 3e0d7e8ae7193f567294036c3235d50ed318b945