~singpolyma/jmp-pay

6cd9f94dbdea8bbb5a2a0b68e3e0b3998b2f8ed8 — Stephen Paul Weber 1 year, 9 months ago 56244eb
Endpoint that pushes all unknown transactions into Redis

The intent is to use `electrum notify <address> <app>/electrum_noify?address=&customer_id=`

The app asks electrum for all transactions on that address, and then checks
which ones we *don't* already have recorded in the transactions table.  These
are pushed into Redis to be picked up by a to-be-written job that will write
them to the transactions table after 3 confirmations.
8 files changed, 118 insertions(+), 1 deletions(-)

M .builds/debian-stable.yml
M .gitignore
A .gitmodules
M .rubocop.yml
M Gemfile
M config.ru
A lib/electrum.rb
A schemas
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