From 8cb7c1830499e74e5299c5b16aa11e2458c9ad7d Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 20 Apr 2021 10:35:54 -0500 Subject: [PATCH] Helpers for doing Electrum RPC --- lib/electrum.rb | 80 +++++++++++++++++++++++++++++++ test/test_electrum.rb | 106 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 lib/electrum.rb create mode 100644 test/test_electrum.rb diff --git a/lib/electrum.rb b/lib/electrum.rb new file mode 100644 index 0000000..88ae116 --- /dev/null +++ b/lib/electrum.rb @@ -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 diff --git a/test/test_electrum.rb b/test/test_electrum.rb new file mode 100644 index 0000000..316c580 --- /dev/null +++ b/test/test_electrum.rb @@ -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 -- 2.45.2