diff --git a/.builds/debian-stable.yml b/.builds/debian-stable.yml index 5b7a7785dbb0cd26edfdd034572e309b82636ef8..c5f9b2338e84c62d19de840c72f98223827112db 100644 --- a/.builds/debian-stable.yml +++ b/.builds/debian-stable.yml @@ -6,6 +6,7 @@ packages: - ruby-dev - bundler - libxml2-dev +- libpq-dev - rubocop environment: LANG: C.UTF-8 diff --git a/.gitignore b/.gitignore index 9f1b2e43f3d783b484dd367a8a63306c058e35da..0ffb26156a239a949bf9012634f98fa5899ecf3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .bundle .gems Gemfile.lock -braintree.dhall \ No newline at end of file +*.dhall \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..7b04718ad592e540c6d1679b2607a72bc1c51f4a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "schemas"] + path = schemas + url = https://git.singpolyma.net/jmp-schemas diff --git a/.rubocop.yml b/.rubocop.yml index 51689e074c871e3eaadf2cb2a2f5b10442a6e455..3f393ef57e08bb0befa746868e21df62110414b8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,10 @@ Metrics/LineLength: Max: 80 +Metrics/BlockLength: + ExcludedMethods: + - route + Layout/Tab: Enabled: false diff --git a/Gemfile b/Gemfile index f5059797f2f35e2dca9d3e10486e888029ebe7af..2ae45b84bafadb0b12b35bb9948a071b18a0319d 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ source "https://rubygems.org" gem "braintree" gem "dhall" +gem "pg" gem "redis" gem "roda" gem "slim" diff --git a/config.ru b/config.ru index 3f9e7dd290dbf0041eedd6eaea1d3a8122645b24..b944b74768a0adb1a26beb3e902ddff3e5eff267 100644 --- a/config.ru +++ b/config.ru @@ -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( diff --git a/lib/electrum.rb b/lib/electrum.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a972d5857500659bdcef7a7d3b91725fdcfee3b --- /dev/null +++ b/lib/electrum.rb @@ -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 diff --git a/schemas b/schemas new file mode 160000 index 0000000000000000000000000000000000000000..3e0d7e8ae7193f567294036c3235d50ed318b945 --- /dev/null +++ b/schemas @@ -0,0 +1 @@ +Subproject commit 3e0d7e8ae7193f567294036c3235d50ed318b945