~singpolyma/sgx-jmp

8cb7c1830499e74e5299c5b16aa11e2458c9ad7d — Stephen Paul Weber 2 years ago 829d69d
Helpers for doing Electrum RPC
2 files changed, 186 insertions(+), 0 deletions(-)

A lib/electrum.rb
A test/test_electrum.rb
A lib/electrum.rb => lib/electrum.rb +80 -0
@@ 0,0 1,80 @@
# frozen_string_literal: true

require "bigdecimal"
require "em_promise"
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 createnewaddress
		rpc_call(:createnewaddress, {}).then { |r| r["result"] }
	end

	def getaddresshistory(address)
		rpc_call(:getaddresshistory, address: address).then { |r| r["result"] }
	end

	def gettransaction(tx_hash)
		rpc_call(:gettransaction, txid: tx_hash).then { |tx|
			rpc_call(:deserialize, [tx["result"]])
		}.then do |tx|
			Transaction.new(self, tx_hash, tx["result"])
		end
	end

	def get_tx_status(tx_hash)
		rpc_call(:get_tx_status, txid: tx_hash).then { |r| r["result"] }
	end

	class Transaction
		def initialize(electrum, tx_hash, tx)
			@electrum = electrum
			@tx_hash = tx_hash
			@tx = tx
		end

		def confirmations
			@electrum.get_tx_status(@tx_hash).then { |r| r["confirmations"] }
		end

		def amount_for(*addresses)
			BigDecimal.new(
				@tx["outputs"]
					.select { |o| addresses.include?(o["address"]) }
					.map { |o| o["value_sats"] }
					.sum
			) * 0.00000001
		end
	end

protected

	def rpc_call(method, params)
		post_json(
			jsonrpc: "2.0",
			id: SecureRandom.hex,
			method: method.to_s,
			params: params
		).then { |res| JSON.parse(res.response) }
	end

	def post_json(data)
		EM::HttpRequest.new(
			@rpc_uri,
			tls: { verify_peer: true }
		).post(
			head: {
				"Authorization" => [@rpc_username, @rpc_password],
				"Content-Type" => "application/json"
			},
			body: data.to_json
		)
	end
end

A test/test_electrum.rb => test/test_electrum.rb +106 -0
@@ 0,0 1,106 @@
# frozen_string_literal: true

require "test_helper"
require "electrum"

class ElectrumTest < Minitest::Test
	RPC_URI = "http://example.com"

	def setup
		@electrum = Electrum.new(
			rpc_uri: RPC_URI,
			rpc_username: "username",
			rpc_password: "password"
		)
	end

	def stub_rpc(method, params)
		stub_request(:post, RPC_URI).with(
			headers: { "Content-Type" => "application/json" },
			basic_auth: ["username", "password"],
			body: hash_including(
				method: method,
				params: params
			)
		)
	end

	property(:getaddresshistory) { string(:alnum) }
	em :test_getaddresshistory
	def getaddresshistory(address)
		req =
			stub_rpc("getaddresshistory", address: address)
			.to_return(body: { result: "result" }.to_json)
		assert_equal "result", @electrum.getaddresshistory(address).sync
		assert_requested(req)
	end

	property(:get_tx_status) { string(:alnum) }
	em :test_get_tx_status
	def get_tx_status(tx_hash)
		req =
			stub_rpc("get_tx_status", txid: tx_hash)
			.to_return(body: { result: "result" }.to_json)
		assert_equal "result", @electrum.get_tx_status(tx_hash).sync
		assert_requested(req)
	end

	property(:gettransaction) { [string(:alnum), string(:xdigit)] }
	em :test_gettransaction
	def gettransaction(tx_hash, dummy_tx)
		req1 =
			stub_rpc("gettransaction", txid: tx_hash)
			.to_return(body: { result: dummy_tx }.to_json)
		req2 =
			stub_rpc("deserialize", [dummy_tx])
			.to_return(body: { result: { outputs: [] } }.to_json)
		assert_kind_of Electrum::Transaction, @electrum.gettransaction(tx_hash).sync
		assert_requested(req1)
		assert_requested(req2)
	end

	class TransactionTest < Minitest::Test
		def transaction(outputs=[])
			electrum_mock = Minitest::Mock.new("Electrum")
			[
				electrum_mock,
				Electrum::Transaction.new(
					electrum_mock,
					"txhash",
					"outputs" => outputs
				)
			]
		end

		def test_confirmations
			electrum_mock, tx = transaction
			electrum_mock.expect(
				:get_tx_status,
				EMPromise.resolve("confirmations" => 1234),
				["txhash"]
			)
			assert_equal 1234, tx.confirmations.sync
		end
		em :test_confirmations

		def test_amount_for_empty
			_, tx = transaction
			assert_equal 0, tx.amount_for
		end

		def test_amount_for_address_not_present
			_, tx = transaction([{ "address" => "address", "value_sats" => 1 }])
			assert_equal 0, tx.amount_for("other_address")
		end

		def test_amount_for_address_present
			_, tx = transaction([{ "address" => "address", "value_sats" => 1 }])
			assert_equal 0.00000001, tx.amount_for("address")
		end

		def test_amount_for_one_of_address_present
			_, tx = transaction([{ "address" => "address", "value_sats" => 1 }])
			assert_equal 0.00000001, tx.amount_for("boop", "address", "lol")
		end
	end
end