@@ 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
@@ 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