├── .rspec ├── .yardopts ├── logo.png ├── lib ├── xrbp │ ├── version.rb │ ├── webclient.rb │ ├── json.rb │ ├── nodestore │ │ ├── sle.rb │ │ ├── uint.rb │ │ ├── shamap │ │ │ ├── errors.rb │ │ │ ├── item.rb │ │ │ ├── tree_node.rb │ │ │ ├── tagged_cache.rb │ │ │ ├── node.rb │ │ │ ├── node_id.rb │ │ │ ├── inner_node.rb │ │ │ └── node_factory.rb │ │ ├── protocol.rb │ │ ├── sle │ │ │ ├── st_account.rb │ │ │ ├── st_ledger_entry.rb │ │ │ ├── st_object.rb │ │ │ ├── st_amount_comparison.rb │ │ │ ├── st_amount.rb │ │ │ └── st_amount_arithmatic.rb │ │ ├── protocol │ │ │ ├── currency.rb │ │ │ ├── quality.rb │ │ │ ├── rate.rb │ │ │ ├── issue.rb │ │ │ └── indexes.rb │ │ ├── amendments.rb │ │ ├── fees.rb │ │ ├── db.rb │ │ ├── backends │ │ │ ├── rocksdb.rb │ │ │ └── nudb.rb │ │ └── sqldb.rb │ ├── overlay.rb │ ├── dsl │ │ ├── webclient.rb │ │ ├── validators.rb │ │ ├── accounts.rb │ │ ├── websocket.rb │ │ └── ledgers.rb │ ├── webclient │ │ ├── plugins.rb │ │ ├── plugins │ │ │ ├── result_parser.rb │ │ │ └── autoretry.rb │ │ └── connection.rb │ ├── plugins.rb │ ├── model.rb │ ├── plugins │ │ ├── base.rb │ │ ├── plugin_registry.rb │ │ ├── result_parser.rb │ │ ├── has_result_parsers.rb │ │ └── has_plugin.rb │ ├── crypto.rb │ ├── terminatable.rb │ ├── nodestore.rb │ ├── websocket │ │ ├── plugins.rb │ │ ├── multi │ │ │ ├── prioritized.rb │ │ │ ├── fallback.rb │ │ │ ├── round_robin.rb │ │ │ ├── parallel.rb │ │ │ └── multi_connection.rb │ │ ├── cmds.rb │ │ ├── cmds │ │ │ ├── server_info.rb │ │ │ ├── ledger_entry.rb │ │ │ ├── subscribe.rb │ │ │ ├── account_tx.rb │ │ │ ├── account_info.rb │ │ │ ├── ledger.rb │ │ │ ├── account_objects.rb │ │ │ ├── account_lines.rb │ │ │ ├── account_offers.rb │ │ │ ├── book_offers.rb │ │ │ └── paginated.rb │ │ ├── command.rb │ │ ├── plugins │ │ │ ├── result_parser.rb │ │ │ ├── command_dispatcher.rb │ │ │ ├── command_paginator.rb │ │ │ ├── autoconnect.rb │ │ │ ├── connection_timeout.rb │ │ │ └── message_dispatcher.rb │ │ ├── message.rb │ │ ├── socket.rb │ │ ├── client.rb │ │ └── connection.rb │ ├── model │ │ ├── parsers │ │ │ ├── node.rb │ │ │ ├── validator.rb │ │ │ ├── market.rb │ │ │ ├── account.rb │ │ │ ├── gateway.rb │ │ │ └── quote.rb │ │ ├── gateway.rb │ │ ├── validator.rb │ │ ├── base.rb │ │ ├── ledger.rb │ │ ├── market.rb │ │ └── node.rb │ ├── websocket.rb │ ├── thread_registry.rb │ ├── common.rb │ ├── crypto │ │ ├── validator.rb │ │ ├── seed.rb │ │ ├── node.rb │ │ ├── account.rb │ │ └── key.rb │ ├── dsl.rb │ ├── overlay │ │ ├── frame.rb │ │ ├── messages.rb │ │ ├── handshake.rb │ │ └── connection.rb │ └── core_ext.rb └── xrbp.rb ├── .gitignore ├── spec ├── xrbp │ ├── nodestore │ │ ├── fees_spec.rb │ │ ├── sle │ │ │ ├── st_ledger_entry_spec.rb │ │ │ ├── st_amount_arithmatic_spec.rb │ │ │ ├── st_amount_comparison_spec.rb │ │ │ ├── st_object_spec.rb │ │ │ ├── st_amount_spec.rb │ │ │ └── st_amount_conversion_spec.rb │ │ ├── shamap │ │ │ ├── node_factory_spec.rb │ │ │ ├── node_id_spec.rb │ │ │ ├── node_spec.rb │ │ │ └── inner_node_spec.rb │ │ ├── amendments_spec.rb │ │ ├── protocol │ │ │ ├── rate_spec.rb │ │ │ └── indexes_spec.rb │ │ ├── backends │ │ │ ├── rocksdb_spec.rb │ │ │ ├── db_access.rb │ │ │ └── nudb_spec.rb │ │ ├── shamap_spec.rb │ │ └── ledger_access.rb │ ├── crypto │ │ ├── seed_spec.rb │ │ ├── key_spec.rb │ │ ├── node_spec.rb │ │ └── account_spec.rb │ └── websocket │ │ ├── command_spec.rb │ │ ├── message_spec.rb │ │ ├── plugins │ │ └── cmd_dispatcher_spec.rb │ │ ├── connection_spec.rb │ │ └── client_spec.rb ├── recordings │ ├── XRBP_WebSocket_Client │ │ ├── _connect │ │ │ ├── is_open.yml │ │ │ ├── starts_reading.yml │ │ │ └── performs_handshake.yml │ │ └── _close │ │ │ ├── closes_connection.yml │ │ │ └── results_in_completed_connection.yml │ ├── XRBP_NodeStore_Backends_RocksDB │ │ └── provides_database_access │ │ │ ├── key_not_found │ │ │ └── returns_nil.yml │ │ │ └── returns_values_for_key.yml │ └── XRBP_NodeStore_Backends_NuDB │ │ ├── provides_a_database_parser │ │ ├── returns_ledger.yml │ │ ├── returns_inner_node.yml │ │ ├── returns_transaction.yml │ │ └── returns_ledger_entry.yml │ │ ├── provides_database_access │ │ ├── returns_values_for_key.yml │ │ └── key_not_found │ │ │ └── returns_nil.yml │ │ └── provides_ledger_access │ │ └── provides_access_to_order_book.yml ├── helpers │ ├── force_serializable.rb │ └── test_handshake.rb └── spec_helper.rb ├── Rakefile ├── examples ├── dsl │ ├── ledger.rb │ ├── validators.rb │ ├── ledger_subscribe.rb │ └── account.rb ├── latest_account.rb ├── autorety.rb ├── validator.rb ├── server_info.rb ├── account.rb ├── gateways.rb ├── cmd.rb ├── multi.rb ├── accounts.rb ├── p2p.rb ├── websocket.rb ├── round_robin.rb ├── paginate.rb ├── ledger_subscribe.rb ├── prioritized.rb ├── ledger.rb ├── market.rb ├── autoconnect_timeout.rb ├── username.rb ├── ledger_multi_subscribe.rb ├── crawl_nodes.rb ├── nodestore1.rb └── nodestore2.rb ├── Gemfile ├── LICENSE.txt ├── xrbp.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --exclude ripple.proto.rb 3 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/logo.png -------------------------------------------------------------------------------- /lib/xrbp/version.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | VERSION = '0.2.8' 3 | end # module XRBP 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | coverage/ 3 | *.swp 4 | .yardoc 5 | doc/ 6 | .bundle/ 7 | vendor/ 8 | -------------------------------------------------------------------------------- /lib/xrbp/webclient.rb: -------------------------------------------------------------------------------- 1 | require 'xrbp/webclient/connection' 2 | require 'xrbp/webclient/plugins' 3 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/fees_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::Fees do 2 | it "returns account reserve" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/sle/st_ledger_entry_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::STLedgerEntry do 2 | it "returns type code" 3 | 4 | it "returns type" 5 | end 6 | -------------------------------------------------------------------------------- /examples/dsl/ledger.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | include XRBP::DSL 5 | 6 | puts ledger 7 | puts ledger(45918932) 8 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_WebSocket_Client/_connect/is_open.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_WebSocket_Client/_connect/is_open.yml -------------------------------------------------------------------------------- /lib/xrbp/json.rb: -------------------------------------------------------------------------------- 1 | # Attempt to use yajl bindings, 2 | # else fallback to stock json 3 | begin 4 | require 'yajl/json_gem' 5 | rescue LoadError 6 | require 'json' 7 | end 8 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_WebSocket_Client/_connect/starts_reading.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_WebSocket_Client/_connect/starts_reading.yml -------------------------------------------------------------------------------- /examples/latest_account.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | puts XRBP::Model::Account.latest(:connection => XRBP::WebClient::Connection.new) 5 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sle.rb: -------------------------------------------------------------------------------- 1 | require_relative './sle/st_object' 2 | require_relative './sle/st_account' 3 | require_relative './sle/st_amount' 4 | require_relative './sle/st_ledger_entry' 5 | -------------------------------------------------------------------------------- /lib/xrbp/overlay.rb: -------------------------------------------------------------------------------- 1 | require_relative './overlay/connection' 2 | require_relative './overlay/handshake' 3 | require_relative './overlay/frame' 4 | require_relative './overlay/messages' 5 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_WebSocket_Client/_close/closes_connection.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_WebSocket_Client/_close/closes_connection.yml -------------------------------------------------------------------------------- /lib/xrbp/nodestore/uint.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | def self.uint256 4 | Array.new(32) { 0 }.pack("C*") 5 | end 6 | end # module NodeStore 7 | end # module XRBP 8 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_WebSocket_Client/_connect/performs_handshake.yml: -------------------------------------------------------------------------------- 1 | {I"EXRBP::WebSocket::Socket-connect-284ec1b84acdd24efefb11b0d41ae751:ETo:Camcorder::Recording: @value0:@behavior: return -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_RocksDB/provides_database_access/key_not_found/returns_nil.yml: -------------------------------------------------------------------------------- 1 | {I"4RocksDB::DB-[]-4cb4033e0f1956abf33172f265e6eb99:ETo:Camcorder::Recording: @value0:@behavior: return -------------------------------------------------------------------------------- /spec/recordings/XRBP_WebSocket_Client/_close/results_in_completed_connection.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_WebSocket_Client/_close/results_in_completed_connection.yml -------------------------------------------------------------------------------- /spec/xrbp/crypto/seed_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::Crypto do 2 | it "generates valid seed" do 3 | seed = described_class.seed 4 | expect(described_class.seed?(seed[:seed])).to be(true) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /examples/dsl/validators.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | include XRBP::DSL 5 | 6 | validators.each { |v| 7 | puts v["validation_public_key"] + ": " + v["domain"].to_s 8 | } 9 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/errors.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | class SHAMap 3 | module Errors 4 | class MissingNode < StandardError 5 | end # class MissingNode 6 | end 7 | end # class SHAMap 8 | end # module XRBP 9 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_ledger.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_ledger.yml -------------------------------------------------------------------------------- /lib/xrbp/nodestore/protocol.rb: -------------------------------------------------------------------------------- 1 | require_relative './protocol/currency' 2 | require_relative './protocol/issue' 3 | require_relative './protocol/indexes' 4 | require_relative './protocol/rate' 5 | require_relative './protocol/quality' 6 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sle/st_account.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | # Serialized Account Representation 4 | class STAccount 5 | # TODO 6 | end # class STAccount 7 | end # module NodeStore 8 | end # module XRBP 9 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_inner_node.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_inner_node.yml -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_transaction.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_transaction.yml -------------------------------------------------------------------------------- /lib/xrbp/dsl/webclient.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module DSL 3 | # Client which may be used to access HTTP resources 4 | def webclient 5 | @webclient ||= WebClient::Connection.new 6 | end 7 | end # module DSL 8 | end # module XRBP 9 | -------------------------------------------------------------------------------- /lib/xrbp/webclient/plugins.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebClient 3 | include PluginRegistry 4 | end # module WebClient 5 | end # module XRPB 6 | 7 | require_relative './plugins/result_parser' 8 | require_relative './plugins/autoretry' 9 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_ledger_entry.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_a_database_parser/returns_ledger_entry.yml -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_database_access/returns_values_for_key.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_database_access/returns_values_for_key.yml -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_RocksDB/provides_database_access/returns_values_for_key.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_NodeStore_Backends_RocksDB/provides_database_access/returns_values_for_key.yml -------------------------------------------------------------------------------- /examples/autorety.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | connection = XRBP::WebClient::Connection.new 5 | connection.add_plugin :autoretry 6 | connection.url = "https://devnull.network" 7 | puts connection.perform 8 | -------------------------------------------------------------------------------- /lib/xrbp/plugins.rb: -------------------------------------------------------------------------------- 1 | require_relative './plugins/base' 2 | require_relative './plugins/has_plugin' 3 | require_relative './plugins/plugin_registry' 4 | 5 | require_relative './plugins/result_parser' 6 | require_relative './plugins/has_result_parsers' 7 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_ledger_access/provides_access_to_order_book.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevNullProd/XRBP/HEAD/spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_ledger_access/provides_access_to_order_book.yml -------------------------------------------------------------------------------- /examples/validator.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | connection = XRBP::WebClient::Connection.new 5 | XRBP::Model::Validator.all(:connection => connection) 6 | .each do |v| 7 | puts v 8 | end 9 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/sle/st_amount_arithmatic_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::STAmount do 2 | it "is addable" 3 | it "is subtactable" 4 | it "is divisable" 5 | it "is multiplicable" 6 | it "is negatable" 7 | end # describe XRBP::NodeStore::STAmount 8 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/shamap/node_factory_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::SHAMap::Node do 2 | it "makes tx_id node" 3 | 4 | it "makes leaf_node node" 5 | 6 | it "makes inner_node node" 7 | 8 | it "makes tx_node node" 9 | end # describe XRBP::SHAMap::Node 10 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/shamap/node_id_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::SHAMap::NodeID do 2 | it "returns masks for depth" 3 | it "returns mask for depth" 4 | it "returns branch for given hash" 5 | it "returns child node id for given branch" 6 | end # describe XRBP::SHAMap::NodeID 7 | -------------------------------------------------------------------------------- /examples/server_info.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s2.ripple.com:443" 5 | ws.add_plugin :command_dispatcher 6 | ws.connect 7 | 8 | puts XRBP::Model::Node.new.server_info(:connection => ws) 9 | -------------------------------------------------------------------------------- /lib/xrbp/model.rb: -------------------------------------------------------------------------------- 1 | require_relative './model/base' 2 | require_relative './model/ledger' 3 | require_relative './model/account' 4 | require_relative './model/validator' 5 | require_relative './model/gateway' 6 | require_relative './model/market' 7 | require_relative './model/node' 8 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/protocol/currency.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | def self.xrp_currency 4 | @xrp_currency ||= 0 5 | end 6 | 7 | def self.no_currency 8 | @no_currency ||= 1 9 | end 10 | end # module NodeStore 11 | end # module XRBP 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | Encoding.default_external = Encoding::UTF_8 2 | Encoding.default_internal = Encoding::UTF_8 3 | 4 | source "https://rubygems.org" 5 | 6 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 7 | 8 | # Specify your gem's dependencies in gdbib.gemspec 9 | gemspec 10 | -------------------------------------------------------------------------------- /spec/helpers/force_serializable.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | # XXX: allow any object to be serialzed via yaml 3 | # by overriding 'psych' serializer callback 4 | def force_serializable! 5 | class << self 6 | def encode_with(coder) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/recordings/XRBP_NodeStore_Backends_NuDB/provides_database_access/key_not_found/returns_nil.yml: -------------------------------------------------------------------------------- 1 | {I"6RuDB::Store-open-8e39a71b973bca34c9b8e6e117963ab5:ETo:Camcorder::Recording: @valueU:RuDB::ErrorCodei:@behavior: returnI"7RuDB::Store-fetch-a07eafd66a1caf9948153bb1451d2b3d;To;;["U;i; ; 2 | -------------------------------------------------------------------------------- /lib/xrbp/dsl/validators.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module DSL 3 | # Return list of all validators 4 | # 5 | # @return [Array] list of validators retrieved 6 | def validators 7 | Model::Validator.all :connection => webclient 8 | end 9 | end # module DSL 10 | end # module XRBP 11 | -------------------------------------------------------------------------------- /examples/account.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s2.ripple.com:443" 5 | ws.add_plugin :command_dispatcher 6 | ws.connect 7 | 8 | puts XRBP::Model::Account.new(:id => "rDsbeomae4FXwgQTJp9Rs64Qg9vDiTCdBv").info(:connection => ws) 9 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/sle/st_amount_comparison_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::STAmount do 2 | it "is < other amount" 3 | 4 | it "is > other amount" 5 | 6 | it "is >= other amount" 7 | 8 | it "is == other amount" 9 | 10 | it "is <=> other amount" 11 | end # describe XRBP::NodeStore::STAmount 12 | -------------------------------------------------------------------------------- /examples/gateways.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | connection = XRBP::WebClient::Connection.new 5 | XRBP::Model::Gateway.all(:connection => connection) 6 | .each do |gw| 7 | puts "#{gw[:id]} #{gw[:names].join(",")} (#{gw[:currencies].join(",")})" 8 | end 9 | -------------------------------------------------------------------------------- /lib/xrbp/plugins/base.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # Base plugin definition, common logic shared by all connection plugins. 3 | class PluginBase 4 | attr_accessor :connection 5 | 6 | def initialize(connection) 7 | @connection = connection 8 | end 9 | end # class PluginBase 10 | end # module XRBP 11 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/amendments_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::Amendments do 2 | describe "#fix1141?" do 3 | context "specified time is > fix1141_time" do 4 | it "returns true" 5 | end 6 | 7 | context "specified time is <= fix1141_time" do 8 | it "returns false" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /examples/cmd.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s1.ripple.com:443" 5 | 6 | ws.connect 7 | ws.add_plugin :command_dispatcher 8 | ws.cmd(XRBP::WebSocket::Cmds::ServerInfo.new) do |r| 9 | puts r 10 | ws.close! 11 | end 12 | 13 | ws.wait_for_close 14 | -------------------------------------------------------------------------------- /lib/xrbp.rb: -------------------------------------------------------------------------------- 1 | require 'event_emitter' 2 | 3 | require 'xrbp/json' 4 | require 'xrbp/common' 5 | require 'xrbp/terminatable' 6 | require 'xrbp/plugins' 7 | require 'xrbp/websocket' 8 | require 'xrbp/webclient' 9 | require 'xrbp/crypto' 10 | require 'xrbp/nodestore' 11 | require 'xrbp/overlay' 12 | require 'xrbp/model' 13 | require 'xrbp/dsl' 14 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/amendments.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | module Amendments 4 | def fix1141_time 5 | @fix1141_time ||= Time.new(2016, 6, 1, 17, 0, 0, 0) 6 | end 7 | 8 | def fix1141?(time) 9 | time > fix1141_time 10 | end 11 | end 12 | end # module NodeStore 13 | end # module XRBP 14 | -------------------------------------------------------------------------------- /examples/multi.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::MultiConnection.new "wss://s1.ripple.com:443", 5 | "wss://s2.ripple.com:443" 6 | 7 | ws.add_plugin :command_dispatcher 8 | ws.connect 9 | 10 | puts ws.cmd(XRBP::WebSocket::Cmds::ServerInfo.new) 11 | -------------------------------------------------------------------------------- /examples/dsl/ledger_subscribe.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | include XRBP::DSL 5 | 6 | Signal.trap("INT") { 7 | Thread.new { 8 | websocket.force_quit! 9 | websocket.close! 10 | } 11 | } 12 | 13 | websocket_msg do |c, msg| 14 | puts msg 15 | end 16 | 17 | subscribe_to_ledgers 18 | websocket_wait 19 | -------------------------------------------------------------------------------- /spec/xrbp/websocket/command_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::WebSocket::Command do 2 | subject { described_class.new(command: 'test') } 3 | 4 | it 'returns command being requested' do 5 | expect(subject.requesting).to eq 'test' 6 | end 7 | 8 | it 'returns bool indicating if we are request command' do 9 | expect(subject).to be_requesting('test') 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /examples/accounts.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | connection = XRBP::WebClient::Connection.new 5 | connection.on :account do |acct| 6 | puts acct 7 | end 8 | 9 | Signal.trap("INT") { 10 | connection.force_quit! 11 | } 12 | 13 | XRBP::Model::Account.all(:connection => connection, 14 | :replay => true) 15 | -------------------------------------------------------------------------------- /examples/p2p.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | overlay = XRBP::Overlay::Connection.new "127.0.0.1", 51235 5 | overlay.connect 6 | puts overlay.handshake.response 7 | 8 | overlay.read_frames do |frame| 9 | puts "Message: #{frame.type_name} (#{frame.size} bytes)" 10 | # frame.message => protobuf message 11 | end 12 | 13 | overlay.close 14 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/protocol/quality.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | # Ripple specific constant used for parsing qualities and other things 4 | # https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/Quality.h#L107 5 | QUALITY_ONE = 1000000000 6 | 7 | class Quality 8 | end # class Quality 9 | end # module NodeStore 10 | end # module XRBP 11 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/item.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | class SHAMap 3 | # Binary data blog stored in DB w/ key 4 | class Item 5 | attr_reader :key 6 | attr_reader :data 7 | 8 | def initialize(args = {}) 9 | @key = args[:key] 10 | @data = args[:data] 11 | end 12 | end # class Item 13 | end # class SHAMap 14 | end # module XRBP 15 | -------------------------------------------------------------------------------- /examples/websocket.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s1.ripple.com:443" 5 | 6 | ws.on :open do 7 | puts "Opened" 8 | ws.close! 9 | end 10 | 11 | ws.on :close do 12 | puts "Closed" 13 | end 14 | 15 | ws.on :error do |e| 16 | puts "Err #{e}" 17 | end 18 | 19 | ws.connect 20 | ws.wait_for_close 21 | -------------------------------------------------------------------------------- /examples/round_robin.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::RoundRobin.new "wss://s1.ripple.com:443", 5 | "wss://s2.ripple.com:443" 6 | 7 | ws.add_plugin :command_dispatcher 8 | ws.connect 9 | 10 | puts ws.cmd(XRBP::WebSocket::Cmds::ServerInfo.new) 11 | puts ws.cmd(XRBP::WebSocket::Cmds::ServerInfo.new) 12 | -------------------------------------------------------------------------------- /lib/xrbp/crypto.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # The Crypto module defines methods to create XRPL compliant 3 | # keys and subsequent entities (accounts, nodes, validators). 4 | module Crypto 5 | end # module Crypto 6 | end # module XRBP 7 | 8 | require 'xrbp/crypto/seed' 9 | require 'xrbp/crypto/key' 10 | require 'xrbp/crypto/account' 11 | require 'xrbp/crypto/node' 12 | require 'xrbp/crypto/validator' 13 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/protocol/rate_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::Rate do 2 | it "is convertable to an amount" do 3 | rate = described_class.new 2000000000 4 | amount = rate.to_amount 5 | 6 | amount.issue.should be(XRBP::NodeStore.no_issue) 7 | amount.mantissa.should eq(2000000000000000) 8 | amount.exponent.should eq(-15) 9 | 10 | amount.iou_amount.should eq(2) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/paginate.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s1.ripple.com:443" 5 | 6 | ws.connect 7 | ws.add_plugin :command_dispatcher, :command_paginator 8 | ws.cmd(XRBP::WebSocket::Cmds::AccountObjects.new("rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", :paginate => true)) do |r| 9 | puts r 10 | ws.close! 11 | end 12 | 13 | ws.wait_for_close 14 | -------------------------------------------------------------------------------- /examples/ledger_subscribe.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s1.ripple.com:443" 5 | ws.add_plugin :command_dispatcher 6 | ws.connect 7 | 8 | i = 0 9 | ws.on :ledger do |ledger| 10 | puts ledger 11 | 12 | i += 1 13 | ws.close! if i > 5 14 | end 15 | 16 | XRBP::Model::Ledger.subscribe(:connection => ws) 17 | 18 | ws.wait_for_close 19 | -------------------------------------------------------------------------------- /examples/prioritized.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Prioritized.new "wss://s1.ripple.com:443", 5 | "wss://s2.ripple.com:443" 6 | 7 | ws.add_plugin :command_dispatcher, :result_parser 8 | ws.parse_results { |res| 9 | res["result"]["ledger"] 10 | } 11 | ws.connect 12 | 13 | puts ws.cmd(XRBP::WebSocket::Cmds::Ledger.new(28327070)) 14 | -------------------------------------------------------------------------------- /lib/xrbp/terminatable.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # Helper mixin facilitating controlled termination of 3 | # asynchronous components. 4 | # 5 | # @private 6 | module Terminatable 7 | def terminate_queue 8 | @terminate_queue ||= Queue.new 9 | end 10 | 11 | def terminate? 12 | !!terminate_queue.pop_or_nil 13 | end 14 | 15 | def terminate! 16 | terminate_queue << true 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/dsl/account.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | include XRBP::DSL 5 | 6 | # override endpoints 7 | #def websocket_endpoints 8 | # ["wss://s1.ripple.com:443", "wss://s2.ripple.com:443"] 9 | #end 10 | 11 | # override websocket 12 | #def websocket 13 | # @websocket ||= WebSocket::Prioritized.new *websocket_endpoints 14 | #end 15 | 16 | puts account_info("rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq") 17 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore.rb: -------------------------------------------------------------------------------- 1 | require_relative './nodestore/uint' 2 | require_relative './nodestore/format' 3 | require_relative './nodestore/sle' 4 | require_relative './nodestore/shamap' 5 | require_relative './nodestore/ledger' 6 | 7 | require_relative './nodestore/protocol' 8 | 9 | require_relative './nodestore/db' 10 | 11 | # optionally include: 12 | #require_relative './nodestore/backends/rocksdb' 13 | #require_relative './nodestore/backends/nudb' 14 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/plugins.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | include PluginRegistry 4 | end # module WebSocket 5 | end # module XRPB 6 | 7 | require_relative './plugins/autoconnect' 8 | require_relative './plugins/connection_timeout' 9 | require_relative './plugins/message_dispatcher' 10 | require_relative './plugins/command_dispatcher' 11 | require_relative './plugins/command_paginator' 12 | require_relative './plugins/result_parser' 13 | -------------------------------------------------------------------------------- /examples/ledger.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s2.ripple.com:443" 5 | ws.add_plugin :command_dispatcher 6 | ws.connect 7 | 8 | puts XRBP::Model::Ledger.new.sync(:connection => ws) 9 | puts XRBP::Model::Ledger.new(:id => 32750).sync(:connection => ws) 10 | 11 | XRBP::Model::Ledger.new.sync(:connection => ws) do |l| 12 | puts l 13 | ws.close! 14 | end 15 | 16 | ws.wait_for_close 17 | -------------------------------------------------------------------------------- /lib/xrbp/model/parsers/node.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | # @private 4 | module Parsers 5 | # Node Peers data parser 6 | # 7 | # @private 8 | class NodePeers < PluginBase 9 | def parser_priority 10 | 0 11 | end 12 | 13 | def parse_result(res, req) 14 | JSON.parse(res)["overlay"]["active"] 15 | end 16 | end 17 | end # module Parsers 18 | end # module Model 19 | end # module XRBP 20 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/tree_node.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | class SHAMap 3 | # Terminating tree node referencing concrete data 4 | class TreeNode < Node 5 | attr_reader :item 6 | 7 | def initialize(args={}) 8 | super 9 | @item = args[:item] 10 | end 11 | 12 | def tree_node? 13 | true 14 | end 15 | 16 | def peek_item 17 | item 18 | end 19 | end # class TreeNode 20 | end # class SHAMap 21 | end # module XRBP 22 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/tagged_cache.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | class SHAMap 3 | # Internal node caching mechanism. 4 | # 5 | # TODO timeout mechanism, metrics 6 | class TaggedCache 7 | def initialize 8 | @cache = {} 9 | end 10 | 11 | def fetch(key) 12 | @cache[key] 13 | end 14 | 15 | def canonicalize(key, node) 16 | @cache[key] = node 17 | end 18 | end # class TaggedCache 19 | end # class SHAMap 20 | end # module XRBP 21 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/multi/prioritized.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | # MultiConnection strategy where connections are tried 4 | # sequentially until one succeeds 5 | class Prioritized < MultiConnection 6 | def next_connection(prev=nil) 7 | return nil if prev == connections.last 8 | return super if prev.nil? 9 | connections[connections.index(prev)+1] 10 | end 11 | end # class Prioritized 12 | end # module WebSocket 13 | end # module XRBP 14 | -------------------------------------------------------------------------------- /examples/market.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | connection = XRBP::WebClient::Connection.new 5 | markets = XRBP::Model::Market.all(:connection => connection) 6 | markets.each do |market| 7 | puts market.inspect 8 | end 9 | 10 | connection = XRBP::WebClient::Connection.new 11 | puts XRBP::Model::Market.new(:connection => connection, 12 | :route => markets.sample[:route] + "/ohlc?periods=60") 13 | .quotes 14 | -------------------------------------------------------------------------------- /examples/autoconnect_timeout.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Connection.new "wss://s1.ripple.com:443" 5 | 6 | ws.on :connecting do 7 | puts "Connecting" 8 | end 9 | 10 | ws.on :open do 11 | puts "Opened" 12 | end 13 | 14 | ws.on :close do 15 | puts "Closed" 16 | end 17 | 18 | ws.on :error do |e| 19 | puts "Err #{e}" 20 | end 21 | 22 | ws.add_plugin :autoconnect, :connection_timeout 23 | ws.connection_timeout = 3 24 | sleep(20) 25 | -------------------------------------------------------------------------------- /examples/username.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | connection = XRBP::WebClient::Connection.new 5 | puts XRBP::Model::Account.new(:id => "rJe12wEAmGtRw44bo3jQqQUMTVFSLPewCS", 6 | :connection => connection) 7 | .username 8 | 9 | puts XRBP::Model::Account.new(:id => "rfexLLNpC6dqyLagjV439EyvfqdYNHsWSH", 10 | :connection => connection) 11 | .username 12 | 13 | -------------------------------------------------------------------------------- /lib/xrbp/plugins/plugin_registry.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # Helper mixin providing list of plugins. 3 | # 4 | # @private 5 | module PluginRegistry 6 | module ClassMethods 7 | def plugins 8 | @plugins ||= {} 9 | end 10 | 11 | def register_plugin(label, cls) 12 | plugins[label] = cls 13 | end 14 | end # module ClassMethods 15 | 16 | def self.included(base) 17 | base.extend(ClassMethods) 18 | end 19 | end # module PluginRegistry 20 | end # module XRBP 21 | -------------------------------------------------------------------------------- /lib/xrbp/websocket.rb: -------------------------------------------------------------------------------- 1 | require 'xrbp/websocket/socket' 2 | require 'xrbp/websocket/client' 3 | require 'xrbp/websocket/connection' 4 | 5 | require 'xrbp/websocket/multi/multi_connection' 6 | require 'xrbp/websocket/multi/round_robin' 7 | require 'xrbp/websocket/multi/prioritized' 8 | require 'xrbp/websocket/multi/fallback' 9 | require 'xrbp/websocket/multi/parallel' 10 | 11 | require 'xrbp/websocket/message' 12 | require 'xrbp/websocket/command' 13 | require 'xrbp/websocket/plugins' 14 | 15 | require 'xrbp/websocket/cmds' 16 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds.rb: -------------------------------------------------------------------------------- 1 | require_relative './cmds/paginated' 2 | require_relative './cmds/server_info' 3 | require_relative './cmds/ledger' 4 | require_relative './cmds/account_lines' 5 | require_relative './cmds/account_info' 6 | require_relative './cmds/account_info' 7 | require_relative './cmds/account_objects' 8 | require_relative './cmds/account_offers' 9 | require_relative './cmds/account_tx' 10 | require_relative './cmds/book_offers' 11 | require_relative './cmds/subscribe' 12 | require_relative './cmds/ledger_entry' 13 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/server_info.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The server_info command asks the server for a human-readable version 5 | # of various information about the rippled server being queried. 6 | # 7 | # https://developers.ripple.com/server_info.html 8 | class ServerInfo < Command 9 | def initialize 10 | super({'command' => 'server_info'}) 11 | end 12 | end 13 | end # module Cmds 14 | end # module WebSocket 15 | end # module Wipple 16 | -------------------------------------------------------------------------------- /lib/xrbp/dsl/accounts.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module DSL 3 | # Return info for the specified account id 4 | # 5 | # @param id [String] account id to query 6 | # @return [Hash, nil] the account info or nil otherwise 7 | def account_info(id) 8 | websocket.add_plugin :autoconnect unless websocket.plugin?(:autoconnect) 9 | websocket.add_plugin :command_dispatcher unless websocket.plugin?(:command_dispatcher) 10 | websocket.cmd(WebSocket::Cmds::AccountInfo.new(id)) 11 | end 12 | end # module DSL 13 | end # module XRBP 14 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/fees.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | class Fees 4 | # FIXME where do these get updated in rippled? 5 | attr_reader :base, :units, :reserve, :increment 6 | 7 | def initialize 8 | @base = 0 9 | @units = 0 10 | @reserve = 0 11 | @increment = 0 12 | end 13 | 14 | def account_reserve(owner_count) 15 | STAmount.new :mantissa => reserve + owner_count + increment 16 | end 17 | end # class Fees 18 | end # module NodeStore 19 | end # module XRBP 20 | -------------------------------------------------------------------------------- /lib/xrbp/thread_registry.rb: -------------------------------------------------------------------------------- 1 | require 'concurrent' 2 | 3 | module XRBP 4 | # Helper mixin providing internal thread management. 5 | # 6 | # @private 7 | module ThreadRegistry 8 | def thread_registry 9 | @thread_registry ||= Concurrent::Array.new 10 | end 11 | 12 | def rsleep(t) 13 | thread_registry << Thread.current 14 | sleep(t) 15 | thread_registry.delete(Thread.current) 16 | end 17 | 18 | def wake_all 19 | thread_registry.each { |th| th.wakeup } 20 | end 21 | end # module ThreadRegistry 22 | end # module XRBP 23 | -------------------------------------------------------------------------------- /lib/xrbp/plugins/result_parser.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # Result Parser plugin base class, allows request results 3 | # to be converted before returning / invoking callback. 4 | class ResultParserBase < PluginBase 5 | attr_accessor :parser 6 | 7 | def added 8 | plugin = self 9 | connection.define_instance_method(:parse_results) do |&bl| 10 | plugin.parser = bl 11 | end 12 | end 13 | 14 | def parse_result(res, req) 15 | return res unless parser 16 | parser.call(res, req) 17 | end 18 | end # class ResultParserBase 19 | end # module XRBP 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'camcorder' 2 | require 'camcorder/rspec' 3 | 4 | Camcorder.config.recordings_dir = 'spec/recordings' 5 | 6 | require 'xrbp' 7 | 8 | RSpec.configure do |config| 9 | config.alias_it_should_behave_like_to :it_provides, 'provides:' 10 | 11 | config.expect_with :rspec do |expectations| 12 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 13 | end 14 | 15 | config.mock_with :rspec do |mocks| 16 | mocks.verify_partial_doubles = true 17 | end 18 | 19 | config.shared_context_metadata_behavior = :apply_to_host_groups 20 | end 21 | -------------------------------------------------------------------------------- /examples/ledger_multi_subscribe.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | ws = XRBP::WebSocket::Parallel.new "wss://s1.ripple.com:443", 5 | "wss://s2.ripple.com:443" 6 | ws.add_plugin :command_dispatcher 7 | ws.connect 8 | 9 | l = 0 10 | ws.on :message do |connection,msg| 11 | msg = JSON.parse(msg.data) 12 | next unless msg["ledger_index"] && msg["ledger_index"] > l 13 | l = msg["ledger_index"] 14 | puts msg 15 | end 16 | 17 | XRBP::Model::Ledger.subscribe(:connection => ws) 18 | 19 | #ws.wait_for_close 20 | sleep(1) while true 21 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/ledger_entry.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The ledger_entry method returns a single ledger object 5 | # from the XRP Ledger in its raw format 6 | # 7 | # https://developers.ripple.com/ledger_entry.html 8 | class LedgerEntry < Command 9 | def initialize(args={}) 10 | @args = args 11 | super(to_h) 12 | end 13 | 14 | def to_h 15 | @args.merge('command' => 'ledger_entry') 16 | end 17 | end 18 | end # module Cmds 19 | end # module WebSocket 20 | end # module Wipple 21 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/multi/fallback.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | # MultiConnection strategy where connections are tried sequentially 4 | # until one is found that is open & succeeds 5 | class Fallback < MultiConnection 6 | def next_connection(prev=nil) 7 | unless prev.nil? 8 | return nil if connections.last == prev 9 | return connections[(connections.index(prev) + 1)..-1].find { |c| !c.closed? } 10 | end 11 | 12 | connections.find { |c| !c.closed? } 13 | end 14 | end # class Fallback 15 | end # module WebSocket 16 | end # module XRBP 17 | -------------------------------------------------------------------------------- /lib/xrbp/common.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module XRBP 4 | # Genesis Ledger: 5 | # https://wiki.ripple.com/Genesis_ledger 6 | # 7 | # Created on 2013-01-01 8 | # https://data.ripple.com/v2/ledgers/32570 9 | GENESIS_TIME = DateTime.new(2013, 1, 1, 0, 0, 0) 10 | 11 | # Convert XRP Ledger time to local time 12 | def self.from_xrp_time(xrp_time) 13 | return nil if xrp_time.nil? 14 | Time.at(xrp_time + 946684800) 15 | end 16 | 17 | # Convert local time to XRP Time 18 | def self.to_xrp_time(local_time) 19 | return nil if local_time.nil? 20 | local_time.to_i - 946684800 21 | end 22 | end # module XRBP 23 | -------------------------------------------------------------------------------- /lib/xrbp/crypto/validator.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Crypto 3 | # Generate a new XRPL validator. Takes same params as {Crypto#node} 4 | def self.validator(key=nil) 5 | return node(key) 6 | end 7 | 8 | # Extract Validator ID from Address. 9 | # Takes same params as {Crypto#parse_node} 10 | def self.parse_validator(validator) 11 | return parse_node(validator) 12 | end 13 | 14 | # Return bool indicating if Validator is valid. 15 | # Takes same params as {Crypto#node?} 16 | def self.validator?(validator) 17 | return node?(validator) 18 | end 19 | end # module Crypto 20 | end # module XRBP 21 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/command.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | class Command < Message 4 | attr_accessor :id 5 | attr_reader :json 6 | 7 | def initialize(data) 8 | @@id ||= 0 9 | @id = (@@id += 1) 10 | 11 | json = Hash[data] 12 | json['id'] = id 13 | 14 | @json = json 15 | 16 | super(json.to_json) 17 | end 18 | 19 | def requesting 20 | @json[:command] || @json["command"] 21 | end 22 | 23 | def requesting?(tgt) 24 | requesting.to_s == tgt.to_s 25 | end 26 | end # class Command 27 | end # module WebSocket 28 | end # module XRBP 29 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/subscribe.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The subscribe method requests periodic notifications 5 | # from the server when certain events happen 6 | # 7 | # https://developers.ripple.com/subscribe.html 8 | class Subscribe < Command 9 | attr_accessor :args 10 | 11 | def initialize(args={}) 12 | @args = args 13 | super(to_h) 14 | end 15 | 16 | def to_h 17 | args.merge(:command => :subscribe) 18 | end 19 | end # class Subscribe 20 | end # module Cmds 21 | end # module WebSocket 22 | end # module Wipple 23 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/sle/st_object_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::STObject do 2 | it "returns object flags" 3 | 4 | describe "#flag?" do 5 | context "flag is set" do 6 | it "returns true" 7 | end 8 | 9 | context "flag is not set" do 10 | it "returns false" 11 | end 12 | end 13 | 14 | describe "#field?" do 15 | context "field is set" do 16 | it "returns true" 17 | end 18 | 19 | context "field is not set" do 20 | it "returns false" 21 | end 22 | end 23 | 24 | it "provide access to field" 25 | 26 | it "provide access to amount field" 27 | 28 | it "provide access to account field" 29 | end 30 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/backends/rocksdb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'xrbp/nodestore/backends/rocksdb' 2 | 3 | require_relative "./db_access" 4 | #require_relative "./db_iterator_access" 5 | #require_relative "../db_parser" 6 | 7 | describe XRBP::NodeStore::Backends::RocksDB do 8 | before(:each) do 9 | Camcorder.intercept_constructor ::RocksDB::DB 10 | end 11 | 12 | after(:each) do 13 | Camcorder.deintercept_constructor ::RocksDB::DB 14 | end 15 | 16 | let(:db) { 17 | XRBP::NodeStore::Backends::RocksDB.new("/var/lib/rippled/rocksdb/") 18 | } 19 | 20 | it_provides "database access" 21 | #it_provides "database iterator access" 22 | #it_provides "a database parser" 23 | end 24 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sle/st_ledger_entry.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | # Special type of Serialized Object whose type is identified 4 | # through the 'ledger_entry_type' field 5 | class STLedgerEntry < STObject 6 | attr_reader :key 7 | 8 | def initialize(args={}) 9 | super 10 | @key = args[:key] 11 | end 12 | 13 | def type_code 14 | @type_code ||= field(:uint16, :ledger_entry_type) 15 | end 16 | 17 | def type 18 | @type ||= Format::LEDGER_ENTRY_TYPE_CODES[type_code] 19 | end 20 | end # class STLedgerEntry 21 | 22 | SLE = STLedgerEntry 23 | end # module NodeStore 24 | end # module XRBP 25 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/backends/db_access.rb: -------------------------------------------------------------------------------- 1 | shared_examples "database access" do |opts={}| 2 | let(:key) { ["516F940640172099A89F9733B8E69F2A56720E67C53EE7E0F3022BB6E55E6986"].pack("H*") } 3 | let(:val) { "0000000000000000014c57520002d4ebd001633dd73a0b3efdc4fad6eb19f6a6089e7aebc73af596db471305bc0ebb99d9d1414bd4db8c9d43934a5e57c598f0505664f9349da87fd32afbe1c315a2cb6cfb5b690aecc6b1a3fea269daaa66c820978ac95f0399ecaca7a3abac91402c9fc7961a53d053332f247c5e24247c5e250a00" } 4 | 5 | it "returns values for key" do 6 | actual = db[key].unpack("H*").first 7 | expect(actual).to eq(val) 8 | end 9 | 10 | context "key not found" do 11 | it "returns nil" do 12 | expect(db["key"]).to be_nil 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/xrbp/dsl.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # The DSL namespace can be *included* in client logic to provide 3 | # an easy-to-use mechanism to read and write XRP data. 4 | # 5 | # @example Retrieve ledger, subscribe to updates 6 | # include XRBP::DSL 7 | # 8 | # puts "Genesis ledger: " 9 | # puts ledger(32570) 10 | # 11 | # websocket_msg do |msg| 12 | # puts "Ledger received:" 13 | # puts msg 14 | # end 15 | # 16 | # subscribe_to_ledgers 17 | module DSL 18 | end # module DSL 19 | end # module XRBP 20 | 21 | require_relative './dsl/websocket' 22 | require_relative './dsl/webclient' 23 | require_relative './dsl/accounts' 24 | require_relative './dsl/ledgers' 25 | require_relative './dsl/validators' 26 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/multi/round_robin.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | # MultiConnection strategy where connections selected in 4 | # a circular-round robin manner, where the next connection 5 | # is always used for the next request even if the current 6 | # one succeeds. 7 | class RoundRobin < MultiConnection 8 | def initialize(*urls) 9 | super(*urls) 10 | @current = 0 11 | end 12 | 13 | def next_connection(prev=nil) 14 | return nil unless prev.nil? 15 | 16 | c = connections[@current] 17 | @current += 1 18 | @current = 0 if @current >= connections.size 19 | c 20 | end 21 | end # class RoundRobin 22 | end # module WebSocket 23 | end # module XRBP 24 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/account_tx.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The account_tx method retrieves a list of transactions 5 | # that involved the specified account. 6 | # 7 | # https://developers.ripple.com/account_tx.html 8 | class AccountTx < Command 9 | attr_accessor :account, :args 10 | 11 | def initialize(account, args={}) 12 | @account = account 13 | @args = args 14 | super(to_h) 15 | end 16 | 17 | def to_h 18 | args.merge(:command => :account_tx, 19 | :account => account) 20 | end 21 | end # class AccountLines 22 | end # module Cmds 23 | end # module WebSocket 24 | end # module Wipple 25 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/account_info.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The account_info command retrieves information about an 5 | # account, its activity, and its XRP balance. 6 | # 7 | # https://developers.ripple.com/account_info.html 8 | class AccountInfo < Command 9 | attr_accessor :account, :args 10 | 11 | def initialize(account, args={}) 12 | @account = account 13 | @args = args 14 | super(to_h) 15 | end 16 | 17 | def to_h 18 | args.merge(:command => :account_info, 19 | :account => account) 20 | end 21 | end # class AccountInfo 22 | end # module Cmds 23 | end # module WebSocket 24 | end # module Wipple 25 | -------------------------------------------------------------------------------- /spec/helpers/test_handshake.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # XXX need to manually set handshake, 3 | # else will be different each time, 4 | # messing up recordings 5 | class TestHandshake 6 | def version 7 | 13 8 | end 9 | 10 | def to_s 11 | "GET / HTTP/1.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nHost: s1.ripple.com:443\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: 0tTEDtzkyx8JPC2rIYYIzA==\r\n\r\n" 12 | end 13 | 14 | def finished? 15 | true 16 | end 17 | 18 | def <<(r) 19 | end 20 | end 21 | 22 | module WebSocket 23 | class Client 24 | def stub_handshake! 25 | @handshake = XRBP::TestHandshake.new 26 | end 27 | end # class Client 28 | end # module WebSocket 29 | end # module XRBP 30 | -------------------------------------------------------------------------------- /lib/xrbp/model/parsers/validator.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | # @private 4 | module Parsers 5 | # Validator list data parser 6 | # 7 | # @private 8 | class Validator < PluginBase 9 | def parser_priority 10 | 0 11 | end 12 | 13 | def parse_result(res, req) 14 | JSON.parse(res)["validators"].collect { |v| 15 | id = v["validation_public_key"] 16 | next nil unless id 17 | 18 | {:id => id, 19 | :domain => v["domain"], 20 | :chain => v["chain"], 21 | :unl => v["unl"]} 22 | }.compact 23 | end 24 | end # class ValidatorParser 25 | end # module Parsers 26 | end # module Model 27 | end # module XRBP 28 | -------------------------------------------------------------------------------- /lib/xrbp/plugins/has_result_parsers.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # Helper mixing providing result parser management capabilities. 3 | # 4 | # @private 5 | module HasResultParsers 6 | def parsing_plugins 7 | raise 8 | end 9 | 10 | def parse_result(res, req) 11 | _res = res 12 | 13 | prioritized = parsing_plugins.select { |p| 14 | p != self && p.respond_to?(:parse_result) 15 | 16 | }.sort { |p1, p2| 17 | (p1.respond_to?(:parser_priority) ? p1.parser_priority : 1) <=> 18 | (p2.respond_to?(:parser_priority) ? p2.parser_priority : 1) 19 | } 20 | 21 | prioritized.each { |plg| 22 | _res = plg.parse_result(_res, req) 23 | } 24 | _res 25 | end 26 | end # module HasResultParsers 27 | end # module XRBP 28 | -------------------------------------------------------------------------------- /lib/xrbp/webclient/plugins/result_parser.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebClient 3 | module Plugins 4 | # Plugin to automatically parse and convert webclient results, 5 | # before returning. 6 | # 7 | # @example parse json 8 | # connection = WebClient::Connection.new 9 | # connection.add_plugin :result_parser 10 | # 11 | # connection.parse_results do |res| 12 | # JSON.parse(res) 13 | # end 14 | # 15 | # connection.url = "https://data.ripple.com/v2/gateways" 16 | # connection.perform 17 | class ResultParser < ResultParserBase 18 | end 19 | 20 | WebClient.register_plugin :result_parser, ResultParser 21 | end # module Plugins 22 | end # module WebClient 23 | end # module XRBP 24 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/multi/parallel.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | # MultiConnection strategy where requests are sent to 4 | # all connections in parallel. 5 | class Parallel < MultiConnection 6 | class All 7 | attr_accessor :connections 8 | 9 | def initialize(connections) 10 | @connections = connections 11 | end 12 | 13 | def method_missing(m, *args, &bl) 14 | connections.collect { |c| 15 | c.send(m, *args, &bl) if c.open? 16 | } 17 | end 18 | end # class All 19 | 20 | def next_connection(prev=nil) 21 | return nil unless prev.nil? 22 | All.new connections 23 | end 24 | end # class RoundRobin 25 | end # module WebSocket 26 | end # module XRBP 27 | -------------------------------------------------------------------------------- /spec/xrbp/crypto/key_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::Crypto::Key do 2 | it "generates valid sec256k1 key" 3 | it "generates valid ed25519 key" 4 | 5 | [:secp256k1, :ed25519].each { |key_type| 6 | 7 | let(:dat) { "ABCDEFGHIJLMNOPQRSTUVWYZ12345678" } 8 | 9 | it "generates signs and verifies #{key_type} digest" do 10 | key = described_class.send(key_type) 11 | signed = XRBP::Crypto::Key.sign_digest(key, dat) 12 | expect(XRBP::Crypto::Key.verify(key, signed, dat)) 13 | end 14 | 15 | it "accepts #{key_type} seed" do 16 | seed = XRBP::Crypto.seed[:seed] 17 | key = described_class.send(key_type, seed) 18 | signed = XRBP::Crypto::Key.sign_digest(key, dat) 19 | expect(XRBP::Crypto::Key.verify(key, signed, dat)) 20 | end 21 | } 22 | end 23 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/shamap/node_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::SHAMap::Node do 2 | it "is not a tree_node?" do 3 | expect(described_class.new).to_not be_a_tree_node 4 | end 5 | 6 | it "is not an inner?" do 7 | expect(described_class.new).to_not be_an_inner 8 | end 9 | 10 | it "does not allow hash to be updated" do 11 | expect { described_class.new.update_hash }.to raise_error("abstract: must be called on a subclass") 12 | end 13 | 14 | context "leaf type" do 15 | it "is a leaf?" do 16 | expect(described_class.new(:type => :transaction_nm)).to be_a_leaf 17 | end 18 | end 19 | 20 | context "non leaf type" do 21 | it "is not a leaf?" do 22 | expect(described_class.new(:type => :infer)).to_not be_a_leaf 23 | end 24 | end 25 | end # describe XRBP::SHAMap::Node 26 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/ledger.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # Retrieve information about the public ledger 5 | # 6 | # https://developers.ripple.com/ledger.html 7 | class Ledger < Command 8 | attr_accessor :ledger_index, :args 9 | 10 | def initialize(ledger_index=nil, args={}) 11 | @ledger_index = ledger_index 12 | @args = args 13 | super(to_h) 14 | end 15 | 16 | def ledger_index? 17 | !!ledger_index 18 | end 19 | 20 | def to_h 21 | h = args.merge(:command => :ledger) 22 | h['ledger_index'] = ledger_index if ledger_index? 23 | return h 24 | end 25 | end 26 | end # module Cmds 27 | end # module WebSocket 28 | end # module Wipple 29 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/backends/nudb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'xrbp/nodestore/backends/nudb' 2 | 3 | require_relative "./db_access" 4 | #require_relative "./db_iterator_access" 5 | #require_relative "./db_decompression" 6 | require_relative "../db_parser" 7 | require_relative "../ledger_access" 8 | 9 | describe XRBP::NodeStore::Backends::NuDB do 10 | before(:each) do 11 | Camcorder.intercept_constructor ::RuDB::Store 12 | end 13 | 14 | after(:each) do 15 | Camcorder.deintercept_constructor ::RuDB::Store 16 | end 17 | 18 | let(:db) { 19 | XRBP::NodeStore::Backends::NuDB.new("/var/lib/rippled/nudb") 20 | } 21 | 22 | it_provides "database access" 23 | # it_provides "database iterator access" 24 | # it_provides "database decompression" 25 | it_provides "a database parser" 26 | it_provides "ledger access" 27 | end 28 | -------------------------------------------------------------------------------- /examples/crawl_nodes.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | connection = XRBP::WebClient::Connection.new 5 | connection.timeout = 3 6 | 7 | Signal.trap("INT") { 8 | connection.force_quit! 9 | } 10 | 11 | connection.on :precrawl do |node| 12 | puts "Crawling: #{node.url}" 13 | end 14 | 15 | connection.on :crawlerr do |node| 16 | puts "Could not Crawl: #{node.url}" 17 | end 18 | 19 | connection.on :postcrawl do |node| 20 | puts "Done Crawling: #{node.url}" 21 | end 22 | 23 | connection.on :peers do |node, peers| 24 | puts "#{node.url}: #{peers.size} peers" 25 | end 26 | 27 | connection.on :peer do |node, peer| 28 | print " #{peer.url.ljust(40)} - Ledgers: #{peer.ledgers}\n" 29 | end 30 | 31 | XRBP::Model::Node.crawl("wss://s1.ripple.com:51235", 32 | :connection => connection) 33 | -------------------------------------------------------------------------------- /lib/xrbp/model/parsers/market.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | # @private 4 | module Parsers 5 | # Market List data parser 6 | # 7 | # @private 8 | class Market < PluginBase 9 | def parser_priority 10 | 0 11 | end 12 | 13 | def parse_result(res, req) 14 | j = JSON.parse(res) 15 | return res unless j["result"] && 16 | j["result"]["markets"] && 17 | j["result"]["markets"]["base"] 18 | j["result"]["markets"]["base"] 19 | .collect { |market| 20 | {:exchange => market["exchange"], 21 | :currency => market["pair"][3..-1], 22 | :route => market["route"]} 23 | } 24 | end 25 | end 26 | end # module Parsers 27 | end # module Model 28 | end # module XRBP 29 | -------------------------------------------------------------------------------- /lib/xrbp/model/gateway.rb: -------------------------------------------------------------------------------- 1 | require_relative './parsers/gateway' 2 | 3 | module XRBP 4 | module Model 5 | class Gateway < Base 6 | extend Base::ClassMethods 7 | 8 | # Retrieve list of gateways provided WebClient::Connection. 9 | # 10 | # @param opts [Hash] options to retrieve gateway list with 11 | # @option opts [WebClient::Connection] :connection Connection 12 | # to use to retrieve gateway list 13 | def self.all(opts={}) 14 | set_opts(opts) 15 | connection.url = "https://data.ripple.com/v2/gateways" 16 | 17 | connection.add_plugin :result_parser unless connection.plugin?(:result_parser) 18 | connection.add_plugin Parsers::Gateway unless connection.plugin?(Parsers::Gateway) 19 | 20 | connection.perform 21 | end 22 | end # class Gateway 23 | end # module Model 24 | end # module XRBP 25 | -------------------------------------------------------------------------------- /lib/xrbp/model/validator.rb: -------------------------------------------------------------------------------- 1 | require_relative './parsers/validator' 2 | 3 | module XRBP 4 | module Model 5 | class Validator < Base 6 | extend Base::ClassMethods 7 | 8 | # Retrieve list of validators via WebClient::Connection 9 | # 10 | # @param opts [Hash] options to retrieve validator list with 11 | # @option opts [WebClient::Connection] :connection Connection 12 | # to use to retrieve validator list 13 | def self.all(opts={}) 14 | set_opts(opts) 15 | connection.url = "https://data.ripple.com/v2/network/validators/" 16 | 17 | connection.add_plugin :result_parser unless connection.plugin?(:result_parser) 18 | connection.add_plugin Parsers::Validator unless connection.plugin?(Parsers::Validator) 19 | 20 | connection.perform 21 | end 22 | end # class Validator 23 | end # module Model 24 | end # module XRBP 25 | -------------------------------------------------------------------------------- /lib/xrbp/dsl/websocket.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module DSL 3 | # Default websocket endpoints. Override to specify 4 | # different ones. 5 | def websocket_endpoints 6 | ["wss://s1.ripple.com:443", "wss://s2.ripple.com:443"] 7 | end 8 | 9 | # Client which may be used to access websocket endpoints. 10 | # 11 | # By default a RoundRobin strategy will be used to cycle 12 | # through specified endpoints. 13 | def websocket 14 | @websocket ||= WebSocket::RoundRobin.new *websocket_endpoints 15 | end 16 | 17 | # Register a callback to be invoked when messages are received 18 | # via websocket connections 19 | def websocket_msg(&bl) 20 | websocket.on :message, &bl 21 | end 22 | 23 | # Block until all websocket connections are closed 24 | def websocket_wait 25 | websocket.wait_for_close 26 | end 27 | end # module DSL 28 | end # module XRBP 29 | -------------------------------------------------------------------------------- /lib/xrbp/model/base.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | # Base model definition, provides common logic to set connection & opts. 4 | class Base 5 | module ClassMethods 6 | attr_accessor :opts, :connection 7 | 8 | def set_opts(opts={}) 9 | self.opts ||= {} 10 | self.opts.merge!(opts) 11 | self.connection = opts[:connection] if opts[:connection] 12 | end 13 | end 14 | 15 | attr_accessor :opts, :connection 16 | 17 | def initialize(opts={}) 18 | set_opts(opts) 19 | end 20 | 21 | def set_opts(opts={}) 22 | @opts ||= {} 23 | @opts.merge!(opts) 24 | @connection = opts[:connection] if opts[:connection] 25 | end 26 | 27 | def full_opts 28 | (self.class.opts || {}).merge(opts || {}).except(:connection) 29 | end 30 | end # class Base 31 | end # module Model 32 | end # module XRBP 33 | -------------------------------------------------------------------------------- /spec/xrbp/crypto/node_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::Crypto do 2 | let(:node) { "n9Kgda1F7evck24hJUqFsn8m6pNERJd6UkjwLuNBSUJ6Ykm76NdJ" } 3 | let(:expected) { [0x2, 0x96, 0x24, 0xe4, 0x37, 0x80, 0xeb, 0xcc, 0xf2, 0x77, 0x67, 0x48, 0x4d, 0x42, 0xc5, 0x54, 0xbe, 0x7b, 0xaa, 0x28, 0x78, 0x82, 0x7e, 0x91, 0x2f, 0x4e, 0x66, 0x94, 0xbf, 0x64, 0xa4, 0x55, 0x80].pack("C*") } 4 | 5 | it "generates valid node" do 6 | node = described_class.node 7 | expect(described_class.node?(node[:node])).to be(true) 8 | end 9 | 10 | it "parses node" do 11 | n = described_class.parse_node(node) 12 | expect(n).to_not be_nil 13 | expect(n).to eq(expected) 14 | end 15 | 16 | it "does not parse invalid node" do 17 | expect(described_class.parse_node("invalid")).to be_nil 18 | end 19 | 20 | it "verifies node" do 21 | expect(described_class.node?(node)).to be(true) 22 | expect(described_class.node?("invalid")).to be(false) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/xrbp/websocket/message_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe XRBP::WebSocket::Message do 4 | subject { described_class.new 'Test message' } 5 | let(:connection) { XRBP::WebSocket::Connection.new "*" } 6 | 7 | before(:each) do 8 | subject.connection = connection 9 | end 10 | 11 | it 'returns message text' do 12 | expect(subject.to_s).to eq 'Test message' 13 | end 14 | 15 | it 'waits for signal' do 16 | Thread.new { 17 | sleep(0.1) 18 | subject.signal 19 | } 20 | subject.wait 21 | end 22 | 23 | it 'waits for connection close' do 24 | expect(connection).to receive(:closed?).and_return true 25 | expect(subject.wait).to be_nil 26 | end 27 | 28 | describe "#bl" do 29 | it 'defaults to callback which signals wait condition' do 30 | Thread.new { 31 | sleep(0.1) 32 | subject.bl.call 33 | } 34 | subject.wait 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/xrbp/websocket/plugins/cmd_dispatcher_spec.rb: -------------------------------------------------------------------------------- 1 | #describe XRBP::WebSocket::Plugins::CommandDispatcher do 2 | # let(:connection) { 3 | # XRBP::WebSocket::Connection.new "wss://s1.ripple.com:443" do |c| 4 | # c.add_plugin :command_dispatcher 5 | # end 6 | # } 7 | # 8 | # before(:each) do 9 | # #connection.client.stub_handshake! 10 | # connection.client.force_serializable! 11 | # Camcorder.intercept_constructor XRBP::WebSocket::Socket 12 | # 13 | # # XXX fix random 14 | # allow(handshake.instance_variable_get(:@handler)).to receive(:rand).with(255).and_return(100) 15 | # #allow(SecureRandom).to receive(:random_bytes).with(4).and_return('1234') 16 | # end 17 | # 18 | # after(:each) do 19 | # Camcorder.deintercept_constructor XRBP::WebSocket::Socket 20 | # end 21 | # 22 | # it "sends command and returns response" do 23 | # connection.connect 24 | # puts connection.cmd XRBP::WebSocket::Cmds::ServerInfo.new 25 | # end 26 | #end 27 | -------------------------------------------------------------------------------- /examples/nodestore1.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | # for rocksdb: 5 | require 'xrbp/nodestore/backends/rocksdb' 6 | db = XRBP::NodeStore::Backends::RocksDB.new "/var/lib/rippled/rocksdb/rippledb.0899" 7 | 8 | # for nudb: 9 | #require 'xrbp/nodestore/backends/nudb' 10 | #db = XRBP::NodeStore::Backends::NuDB.new "/var/lib/rippled/nudb/" 11 | 12 | ledger = "32E073D7E4D722D956F7FDE095F756FBB86DC9CA487EB0D9ABF5151A8D88F912" 13 | ledger = [ledger].pack("H*") 14 | puts db.ledger(ledger) 15 | 16 | gw1 = 'razqQKzJRdB4UxFPWf5NEpEG3WMkmwgcXA' 17 | iou1 = {:currency => 'XRP', :account => XRBP::Crypto.xrp_account} 18 | iou2 = {:currency => 'CNY', :account => gw1} 19 | nledger = XRBP::NodeStore::Ledger.new(:db => db, :hash => ledger) 20 | puts nledger.order_book iou1, iou2 21 | puts nledger.txs 22 | 23 | require 'xrbp/nodestore/sqldb' 24 | sql = XRBP::NodeStore::SQLDB.new("/var/lib/rippled/nudb") 25 | puts sql.ledgers.hash_for_seq(49340234) 26 | puts sql.ledgers.count 27 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/protocol/rate.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | # Represents a transfer rate. 4 | # 5 | # The percent of an amount sent that is charged 6 | # to the sender and paid to the issuer. 7 | # 8 | # https://xrpl.org/transfer-fees.html 9 | # 10 | # From rippled docs: 11 | # Transfer rates are specified as fractions of 1 billion. 12 | # For example, a transfer rate of 1% is represented as 13 | # 1,010,000,000. 14 | class Rate 15 | attr_reader :rate 16 | 17 | def initialize(rate=nil) 18 | @rate = rate 19 | end 20 | 21 | # Rate signifying a 1:1 exchange 22 | def self.parity 23 | @parity ||= Rate.new(QUALITY_ONE) 24 | end 25 | 26 | def to_amount 27 | STAmount.new :issue => NodeStore.no_issue, 28 | :mantissa => rate, 29 | :exponent => -9 30 | end 31 | end # class Rate 32 | end # module NodeStore 33 | end # module XRBP 34 | -------------------------------------------------------------------------------- /lib/xrbp/dsl/ledgers.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module DSL 3 | # Return ledger object for the specified id 4 | # 5 | # @param id [Integer] id of the ledger to query 6 | # @return [Hash, nil] the ledger object retrieved or nil otherwise 7 | def ledger(id=nil) 8 | websocket.add_plugin :autoconnect unless websocket.plugin?(:autoconnect) 9 | websocket.add_plugin :command_dispatcher unless websocket.plugin?(:command_dispatcher) 10 | websocket.cmd(WebSocket::Cmds::Ledger.new(id)) 11 | end 12 | 13 | # Subscribed to the ledger stream. 14 | # 15 | # After calling this, :ledger events will be emitted via the 16 | # websocket connection object. 17 | def subscribe_to_ledgers 18 | websocket.add_plugin :autoconnect unless websocket.plugin?(:autoconnect) 19 | websocket.add_plugin :command_dispatcher unless websocket.plugin?(:command_dispatcher) 20 | websocket.cmd(WebSocket::Cmds::Subscribe.new(:streams => ["ledger"])) 21 | end 22 | end # module DSL 23 | end # module XRBP 24 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/sle/st_amount_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::STAmount do 2 | it "provides access to #zero amount" 3 | 4 | it "returns amount from quality" 5 | 6 | context "quality rate = 0" do 7 | it "returns default STAmount" 8 | end 9 | 10 | describe "default STAmount" do 11 | describe "issue" do 12 | it "is nil" 13 | end 14 | 15 | describe "mantissa" do 16 | it "is zero" 17 | end 18 | 19 | describe "exponent" do 20 | it "is zero" 21 | end 22 | 23 | it "is not negative" 24 | end 25 | 26 | it "canonicalizes data" 27 | 28 | describe "#native?" do 29 | context "native issuer" do 30 | it "returns true" 31 | end 32 | 33 | context "non native issuer" do 34 | it "returns false" 35 | end 36 | end 37 | 38 | describe "zero?" do 39 | context "mantissa is zero" do 40 | it "returns true" 41 | end 42 | 43 | context "mantissa is not zero" do 44 | it "returns false" 45 | end 46 | end 47 | end # describe XRBP::NodeStore::STAmount 48 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/shamap/inner_node_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::SHAMap::InnerNode do 2 | context "no branches" do 3 | it "is empty" 4 | 5 | it "always returns true empty_branch?" 6 | 7 | it "always returns nil child hash" 8 | 9 | it "always returns nil child" 10 | end 11 | 12 | context "child branches" do 13 | before(:each) do 14 | end 15 | 16 | it "is not empty" 17 | 18 | context "querying for non-empty child" do 19 | it "returns false empty_branch?" 20 | 21 | it "it returns child hash" 22 | 23 | it "returns child" 24 | end 25 | 26 | context "querying for empty child" do 27 | it "returns true empty_branch?" 28 | 29 | it "returns nil child hash" 30 | 31 | it "returns nil child" 32 | end 33 | 34 | context "querying for invalid branch" do 35 | it "always return true empty_child?" 36 | 37 | it "raises error when retrieving child hash" 38 | 39 | it "raises error when retrieving child" 40 | end 41 | end 42 | 43 | it "updates hash" 44 | end # describe XRBP::SHAMap::InnerNode 45 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/plugins/result_parser.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Plugins 4 | # Plugin to automatically parse and convert websocket results, 5 | # before returning. 6 | # 7 | # @example parse json 8 | # connection = WebClient::Connection.new "wss://s1.ripple.com:443" 9 | # connection.add_plugin :command_dispatcher, :result_parser 10 | # 11 | # connection.parse_results do |res| 12 | # JSON.parse(res) 13 | # end 14 | # 15 | # puts connection.cmd(WebSocket::Cmds::ServerInfo.new)["result"]["info"]["build_version"] 16 | class ResultParser < ResultParserBase 17 | def parser=(p) 18 | super(p) 19 | 20 | self.connection.connections.each { |conn| 21 | conn.parse_results &p 22 | } if self.connection.kind_of?(MultiConnection) 23 | 24 | p 25 | end 26 | end # class ResultParser 27 | 28 | WebSocket.register_plugin :result_parser, ResultParser 29 | end # module Plugins 30 | end # module WebSocket 31 | end # module XRBP 32 | -------------------------------------------------------------------------------- /spec/xrbp/crypto/account_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::Crypto do 2 | let(:account) { "rDbWJ9C7uExThZYAwV8m6LsZ5YSX3sa6US" } 3 | let(:account_id) { 788735140293854337814932116604999410196843811141 } 4 | let(:parsed) { [0x8a, 0x28, 0x1b, 0x5e, 0x46, 0xb0, 0x27, 0xc3, 0x70, 0x26, 0xe3, 0x8d, 0xbc, 0x5f, 0x9a, 0xa1, 0x4c, 0x37, 0x51, 0x45].pack("C*") } 5 | 6 | it "generates valid account" do 7 | acct = described_class.account 8 | expect(described_class.account?(acct[:account])).to be(true) 9 | end 10 | 11 | it "returns account id for account" do 12 | expect(described_class.account_id(account).to_bn).to eq(account_id) 13 | end 14 | 15 | it "parses account" do 16 | a = described_class.parse_account(account) 17 | expect(a).to_not be_nil 18 | expect(a).to eq(parsed) 19 | end 20 | 21 | it "does not parse invalid account" do 22 | expect(described_class.parse_account("invalid")).to be_nil 23 | end 24 | 25 | it "verifies account" do 26 | expect(described_class.account?(account)).to be(true) 27 | expect(described_class.account?("invalid")).to be(false) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/account_objects.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The account_objects command returns the raw ledger format for 5 | # all objects owned by an account 6 | # 7 | # https://developers.ripple.com/account_objects.html 8 | class AccountObjects < Command 9 | include Paginated 10 | 11 | def page_title 12 | "account_objects" 13 | end 14 | 15 | attr_accessor :account, :args 16 | 17 | def initialize(account, args={}) 18 | @account = account 19 | @args = args 20 | parse_paginate(args) 21 | super(to_h) 22 | end 23 | 24 | def self.from_h(h) 25 | _h = Hash[h] 26 | a = _h.delete(:account) 27 | new a, _h 28 | end 29 | 30 | def to_h 31 | args_without_paginate.merge(:command => :account_objects, 32 | :account => account) 33 | end 34 | end # class AccountLines 35 | end # module Cmds 36 | end # module WebSocket 37 | end # module Wipple 38 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/message.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | # Raw data which to write to websocket and mechanisms 4 | # which to track and manage response state. 5 | class Message 6 | attr_reader :result 7 | attr_accessor :time, :connection 8 | 9 | def initialize(data) 10 | @data = data 11 | @result = nil 12 | @cv = ConditionVariable.new 13 | @signalled = false 14 | @time = Time.now 15 | end 16 | 17 | def to_s 18 | @data 19 | end 20 | 21 | def signal 22 | @signalled = true 23 | @cv.signal 24 | self 25 | end 26 | 27 | def wait 28 | connection.state_mutex.synchronize { 29 | # only wait if we haven't received response 30 | @cv.wait(connection.state_mutex) unless connection.closed? || @signalled 31 | } 32 | end 33 | 34 | attr_writer :bl 35 | 36 | def bl 37 | @bl ||= proc { |res| 38 | @result = res 39 | signal 40 | } 41 | end 42 | end # class Message 43 | end # module WebSocket 44 | end # module XRBP 45 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/account_lines.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The account_lines method returns information about an 5 | # account's trust lines, including balances in all non-XRP 6 | # currencies and assets 7 | # 8 | # https://developers.ripple.com/account_lines.html 9 | class AccountLines < Command 10 | include Paginated 11 | 12 | def page_title 13 | "account_lines" 14 | end 15 | 16 | attr_accessor :account, :args 17 | 18 | def initialize(account, args={}) 19 | @account = account 20 | @args = args 21 | parse_paginate(args) 22 | super(to_h) 23 | end 24 | 25 | def self.from_h(h) 26 | _h = Hash[h] 27 | a = _h.delete(:account) 28 | new a, _h 29 | end 30 | 31 | def to_h 32 | args_without_paginate.merge(:command => :account_lines, 33 | :account => account) 34 | end 35 | end # class AccountLines 36 | end # module Cmds 37 | end # module WebSocket 38 | end # module Wipple 39 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/account_offers.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The account_offers method retrieves a list of offers made 5 | # by a given account that are outstanding as of a particular 6 | # ledger version. 7 | # 8 | # https://developers.ripple.com/account_offers.html 9 | class AccountOffers < Command 10 | include Paginated 11 | 12 | def page_title 13 | "offers" 14 | end 15 | 16 | attr_accessor :account, :args 17 | 18 | def initialize(account, args={}) 19 | @account = account 20 | @args = args 21 | parse_paginate(args) 22 | super(to_h) 23 | end 24 | 25 | def self.from_h(h) 26 | _h = Hash[h] 27 | a = _h.delete(:account) 28 | new a, _h 29 | end 30 | 31 | def to_h 32 | args_without_paginate.merge(:command => :account_offers, 33 | :account => account) 34 | end 35 | end # class AccountLines 36 | end # module Cmds 37 | end # module WebSocket 38 | end # module Wipple 39 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/node.rb: -------------------------------------------------------------------------------- 1 | require_relative './node_factory' 2 | 3 | module XRBP 4 | class SHAMap 5 | # Base Node class, all entries stored in tree structures 6 | # in nodestore DB inherit from this class 7 | class Node 8 | extend NodeFactory 9 | 10 | attr_accessor :hash 11 | 12 | TYPES = { 13 | :error => 0, 14 | :infer => 1, 15 | :transaction_nm => 2, 16 | :transaction_md => 3, 17 | :account_state => 4 18 | } 19 | 20 | LEAF_TYPES = [ 21 | :transaction_nm, 22 | :transaction_md, 23 | :account_state 24 | ] 25 | 26 | def initialize(args={}) 27 | @hash = args[:hash] 28 | @type = args[:type] 29 | @seq = args[:seq] 30 | end 31 | 32 | def leaf? 33 | LEAF_TYPES.include?(@type) 34 | end 35 | 36 | def inner? 37 | false 38 | end 39 | 40 | def tree_node? 41 | false 42 | end 43 | 44 | def update_hash 45 | raise "abstract: must be called on a subclass" 46 | end 47 | end # class Node 48 | end # class SHAMap 49 | end # module XRBP 50 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Dev Null Productions 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sle/st_object.rb: -------------------------------------------------------------------------------- 1 | require_relative '../parser' 2 | 3 | module XRBP 4 | module NodeStore 5 | # Seralized Type containing fields associated with ids 6 | class STObject 7 | # NodeStore Parser 8 | include Parser 9 | 10 | attr_reader :item, :fields 11 | 12 | def initialize(args={}) 13 | @item = args[:item] 14 | 15 | @fields, remaining = parse_fields(item.data) 16 | raise unless remaining.size == 0 17 | end 18 | 19 | def flags 20 | @flags ||= fields[:flags] 21 | end 22 | 23 | def flag?(flag) 24 | flag = NodeStore::Format::SERIALIZED_FLAGS[flag] if flag.is_a?(Symbol) 25 | flags & flag == flag 26 | end 27 | 28 | def field?(id) 29 | fields.key?(id) 30 | end 31 | 32 | def field(type, id) 33 | fields[id] 34 | # type should already be converted in parsing process (TODO verify?) 35 | end 36 | 37 | def amount(field) 38 | field(STAmount, field) 39 | end 40 | 41 | def account_id(field) 42 | field(STAccount, field) 43 | end 44 | end # class STObject 45 | end # module NodeStore 46 | end # module XRBP 47 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/book_offers.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Cmds 4 | # The book_offers method retrieves a list of offers, also known as 5 | # the order book , between two currencies 6 | # 7 | # https://developers.ripple.com/book_offers.html 8 | class BookOffers < Command 9 | include Paginated 10 | 11 | def page_title 12 | "offers" 13 | end 14 | 15 | attr_accessor :args 16 | 17 | def initialize(args={}) 18 | @args = args 19 | parse_paginate(args) 20 | super(to_h) 21 | end 22 | 23 | def self.from_h(h) 24 | new Hash[h] 25 | end 26 | 27 | def sanitized_args 28 | sa = Hash[args_without_paginate] 29 | 30 | sa[:taker_gets].delete(:issuer) if sa[:taker_gets][:currency] == "XRP" 31 | sa[:taker_pays].delete(:issuer) if sa[:taker_pays][:currency] == "XRP" 32 | 33 | sa 34 | end 35 | 36 | def to_h 37 | sanitized_args.merge(:command => :book_offers) 38 | end 39 | end # class BookOffers 40 | end # module Cmds 41 | end # module WebSocket 42 | end # module Wipple 43 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/protocol/issue.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | class Issue 4 | attr_reader :currency, :account 5 | 6 | def initialize(currency, account) 7 | @currency = currency 8 | @account = account 9 | end 10 | 11 | def to_h 12 | {:currency => currency, :account => account} 13 | end 14 | 15 | def xrp? 16 | self == NodeStore.xrp_issue 17 | end 18 | 19 | def inspect 20 | c = currency == NodeStore.no_currency ? '' : 21 | currency == NodeStore.xrp_currency ? 'XRP' : 22 | "#{currency}" 23 | 24 | a = account == Crypto.no_account ? '' : 25 | account == Crypto.xrp_account ? '' : 26 | "@#{account}" 27 | "#{c}#{a}" 28 | end 29 | end # class Issue 30 | 31 | def self.xrp_issue 32 | @xrp_issue ||= Issue.new(NodeStore.xrp_currency, 33 | Crypto.xrp_account) 34 | end 35 | 36 | def self.no_issue 37 | @no_issue ||= Issue.new(NodeStore.no_currency, 38 | Crypto.no_account) 39 | end 40 | end # module NodeStore 41 | end # module XRBP 42 | -------------------------------------------------------------------------------- /lib/xrbp/model/parsers/account.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | # @private 4 | module Parsers 5 | # Account Info data parser 6 | # 7 | # @private 8 | class AccountInfo < PluginBase 9 | def parser_priority 10 | 0 11 | end 12 | 13 | def parse_result(res, req) 14 | j = JSON.parse(res) 15 | # return res unless j.key?("accounts") 16 | 17 | accts = (j["accounts"] || []).collect { |a| 18 | {:id => a["account"], 19 | :inception => a["inception"], 20 | :parent_id => a["parent"]} 21 | } 22 | 23 | {:marker => j["marker"], 24 | :accounts => accts} 25 | end 26 | end 27 | 28 | # Account Username data parser 29 | # 30 | # @private 31 | class AccountUsername < PluginBase 32 | def parser_priority 33 | 0 34 | end 35 | 36 | def parse_result(res, req) 37 | j = JSON.parse(res) 38 | # return res unless j.key?("exists") 39 | j["exists"] ? j["username"] : nil 40 | end 41 | end 42 | end # module Parsers 43 | end # module Model 44 | end # module XRBP 45 | -------------------------------------------------------------------------------- /examples/nodestore2.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../../lib', __FILE__) 2 | require 'xrbp' 3 | 4 | # for rocksdb: 5 | require 'xrbp/nodestore/backends/rocksdb' 6 | db = XRBP::NodeStore::Backends::RocksDB.new "/var/lib/rippled/rocksdb/rippledb.0899" 7 | 8 | # for nudb: 9 | #require 'xrbp/nodestore/backends/nudb' 10 | #db = XRBP::NodeStore::Backends::NuDB.new "/var/lib/rippled/nudb/" 11 | 12 | db.on :unknown do |hash, node| 13 | puts "Unknown #{hash}: #{node}" 14 | end 15 | 16 | db.on :inner_node do |hash, node| 17 | #puts "Inner Node #{hash}" 18 | end 19 | 20 | db.on :ledger do |hash, ledger| 21 | #puts "Ledger #{ledger['index']}" 22 | end 23 | 24 | db.on :tx do |hash, tx| 25 | puts "Tx #{tx}" 26 | end 27 | 28 | db.on :account do |hash, account| 29 | #puts "Account #{account}" 30 | end 31 | 32 | ### 33 | 34 | tallys = {} 35 | 36 | # object iterator invokes event emitters 37 | db.each do |node| 38 | obj = XRBP::NodeStore::Format::TYPE_INFER.decode(node.value) 39 | node_type = XRBP::NodeStore::Format::NODE_TYPES[obj["node_type"]] 40 | hash_prefix = XRBP::NodeStore::Format::HASH_PREFIXES[obj["hash_prefix"].upcase] 41 | 42 | type = node_type.to_s + "/" + hash_prefix.to_s 43 | tallys[type] ||= 0 44 | tallys[type] += 1 45 | end 46 | 47 | puts tallys 48 | -------------------------------------------------------------------------------- /lib/xrbp/plugins/has_plugin.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # Helper mixin provding plugin management capabilities. 3 | # 4 | # @private 5 | module HasPlugin 6 | # should be overridden 7 | def plugin_namespace 8 | raise 9 | end 10 | 11 | def plugins 12 | @plugins ||= [] 13 | end 14 | 15 | def add_plugin(*plgs) 16 | plgs.each { |plg| 17 | plg = plugin_namespace.plugins[plg] if plg.is_a?(Symbol) 18 | raise ArgumentError unless !!plg 19 | plg = plg.new(self) 20 | plugins << plg 21 | plg.added if plg.respond_to?(:added) 22 | } 23 | end 24 | 25 | def plugin?(plg) 26 | clss = plugins.collect { |plg| plg.class } 27 | cls = plugin_namespace.plugins[plg] 28 | clss.include?(plg) || clss.include?(cls) 29 | end 30 | 31 | def plugin(plg) 32 | cls = plugin_namespace.plugins[plg] 33 | plugins.find { |_plg| 34 | (plg.is_a?(Class) && _plg.kind_of?(plg)) || 35 | (cls.is_a?(Class) && _plg.kind_of?(cls)) 36 | } 37 | end 38 | 39 | def define_instance_method(name, &block) 40 | (class << self; self; end).class_eval do 41 | define_method name, &block 42 | end 43 | end 44 | end # module HasPlugin 45 | end # module XRBP 46 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/cmds/paginated.rb: -------------------------------------------------------------------------------- 1 | require 'xrbp/core_ext' 2 | 3 | module XRBP 4 | module WebSocket 5 | module Cmds 6 | # Helper mixin facilitating paginated command retrieval. 7 | # 8 | # @private 9 | module Paginated 10 | attr_reader :prev_cmd 11 | 12 | def root_cmd 13 | return self unless prev_cmd 14 | prev_cmd.root_cmd 15 | end 16 | 17 | def each_ancestor(&bl) 18 | bl.call self 19 | prev_cmd.each_ancestor &bl if prev_cmd 20 | end 21 | 22 | def parse_paginate(args) 23 | @paginate = args[:paginate] 24 | @prev_cmd = args[:prev_cmd] 25 | end 26 | 27 | def paginate_args 28 | return :prev_cmd #, :paginate # XXX need to forward paginate 29 | end 30 | 31 | def args_without_paginate 32 | args.except(*paginate_args) 33 | end 34 | 35 | def paginate? 36 | !!@paginate 37 | end 38 | 39 | def next_page(marker) 40 | self.class.from_h(to_h.merge({:marker => marker, 41 | :prev_cmd => self})) 42 | end 43 | end # module Paginated 44 | end # module Cmds 45 | end # module WebSocket 46 | end # module Wipple 47 | -------------------------------------------------------------------------------- /lib/xrbp/model/parsers/gateway.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | # @private 4 | module Parsers 5 | # Gateway list data parser 6 | # 7 | # @private 8 | class Gateway < PluginBase 9 | def parser_priority 10 | 0 11 | end 12 | 13 | def parse_result(res, req) 14 | gateways = [] 15 | 16 | j = JSON.parse(res) 17 | j.each_key { |currency| 18 | j[currency].each { |currency_gateway| 19 | id = currency_gateway["account"] 20 | name = currency_gateway["name"] 21 | gateway = gateways.find { |gw| gw[:id] == id } 22 | if gateway 23 | gateway[:currencies] << "#{currency}" 24 | gateway[:names] << "#{name}" unless gateway[:names].include?(name) 25 | 26 | else 27 | gateways << {:id => id, 28 | :names => [name], 29 | :currencies => [currency], 30 | :start_date => currency_gateway["start_date"]} 31 | end 32 | } 33 | } 34 | 35 | gateways 36 | end 37 | end # class GatewayParser 38 | end # module Parsers 39 | end # module Model 40 | end # module XRBP 41 | -------------------------------------------------------------------------------- /lib/xrbp/model/parsers/quote.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | # @private 4 | module Parsers 5 | # Market quotes data parser 6 | # 7 | # @private 8 | class Quote < PluginBase 9 | def parser_priority 10 | 0 11 | end 12 | 13 | def parse_result(res, req) 14 | return [] unless res && res != '' 15 | 16 | j = JSON.parse(res) 17 | return [] unless j["result"] 18 | 19 | j["result"].collect { |p, quotes| 20 | next nil unless quotes 21 | quotes.collect { |q| 22 | t = q[0] 23 | o = q[1] 24 | h = q[2] 25 | l = q[3] 26 | c = q[4] 27 | vol = q[5] 28 | 29 | # discard invalid data 30 | # (some exchanges periodically 31 | # return '0's for some timestamps, 32 | # perhaps for periods with no trades?) 33 | next nil if o.zero? || h.zero? || l.zero? || c.zero? || vol.zero? 34 | 35 | {:timestamp => Time.at(t).to_datetime, 36 | :open => o, 37 | :high => h, 38 | :low => l, 39 | :close => c, 40 | :volume => vol} 41 | } 42 | }.flatten.compact 43 | end 44 | end 45 | end # module Parsers 46 | end # module Model 47 | end # module XRBP 48 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sle/st_amount_comparison.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | class STAmount 4 | module Comparison 5 | def <(o) 6 | return self < STAmount.new(:mantissa => o) if o.kind_of?(Numeric) 7 | 8 | return neg if neg && !o.neg 9 | if mantissa == 0 10 | return false if o.neg 11 | return o.mantissa != 0 12 | end 13 | 14 | return false if o.mantissa == 0 15 | return neg if exponent > o.exponent 16 | return !neg if exponent < o.exponent 17 | return neg if mantissa > o.mantissa 18 | return !neg if mantissa < o.mantissa 19 | 20 | return false 21 | end 22 | 23 | def >=(o) 24 | !(self < o) 25 | end 26 | 27 | def >(o) 28 | self >= o && self != o 29 | end 30 | 31 | def ==(o) 32 | return self == STAmount.new(:mantissa => o) if o.kind_of?(Numeric) 33 | 34 | neg == o.neg && 35 | mantissa == o.mantissa && 36 | exponent == o.exponent 37 | end 38 | 39 | def <=>(o) 40 | return self <=> STAmount.new(:mantissa => o) if o.kind_of?(Numeric) 41 | 42 | return 0 if self == o 43 | return -1 if self < o 44 | return 1 if self > o 45 | end 46 | end # module Comparison 47 | end # class STAmount 48 | end # module NodeStore 49 | end # module XRBP 50 | -------------------------------------------------------------------------------- /lib/xrbp/overlay/frame.rb: -------------------------------------------------------------------------------- 1 | require "bistro" 2 | require_relative './ripple.proto' 3 | 4 | module XRBP 5 | module Overlay 6 | # Overlay Message Frame, prefixes Protobuf based message in 7 | # with header describing size and type. 8 | # 9 | # @private 10 | class Frame 11 | TYPE_INFER = Bistro.new([ 12 | 'L>', 'size', 13 | 'S>', 'type' 14 | ]) 15 | 16 | def self.header_size 17 | TYPE_INFER.size 18 | end 19 | 20 | def header_size 21 | self.class.header_size 22 | end 23 | 24 | def self.type_name(t) 25 | Protocol::MessageType.lookup(t) 26 | end 27 | 28 | ### 29 | 30 | attr_reader :type, :size 31 | attr_accessor :data 32 | 33 | def self.from_msg(msg) 34 | # ... 35 | end 36 | 37 | def initialize(type, size) 38 | @type = type 39 | @size = size 40 | 41 | @data = "" 42 | end 43 | 44 | def type_name 45 | @type_name ||= self.class.type_name(type) 46 | end 47 | 48 | def message 49 | @message ||= MESSAGES[type_name].decode(data) 50 | end 51 | 52 | def <<(data) 53 | remaining = size - @data.size 54 | @data += data[0..remaining-1] 55 | return @data, data[remaining..-1] 56 | end 57 | 58 | def complete? 59 | @data.size == size 60 | end 61 | end # class Frame 62 | end # module WebClient 63 | end # module XRBP 64 | -------------------------------------------------------------------------------- /lib/xrbp/overlay/messages.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Overlay 3 | # Map of Protocol Message Type to Class 4 | # 5 | # See {https://github.com/ripple/rippled/blob/develop/src/ripple/overlay/impl/ProtocolMessage.h ProtocolMessage.h} 6 | MESSAGES = { 7 | :MTHELLO => Protocol::TMHello, 8 | :MTMANIFESTS => Protocol::TMManifests, 9 | :MTPING => Protocol::TMPing, 10 | :MTCLUSTER => Protocol::TMCluster, 11 | :MTGET_SHARD_INFO => Protocol::TMGetShardInfo, 12 | :MTSHARD_INFO => Protocol::TMShardInfo, 13 | :MTGET_PEER_SHARD_INFO => Protocol::TMGetPeerShardInfo, 14 | :MTGET_PEERS => Protocol::TMGetPeers, 15 | :MTPEERS => Protocol::TMPeers, 16 | :MTENDPOINTS => Protocol::TMEndpoints, 17 | :MTTRANSACTION => Protocol::TMTransaction, 18 | :MTGET_LEDGER => Protocol::TMGetLedger, 19 | :MTLEDGER_DATA => Protocol::TMLedgerData, 20 | :MTPROPOSE_LEDGER => Protocol::TMProposeSet, 21 | :MTSTATUS_CHANGE => Protocol::TMStatusChange, 22 | :MTHAVE_SET => Protocol::TMHaveTransactionSet, 23 | :MTVALIDATION => Protocol::TMValidation, 24 | :MTGET_OBJECTS => Protocol::TMGetObjectByHash 25 | } 26 | 27 | def self.create_msg(hash) 28 | # ... 29 | end 30 | end # module Overlay 31 | end # module XRBP 32 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/plugins/command_dispatcher.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Plugins 4 | # Dispatch commands (based on message dispatcher) 5 | # 6 | # @example dispatching server info command 7 | # connection = WebSocket::Connection.new "wss://s1.ripple.com:443" 8 | # connection.add_plugin :command_dispatcher 9 | # puts connection.cmd(WebSocket::Cmds::ServerInfo.new) 10 | class CommandDispatcher < MessageDispatcher 11 | def added 12 | super 13 | 14 | plugin = self 15 | 16 | connection.define_instance_method(:cmd) do |cmd, &bl| 17 | return next_connection.cmd cmd, &bl if self.kind_of?(MultiConnection) 18 | 19 | cmd = Command.new(cmd) unless cmd.kind_of?(Command) 20 | msg(cmd, &bl) 21 | end 22 | end 23 | 24 | def match_message(msg) 25 | begin 26 | return nil if msg.data == "" 27 | parsed = JSON.parse(msg.data) 28 | 29 | rescue => e 30 | return nil 31 | end 32 | 33 | id = parsed['id'] 34 | msg = messages.find { |msg| msg.kind_of?(Command) && msg.id == id } 35 | 36 | return nil unless msg 37 | [msg, parsed] 38 | end 39 | end # class CommandDispatcher 40 | 41 | WebSocket.register_plugin :command_dispatcher, CommandDispatcher 42 | end # module Plugins 43 | end # module WebSocket 44 | end # module XRBP 45 | -------------------------------------------------------------------------------- /lib/xrbp/webclient/plugins/autoretry.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebClient 3 | module Plugins 4 | # Plugin to automatically retry WebClient Connection requests 5 | # multiple times. 6 | # 7 | # If no max_tries are specified, requests will be tried indefinitely. 8 | # Optionally configured interval to wait between retries. 9 | # 10 | # @example retrying request: 11 | # connection = WebClient::Connection.new 12 | # connection.add_plugin :autoretry 13 | # 14 | # connection.max_tries = 3 15 | # connection.interval = 1 16 | # connection.timeout = 1 17 | # 18 | # connection.url = "http://doesnt.exist" 19 | # connection.perform 20 | class AutoRetry < PluginBase 21 | attr_accessor :interval, :max_tries 22 | 23 | def initialize(connection) 24 | super(connection) 25 | @interval = 3 26 | @max_tries = nil 27 | @retry_num = 0 28 | end 29 | 30 | def added 31 | plugin = self 32 | connection.define_instance_method(:retry_interval=) do |i| 33 | plugin.interval = i 34 | end 35 | 36 | connection.define_instance_method(:max_retries=) do |i| 37 | plugin.max_tries = i 38 | end 39 | end 40 | 41 | def handle_error 42 | @retry_num += 1 43 | return nil if connection.force_quit? || 44 | (!@max_tries.nil? && @retry_num > @max_tries) 45 | 46 | connection.rsleep(@interval) 47 | connection.perform 48 | end 49 | end # class AutoRetry 50 | 51 | WebClient.register_plugin :autoretry, AutoRetry 52 | end # module Plugins 53 | end # module WebClient 54 | end # module XRBP 55 | -------------------------------------------------------------------------------- /lib/xrbp/model/ledger.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module Model 3 | class Ledger < Base 4 | extend Base::ClassMethods 5 | 6 | attr_accessor :id 7 | 8 | def initialize(opts={}) 9 | @id = opts[:id] 10 | super(opts) 11 | end 12 | 13 | # Retreive specified ledger via WebSocket::Connection 14 | # 15 | # @param opts [Hash] options to retrieve ledger with 16 | # @option opts [WebSocket::Connection] :connection Connection 17 | # to use to retrieve ledger 18 | def sync(opts={}, &bl) 19 | set_opts(opts) 20 | connection.cmd(WebSocket::Cmds::Ledger.new(id, full_opts.except(:id)), &bl) 21 | end 22 | 23 | # Subscribe to ledger stream via WebSocket::Connection 24 | # 25 | # @param opts [Hash] options to subscribe to ledger stream with 26 | # @option opts [WebSocket::Connection] :connection Connection 27 | # to use to subscribe to ledger stream 28 | def self.subscribe(opts={}, &bl) 29 | set_opts(opts) 30 | conn = connection 31 | conn.cmd(WebSocket::Cmds::Subscribe.new(:streams => ["ledger"]), &bl) 32 | conn.on :message do |*args| 33 | c,msg = args.size > 1 ? [args[0], args[1]] : 34 | [nil, args[0]] 35 | 36 | begin 37 | i = JSON.parse(msg.to_s) 38 | if i["ledger_hash"] && 39 | i["ledger_index"] 40 | if c 41 | conn.emit :ledger, c, i 42 | else 43 | conn.emit :ledger, i 44 | conn.parent.emit :ledger, conn, i if conn.parent 45 | end 46 | end 47 | rescue 48 | end 49 | end 50 | end 51 | end # class Ledger 52 | end # module Model 53 | end # module XRBP 54 | -------------------------------------------------------------------------------- /lib/xrbp/model/market.rb: -------------------------------------------------------------------------------- 1 | require_relative './parsers/market' 2 | require_relative './parsers/quote' 3 | 4 | module XRBP 5 | module Model 6 | class Market < Base 7 | extend Base::ClassMethods 8 | # TODO plugabble system to pull in markets from other sources 9 | 10 | # Retrieve list of markets via WebClient::Connection 11 | # 12 | # @param opts [Hash] options to retrieve market list with 13 | # @option opts [WebClient::Connection] :connection Connection 14 | # to use to retrieve market list 15 | def self.all(opts={}) 16 | set_opts(opts) 17 | connection.url = "https://api.cryptowat.ch/assets/xrp" 18 | 19 | connection.add_plugin :result_parser unless connection.plugin?(:result_parser) 20 | connection.add_plugin Parsers::Market unless connection.plugin?(Parsers::Market) 21 | 22 | connection.perform 23 | end 24 | 25 | attr_accessor :route 26 | 27 | def initialize(opts={}) 28 | set_opts(opts) 29 | end 30 | 31 | def set_opts(opts={}) 32 | super opts 33 | @route = opts[:route] if opts[:route] 34 | end 35 | 36 | # Retrieve list of quotes for market via WebClient::Connection 37 | # 38 | # @param opts [Hash] options to retrieve quotes with 39 | # @option opts [WebClient::Connection] :connection Connection 40 | # to use to retrieve quotes 41 | def quotes(opts={}) 42 | set_opts(opts) 43 | connection.url = self.route 44 | 45 | connection.add_plugin :result_parser unless connection.plugin?(:result_parser) 46 | connection.add_plugin Parsers::Quote unless connection.plugin?(Parsers::Quote) 47 | 48 | connection.perform 49 | end 50 | end # class Market 51 | end # module Model 52 | end # module XRBP 53 | -------------------------------------------------------------------------------- /lib/xrbp/core_ext.rb: -------------------------------------------------------------------------------- 1 | # Extend Hash class w/ some methods pulled from activesupport 2 | # @private 3 | class Hash 4 | def except(*keys) 5 | dup.except!(*keys) 6 | end 7 | 8 | def except!(*keys) 9 | keys.each { |key| delete(key) } 10 | self 11 | end 12 | end 13 | 14 | # @private 15 | class Queue 16 | # Return next queue item or nil 17 | def pop_or_nil 18 | begin 19 | pop(true) 20 | rescue 21 | nil 22 | end 23 | end 24 | end 25 | 26 | # @private 27 | class String 28 | # return bignum corresponding to string 29 | def to_bn 30 | bytes.inject(0) { |bn, b| (bn << 8) | b } 31 | end 32 | 33 | def all?(&bl) 34 | each_char { |c| return false unless bl.call(c) } 35 | return true 36 | end 37 | 38 | def zero? 39 | return self == "\0" if size == 1 40 | all? { |c| c.zero? } 41 | end 42 | 43 | # scan(regex) will not work as we need to process 44 | # binary strings (\n's seem to trip scan up) 45 | def chunk(size) 46 | ((self.length + size - 1) / size).times.collect { |i| self[i * size, size] } 47 | end 48 | end 49 | 50 | # @private 51 | class Integer 52 | # return bytes 53 | def bytes 54 | i = dup 55 | b = [] 56 | until i == 0 57 | b << (i & 0xFF) 58 | i = i >> 8 59 | end 60 | b 61 | end 62 | 63 | def byte_string 64 | bytes.reverse.pack("C*") 65 | end 66 | 67 | def to_int32 68 | self & (2**32-1) 69 | end 70 | 71 | alias :to_int :to_int32 72 | 73 | def from_xrp_time 74 | XRBP.from_xrp_time(self) 75 | end 76 | end 77 | 78 | # @private 79 | class Array 80 | def rjust!(n, x) 81 | insert(0, *Array.new([0, n-length].max, x)) 82 | end 83 | 84 | def ljust!(n, x) 85 | fill(x, length...n) 86 | end 87 | end 88 | 89 | # @private 90 | class Time 91 | def to_xrp_time 92 | XRBP.to_xrp_time(self) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/plugins/command_paginator.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Plugins 4 | # Handles multi-page responses, automatically issuing subsequent requests 5 | # when more data is available and concatinating results. 6 | # 7 | # This is most useful with account transaction and object lists where a 8 | # single account may be associated with more data than can returned in a 9 | # single result. In this case response will include pagination marker 10 | # which we leverage here to retrieve all data. 11 | class CommandPaginator < PluginBase 12 | def added 13 | raise "Must also include CommandDispatcher plugin" unless connection.plugin?(CommandDispatcher) 14 | end 15 | 16 | def unlock!(cmd, res) 17 | return true unless cmd.respond_to?(:paginate?) && cmd.paginate? 18 | return true unless res["result"] # unlock if we cannot get result 19 | 20 | marker = res["result"]["marker"] 21 | page = res["result"][cmd.page_title] 22 | 23 | if marker && next_cmd = cmd.next_page(marker) 24 | connection.cmd next_cmd do 25 | page 26 | end 27 | 28 | else 29 | # XXX can't recursively use stack to unwind 30 | # callbacks as there may be too many pages. 31 | # Do it serially. 32 | res = Array.new(page) 33 | cmd.each_ancestor { |page_cmd| 34 | page_res = page_cmd.bl.call res 35 | res = page_res + res if page_cmd.prev_cmd 36 | } 37 | end 38 | 39 | false 40 | end 41 | end # class CommandPaginator 42 | 43 | WebSocket.register_plugin :command_paginator, CommandPaginator 44 | end # module Plugins 45 | end # module WebSocket 46 | end # module XRBP 47 | -------------------------------------------------------------------------------- /lib/xrbp/crypto/seed.rb: -------------------------------------------------------------------------------- 1 | require 'base58' 2 | require 'securerandom' 3 | 4 | module XRBP 5 | module Crypto 6 | # Generate a new XRPL seed. 7 | # 8 | # @param key [Symbol] key type to generate (optional) 9 | # @return [Hash] seed details containing encoded seed and key type 10 | def self.seed(key=nil) 11 | prefix = nil 12 | if key == :secp256k1 || key.nil? 13 | prefix = [Key::TOKEN_TYPES[:family_seed]] 14 | key = { :type => :secp256k1 } 15 | 16 | elsif key == :ed25519 17 | prefix = [0x01, 0xE1, 0x4B] 18 | key = { :type => :ed25519 } 19 | 20 | else 21 | raise ArgumentError, key 22 | end 23 | 24 | sha256 = OpenSSL::Digest::SHA256.new 25 | base = SecureRandom.random_bytes(16) 26 | pref = (prefix + base.bytes).pack("C*") 27 | chk = sha256.digest(sha256.digest(pref))[0..3] 28 | { :seed => Base58.binary_to_base58(pref + chk, :ripple) }.merge(key) 29 | end 30 | 31 | # Extract Seed ID from Encoding. 32 | # 33 | # @param seed [String] Base58 encoded seed 34 | # @return [String, nil] extracted seed or nil 35 | # if not valid 36 | def self.parse_seed(seed) 37 | bin = Base58.base58_to_binary(seed, :ripple) 38 | typ = bin[0] 39 | chk = bin[-4..-1] 40 | bin = bin[1...-4] 41 | 42 | # TODO also permit ED25519 prefix (?) 43 | return nil unless typ.unpack("C*").first == Key::TOKEN_TYPES[:family_seed] 44 | 45 | sha256 = OpenSSL::Digest::SHA256.new 46 | return nil unless sha256.digest(sha256.digest(typ + bin))[0..3] == chk 47 | 48 | return nil unless bin.size == 16 49 | 50 | return bin 51 | end 52 | 53 | # Return boolean indicating if the specified seed is valid 54 | def self.seed?(seed) 55 | parse_seed(seed) != nil 56 | end 57 | end # module Crypto 58 | end # module XRBP 59 | -------------------------------------------------------------------------------- /lib/xrbp/crypto/node.rb: -------------------------------------------------------------------------------- 1 | require 'base58' 2 | 3 | module XRBP 4 | module Crypto 5 | # Generate a new XRPL node. 6 | # 7 | # @param key [Symbol, Hash] key type to generate or key itself (optional) 8 | # @return [Hash] node details containing id and pub/priv key pair 9 | def self.node(key=nil) 10 | pub = nil 11 | if key == :secp256k1 || key.nil? 12 | key = Key::secp256k1 13 | pub = key[:public] 14 | 15 | elsif key.is_a?(Hash) 16 | key = Key::secp256k1(key[:seed]) if key[:seed] 17 | pub = key[:public] 18 | 19 | else 20 | pub = key 21 | key = {:public => pub} 22 | end 23 | 24 | sha256 = OpenSSL::Digest::SHA256.new 25 | node_id = [Key::TOKEN_TYPES[:node_public]].pack("C") + [pub].pack("H*") 26 | chksum = sha256.digest(sha256.digest(node_id))[0..3] 27 | 28 | { :node => Base58.binary_to_base58(node_id + chksum, :ripple) }.merge(key) 29 | end 30 | 31 | # Extract Node ID from Address. 32 | # 33 | # @param node [String] Base58 encoded node address 34 | # @return [String, nil] unique node id or nil if input 35 | # if not an node 36 | def self.parse_node(node) 37 | begin 38 | bin = Base58.base58_to_binary(node, :ripple) 39 | rescue ArgumentError 40 | return nil 41 | end 42 | 43 | typ = bin[0] 44 | chk = bin[-4..-1] 45 | bin = bin[1...-4] 46 | 47 | return nil unless typ.unpack("C*").first == Key::TOKEN_TYPES[:node_public] 48 | 49 | sha256 = OpenSSL::Digest::SHA256.new 50 | return nil unless sha256.digest(sha256.digest(typ + bin))[0..3] == chk 51 | 52 | return bin 53 | end 54 | 55 | # Return boolean indicating if the specified node is valid 56 | def self.node?(node) 57 | parse_node(node) != nil 58 | end 59 | end # module Crypto 60 | end # module XRBP 61 | -------------------------------------------------------------------------------- /xrbp.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'xrbp/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "xrbp" 7 | spec.version = XRBP::VERSION 8 | spec.authors = ["Dev Null Productions"] 9 | spec.email = ["devnullproductions@gmail.com"] 10 | spec.description = %q{Ruby XRP Tools} 11 | spec.summary = %q{Helper module to read and write data from the XRP Ledger and related resources!} 12 | spec.homepage = "https://github.com/DevNullProd/XRBP" 13 | spec.license = "MIT" 14 | 15 | spec.files = Dir.glob("examples/**/*.rb") + 16 | Dir.glob("lib/**/*.rb") + 17 | Dir.glob("spec/**/*.rb") + 18 | ["README.md", "LICENSE.txt", ".yardopts"] 19 | 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency "json", '~> 2.1' 23 | spec.add_dependency "event_emitter", '~> 0.2' 24 | spec.add_dependency "concurrent-ruby", '~> 1.0' 25 | 26 | # XXX: upstream features needed for some functionality: 27 | # https://github.com/ruby/openssl/pull/250 28 | spec.add_dependency "openssl", '~> 2.1' 29 | 30 | # for websocket module 31 | spec.add_dependency "websocket", '~> 1.2' 32 | 33 | # for webclient module 34 | # TODO remove this dep, fallback to net-http if curb isn't avaiable 35 | spec.add_dependency 'curb', '~> 0.9' 36 | 37 | # for nodstore module 38 | # (specific nodestore backends require additional deps) 39 | spec.add_dependency "bistro", '~> 2.2' 40 | 41 | # for various modules (nodestore, crypto) 42 | spec.add_dependency 'base58', '~> 0.2' 43 | 44 | # for overlay 45 | spec.add_dependency 'google-protobuf', '~> 3.6' 46 | 47 | spec.add_development_dependency 'rspec', '~> 3.8' 48 | spec.add_development_dependency 'camcorder', '~> 0.0.5' 49 | end 50 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/sle/st_amount_conversion_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::STAmount do 2 | it "is negatable" 3 | 4 | it "is convertable to/from wire format" do 5 | v = 1000000000000 6 | s = described_class.new(:mantissa => v) 7 | expect(described_class.from_wire(s.to_wire).to_h).to eq(s.to_h) 8 | 9 | s = described_class.new(:mantissa => v, 10 | :issue => XRBP::NodeStore.xrp_issue) 11 | expect(described_class.from_wire(s.to_wire).to_h).to eq(s.to_h) 12 | end 13 | 14 | it "can be parsed from string" do 15 | s = "12345.6789" 16 | expect(described_class.parse(s).iou_amount.to_f).to eq(s.to_f) 17 | 18 | expect { described_class.parse("whatever") }.to raise_error("Number 'whatever' is not valid") 19 | end 20 | 21 | context "canonicalize" do 22 | context "native amount" do 23 | context "zero mantissa" do 24 | it "zeros exponent" 25 | it "is not negative" 26 | end 27 | 28 | it "zeros exponent" 29 | end 30 | 31 | context "zero mantissa" do 32 | it "sets exponent to -100" 33 | it "is not negative" 34 | end 35 | 36 | it "increases mantissa to >= MIN_VAL" 37 | it "increases mantissa to <= MAX_VAL" 38 | 39 | it "increases exponent to >= MIN_OFFSET" 40 | it "descreases exponent to <= MAX_OFFSET" 41 | 42 | context "offset cannot be set appropriately" do 43 | it "raises value overflow" 44 | end 45 | 46 | context "manitssa out of bounds" do 47 | it "raises error" 48 | end 49 | 50 | context "exponent out of bounds" do 51 | it "raises error" 52 | end 53 | 54 | context "invalid mantissa / exponent combination" do 55 | it "raises error" 56 | end 57 | end # describe #canoncialize 58 | 59 | it "clears amount" 60 | 61 | it "returns sn_value" 62 | it "returns xrp_amount" 63 | it "returns iou_amount" 64 | end # describe XRBP::NodeStore::STAmount 65 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/plugins/autoconnect.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Plugins 4 | # Automatically connects on instantiation and reconnects 5 | # on close events. 6 | # 7 | # @example autoconnecting 8 | # connection = WebSocket::Connection.new "wss://s1.ripple.com:443" 9 | # connection.add_plugin :autoconnect 10 | # connection.open? # => true 11 | class AutoConnect < PluginBase 12 | attr_accessor :reconnect_delay 13 | 14 | def initialize(connection) 15 | super(connection) 16 | @reconnect_delay = nil 17 | end 18 | 19 | def added 20 | plugin = self 21 | 22 | connection.define_instance_method(:reconnect_delay=) do |d| 23 | plugin.reconnect_delay = d 24 | 25 | connections.each{ |c| 26 | c.plugin(AutoConnect) 27 | .reconnect_delay = d 28 | } if self.kind_of?(MultiConnection) 29 | end 30 | 31 | return if connection.kind_of?(MultiConnection) 32 | 33 | conn = connection 34 | connection.on :completed do 35 | connected = false 36 | until conn.force_quit? || connected 37 | conn.rsleep(plugin.reconnect_delay) if plugin.reconnect_delay 38 | next if conn.force_quit? 39 | 40 | begin 41 | conn.connect 42 | connected = true 43 | rescue 44 | conn.rsleep(3) 45 | end 46 | end 47 | end 48 | 49 | until connection.force_quit? || connection.open? 50 | begin 51 | connection.connect 52 | rescue 53 | connection.rsleep(3) 54 | end 55 | end 56 | end 57 | end # class AutoConnect 58 | 59 | WebSocket.register_plugin :autoconnect, AutoConnect 60 | end # module Plugins 61 | end # module WebSocket 62 | end # module XRBP 63 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/plugins/connection_timeout.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Plugins 4 | # Automatic disconnection if no server data in certain time 5 | # 6 | # @example timed out connection 7 | # connection = WebSocket::Connection.new "wss://s1.ripple.com:443" 8 | # connection.add_plugin :connection_timeout 9 | # connection.connection_timeout = 3 10 | # connection.connect 11 | # sleep(3) 12 | # connection.closed? # => true 13 | class ConnectionTimeout < PluginBase 14 | include Terminatable 15 | 16 | attr_accessor :connection_timeout 17 | 18 | DEFAULT_TIMEOUT = 10 19 | 20 | def initialize(connection) 21 | super(connection) 22 | @connection_timeout = DEFAULT_TIMEOUT 23 | end 24 | 25 | def timeout? 26 | Time.now - @last_msg > @connection_timeout 27 | end 28 | 29 | def added 30 | plugin = self 31 | connection.define_instance_method(:connection_timeout=) do |t| 32 | plugin.connection_timeout = t 33 | 34 | connections.each{ |c| 35 | c.plugin(ConnectionTimeout) 36 | .connection_timeout = t 37 | } if self.kind_of?(MultiConnection) 38 | end 39 | end 40 | 41 | def message(msg) 42 | @last_msg = Time.now 43 | end 44 | 45 | def opened 46 | connection.add_work do 47 | @last_msg = Time.now 48 | until terminate? || 49 | connection.force_quit? || 50 | connection.closed? 51 | connection.async_close! if timeout? 52 | connection.rsleep(0.1) 53 | end 54 | end 55 | end 56 | 57 | def closed 58 | terminate! 59 | end 60 | end # class ConnectionTimeout 61 | 62 | WebSocket.register_plugin :connection_timeout, ConnectionTimeout 63 | end # module Plugins 64 | end # module WebSocket 65 | end # module XRBP 66 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/protocol/indexes_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::NodeStore::Indexes do 2 | it "returns quality for base" do 3 | base = 57159940697819848473151736347809673846048755675063285491534159019684120657920.byte_string 4 | described_class.get_quality(base).should eq(6197953087261802496) 5 | end 6 | 7 | it "returns next quality for base" do 8 | base = 57159940697819848473151736347809673846048755675063285491534159019684120657920.byte_string 9 | nxt = 57159940697819848473151736347809673846048755675063285491552605763757830209536 10 | described_class.get_quality_next(base).to_bn.should eq(nxt) 11 | end 12 | 13 | it "returns directory node index" 14 | 15 | it "returns page index" 16 | 17 | it "returns account index" do 18 | account = "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" 19 | described_class.account(account).to_bn.should eq(83149858826312963445632044328430706857892442949720032032422519427184563347763) 20 | end 21 | 22 | context "account > issuer" do 23 | it "returns trust line index" do 24 | account = "rnixnrMHHvR7ejMpJMRCWkaNrq3qREwMDu" 25 | iou = {:currency=>"EUR", :account=>"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"} 26 | described_class.line(account, iou).to_bn.should eq(45370088078399581796930875479259405419767996706903582207576813674682296859050) 27 | end 28 | end 29 | 30 | context "account < issuer" do 31 | it "returns trust line index" do 32 | account = "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" 33 | iou = {:currency=>"EUR", :account=>"rnixnrMHHvR7ejMpJMRCWkaNrq3qREwMDu"} 34 | described_class.line(account, iou).to_bn.should eq(45370088078399581796930875479259405419767996706903582207576813674682296859050) 35 | end 36 | end 37 | 38 | it "returns order book index" do 39 | input = {:currency=>"USD", :account=>"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"} 40 | output = {:currency=>"EUR", :account=>"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"} 41 | described_class.order_book(input, output).to_bn.should eq(57159940697819848473151736347809673846048755675063285491527961066596858855424) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/db.rb: -------------------------------------------------------------------------------- 1 | require 'base58' 2 | require 'openssl' 3 | 4 | require_relative './parser' 5 | 6 | module XRBP 7 | # The NodeStore is the Key/Value DB which rippled persistent stores 8 | # ledger data. Implemented via a backend configured at run time, 9 | # the NodeStore is used to store the tree-like structures that 10 | # consistute the XRP ledger. 11 | # 12 | # The Keys and Values stored in the NodeStore are custom binary 13 | # encodings of tree-node IDs and data. See this module 14 | # and the others in this directory for specifics on how keys & values 15 | # are stored and extracted. 16 | module NodeStore 17 | 18 | # Base NodeStore DB module, the client will use this class through 19 | # specific DB-type subclass. 20 | # 21 | # Subclasses should define the [ ] (index) method taking key to 22 | # lookup, returning corresponding NodeStore value and *each* method, 23 | # iterating over nodestore values (see existing subclasses for 24 | # implementation details) 25 | class DB 26 | include Enumerable 27 | include EventEmitter 28 | 29 | # NodeStore Parser 30 | include Parser 31 | 32 | # Return the NodeStore Ledger for the given lookup hash 33 | def ledger(hash) 34 | val = self[hash] 35 | return nil if val.nil? 36 | parse_ledger(val) 37 | end 38 | 39 | # Return the NodeStore Account for the given lookup hash 40 | def account(hash) 41 | ledger_entry(hash) 42 | end 43 | 44 | # Return the NodeStore Ledger Entry for the given lookup hash 45 | def ledger_entry(hash) 46 | val = self[hash] 47 | return nil if val.nil? 48 | parse_ledger_entry(val) 49 | end 50 | 51 | # Return the NodeStore Transaction for the given lookup hash 52 | def tx(hash) 53 | val = self[hash] 54 | return nil if val.nil? 55 | parse_tx(val) 56 | end 57 | 58 | # Return the NodeStore InnerNode for the given lookup hash 59 | def inner_node(hash) 60 | val = self[hash] 61 | return nil if val.nil? 62 | parse_inner_node(val) 63 | end 64 | end # class DB 65 | end # module NodeStore 66 | end # module XRBP 67 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/backends/rocksdb.rb: -------------------------------------------------------------------------------- 1 | # requires rocksdb-ruby gem 2 | require "rocksdb" 3 | 4 | module XRBP 5 | module NodeStore 6 | module Backends 7 | # RocksDB nodestore backend, faciliates accessing XRP Ledger data 8 | # in a RocksDB database. 9 | # 10 | # @example retrieve data from RocksDB backend 11 | # require 'nodestore/backends/rocksdb' 12 | # rocksdb = NodeStore::Backends::RocksDB.new '/var/lib/rippled/db/rocksdb' 13 | # puts rocksdb.ledger('B506ADD630CB707044B4BFFCD943C1395966692A13DD618E5BD0978A006B43BD') 14 | class RocksDB < DB 15 | # cap max open files for performance 16 | MAX_OPEN_FILES = 2000 17 | 18 | def initialize(path) 19 | @db = ::RocksDB::DB.new path, 20 | {:readonly => true, 21 | :max_open_files => MAX_OPEN_FILES} 22 | end 23 | 24 | # Retrieve database value for the specified key 25 | # 26 | # @param key [String] binary key to lookup 27 | # @return [String] binary value 28 | def [](key) 29 | @db[key] 30 | end 31 | 32 | # Iterate over each database key/value pair, 33 | # invoking callback. During iteration will 34 | # emit signals specific to the DB types being 35 | # parsed 36 | # 37 | # @example iterating over RocksDB entries 38 | # rocksdb.each do |iterator| 39 | # puts "Key/Value: #{iterator.key}/#{iterator.value}" 40 | # end 41 | # 42 | # @example handling account via event callback 43 | # rocksdb.on(:account) do |hash, account| 44 | # puts "Account #{hash}" 45 | # pp account 46 | # end 47 | # 48 | # # Any Enumerable method that invokes #each will 49 | # # have intended effect 50 | # rocksdb.to_a 51 | def each 52 | iterator = @db.new_iterator 53 | iterator.seek_to_first 54 | 55 | while(iterator.valid) 56 | type, obj = infer_type(iterator.value) 57 | 58 | if type 59 | emit type, iterator.key, obj 60 | else 61 | emit :unknown, iterator.key, 62 | iterator.value 63 | end 64 | 65 | yield iterator 66 | iterator.next 67 | end 68 | 69 | iterator.close 70 | return self 71 | end 72 | end # class RocksDB 73 | end # module Backends 74 | end # module NodeStore 75 | end # module XRBP 76 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/node_id.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | class SHAMap 3 | # Encapsulates node key to allow for tree traversal. 4 | # 5 | # Provides branch extraction/generation logic. 6 | # Since branch is between 0-15, only a nibble (4bits) 7 | # are needed to store. Thus each char (8bits) can describe 8 | # 2 tree branches 9 | class NodeID 10 | attr_reader :depth, :key 11 | 12 | def initialize(args={}) 13 | @depth ||= args[:depth] || 0 14 | @key ||= args[:key] || NodeStore.uint256 15 | end 16 | 17 | MASK_SIZE = 65 18 | 19 | # Masks corresponding to each tree level. 20 | # Used to calculate inner node hash for 21 | # tree level: 22 | # inner node = lookup key & mask 23 | def self.masks 24 | @masks ||= begin 25 | masks = Array.new(MASK_SIZE) 26 | 27 | i = 0 28 | selector = NodeStore.uint256 29 | while(i < MASK_SIZE-1) 30 | masks[i] = String.new(selector) 31 | selector[i / 2] = 0xF0.chr 32 | masks[i+1] = String.new(selector) 33 | selector[i / 2] = 0xFF.chr 34 | i += 2 35 | end 36 | masks[MASK_SIZE-1] = selector 37 | 38 | masks 39 | end 40 | end 41 | 42 | # Return mask for current tree depth 43 | def mask 44 | @mask ||= self.class.masks[depth] 45 | end 46 | 47 | # Return branch number of specified hash. 48 | def select_branch(hash) 49 | #if RIPPLE_VERIFY_NODEOBJECT_KEYS 50 | raise if depth >= 64 51 | raise if (hash.to_bn & mask.to_bn) != key.to_bn 52 | #end 53 | 54 | # Extract hash byte at local node depth 55 | br = hash[depth / 2].ord 56 | 57 | # Reduce to relevant nibble 58 | if (depth & 1) == 1 59 | br &= 0xf 60 | else 61 | br >>= 4 62 | end 63 | 64 | raise unless (br >= 0) && (br < 16) 65 | br 66 | end 67 | 68 | # Return NodeID for specified branch under this one. 69 | def child_node_id(branch) 70 | raise unless branch >= 0 && branch < 16 71 | raise unless depth < 64 72 | 73 | # Copy local key and assign branch number to 74 | # nibble in byte at local depth 75 | child = key.unpack("C*") 76 | child[depth/2] |= ((depth & 1) == 1) ? branch : (branch << 4) 77 | 78 | NodeID.new :depth => (depth + 1), 79 | :key => child.pack("C*") 80 | end 81 | end # class NodeID 82 | end # class SHAMap 83 | end # module XRBP 84 | -------------------------------------------------------------------------------- /lib/xrbp/overlay/handshake.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'openssl' 3 | 4 | module XRBP 5 | module Overlay 6 | 7 | # Overlay Connection Handshake, the first message sent from connection 8 | # initiator to remote endpoint establishing session. Once connection 9 | # is successfully established subsequent messages will be encapsulated 10 | # Frames. 11 | # 12 | # XXX this module requires the openssl-ruby gem with the following patches: 13 | # https://github.com/ruby/openssl/pull/250 14 | # 15 | # Otherwise the ssl_socket#finished_message and #peer_finished_message 16 | # methods will not be available 17 | # 18 | # Currently the only way to apply this is to checkout openssl-ruby, apply 19 | # the patches, and then rebuild/reinstall the gem locally! 20 | # 21 | # @private 22 | class Handshake 23 | attr_reader :connection, :response 24 | 25 | def initialize(connection) 26 | @connection = connection 27 | end 28 | 29 | def socket 30 | connection.ssl_socket 31 | end 32 | 33 | def node 34 | connection.node 35 | end 36 | 37 | ### 38 | 39 | def shared 40 | @shared ||= begin 41 | sha512 = OpenSSL::Digest::SHA512.new 42 | sf = socket.finished_message 43 | pf = socket.peer_finished_message 44 | sf = sha512.digest(sf) 45 | pf = sha512.digest(pf) 46 | shared = sf.to_bn ^ pf.to_bn 47 | shared = shared.byte_string 48 | shared = sha512.digest(shared)[0..31] 49 | 50 | shared = Crypto::Key.sign_digest(node, shared) 51 | Base64.strict_encode64(shared) 52 | end 53 | end 54 | 55 | def data 56 | @data ||= 57 | "GET / HTTP/1.1\r 58 | User-Agent: rippled-1.1.2\r 59 | Upgrade: RTXP/1.2, RTXP/1.3\r 60 | Connection: Upgrade\r 61 | Connect-As: Leaf, Peer\r 62 | Public-Key: #{node[:node]}\r 63 | Session-Signature: #{shared}\r 64 | \r\n" 65 | end 66 | 67 | ### 68 | 69 | def execute! 70 | socket.puts(data) 71 | 72 | @response = "" 73 | until connection.closed? # || connection.force_quit? 74 | read_sockets, _, _ = IO.select([socket], nil, nil, 0.1) 75 | 76 | if read_sockets && read_sockets[0] 77 | begin 78 | out = socket.read_nonblock(1024) 79 | @response += out.strip 80 | break if out[-4..-1] == "\r\n\r\n" 81 | rescue OpenSSL::SSL::SSLErrorWaitReadable 82 | end 83 | end 84 | end 85 | end 86 | end # class Handshake 87 | end # module WebClient 88 | end # module XRBP 89 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/multi/multi_connection.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | # Base class facilitating transparent multiple 4 | # connection dispatching. This provides mechanism which 5 | # to instantiate multiple WebSocket::Connection instances 6 | # proxying requests to them depending on the *next_connection* 7 | # selected. 8 | # 9 | # This class provides all the common logic to manage 10 | # multiple connections. Subclasses should override and 11 | # implement *next_connection* specifying the strategy 12 | # used to select the connection to use for any given 13 | # request. 14 | class MultiConnection 15 | include EventEmitter 16 | include HasPlugin 17 | 18 | def plugin_namespace 19 | WebSocket 20 | end 21 | 22 | attr_reader :connections 23 | 24 | # MultiConnection initializer taking list of urls which 25 | # to connect to 26 | # 27 | # @param urls [Array] list of urls which to establish 28 | # connections to 29 | def initialize(*urls) 30 | @connections = [] 31 | 32 | urls.each { |url| 33 | @connections << Connection.new(url) 34 | } 35 | 36 | connections.each { |c| c.parent = self } 37 | 38 | yield self if block_given? 39 | end 40 | 41 | # Force terminate all connections 42 | def force_quit! 43 | connections.each { |c| c.force_quit! } 44 | end 45 | 46 | # Close all connections 47 | def close! 48 | connections.each { |c| c.close! } 49 | end 50 | 51 | # Block until all connections are openend 52 | def wait_for_open 53 | connections.each { |c| c.wait_for_open } 54 | end 55 | 56 | # Block until all connections are closed 57 | def wait_for_close 58 | connections.each { |c| c.wait_for_close } 59 | end 60 | 61 | # Block until all connections are completed 62 | def wait_for_completed 63 | connections.each { |c| c.wait_for_completed } 64 | end 65 | 66 | alias :_add_plugin :add_plugin 67 | 68 | def add_plugin(*plg) 69 | connections.each { |c| 70 | c.add_plugin *plg 71 | } 72 | 73 | _add_plugin(*plg) 74 | end 75 | 76 | # Always return first connection by default, 77 | # override in subclasses 78 | def next_connection(prev=nil) 79 | return nil unless prev.nil? 80 | @connections.first 81 | end 82 | 83 | def connect 84 | @connections.each { |c| 85 | c.connect 86 | } 87 | end 88 | end # class MultiConnection 89 | end # module WebSocket 90 | end # module XRBP 91 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sle/st_amount.rb: -------------------------------------------------------------------------------- 1 | require_relative './st_amount_arithmatic' 2 | require_relative './st_amount_comparison' 3 | require_relative './st_amount_conversion' 4 | 5 | module XRBP 6 | module NodeStore 7 | # Serialized Amount Representation. 8 | # 9 | # From rippled docs: 10 | # Internal form: 11 | # 1: If amount is zero, then value is zero and offset is -100 12 | # 2: Otherwise: 13 | # legal offset range is -96 to +80 inclusive 14 | # value range is 10^15 to (10^16 - 1) inclusive 15 | # amount = value * [10 ^ offset] 16 | # 17 | # Wire form: 18 | # High 8 bits are (offset+142), legal range is, 80 to 22 inclusive 19 | # Low 56 bits are value, legal range is 10^15 to (10^16 - 1) inclusive 20 | class STAmount 21 | include Arithmatic 22 | include Comparison 23 | include Conversion 24 | 25 | # DEFINES FROM STAmount.h 26 | 27 | MIN_OFFSET = -96 28 | MAX_OFFSET = 80 29 | 30 | MIN_VAL = 1000000000000000 31 | MAX_VAL = 9999999999999999 32 | NOT_NATIVE = 0x8000000000000000 33 | POS_NATIVE = 0x4000000000000000 34 | MAX_NATIVE = 100000000000000000 35 | 36 | attr_reader :mantissa, :exponent, :neg 37 | attr_accessor :issue 38 | 39 | alias :value :mantissa 40 | alias :offset :exponent 41 | 42 | def self.zero(args={}) 43 | STAmount.new args.merge({:mantissa => 0}) 44 | end 45 | 46 | def self.from_quality(rate) 47 | return STAmount.new(:issue => NodeStore.no_issue) if rate == 0 48 | 49 | mantissa = rate & ~(255 << (64 - 8)) 50 | 51 | exponent = (rate >> (64 - 8)).to_int32 - 100 52 | 53 | return STAmount.new(:issue => NodeStore.no_issue, 54 | :mantissa => mantissa, 55 | :exponent => exponent) 56 | end 57 | 58 | def initialize(args={}) 59 | @issue = args[:issue] 60 | @mantissa = args[:mantissa] || 0 61 | @exponent = args[:exponent] || 0 62 | @neg = !!args[:neg] 63 | 64 | canonicalize 65 | end 66 | 67 | ### 68 | 69 | def native? 70 | @issue && @issue.xrp? 71 | end 72 | 73 | def zero? 74 | @mantissa == 0 75 | end 76 | 77 | def inspect 78 | return "0" if zero? 79 | 80 | i = issue.inspect 81 | i = i == '' ? '' : "(#{i})" 82 | (native? ? xrp_amount : iou_amount.to_f).to_s + 83 | (native? ? "" : i) 84 | end 85 | 86 | def to_s 87 | inspect 88 | end 89 | end # class STAmount 90 | end # module NodeStore 91 | end # module XRBP 92 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/inner_node.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | class SHAMap 3 | # A DB entry which may contain references of up to 16-child 4 | # nodes, facilitating abstract tree-like traversal. 5 | # 6 | # This class simply encapsulates children w/ hashes 7 | class InnerNode < Node 8 | attr_accessor :depth, :common, :hashes, :is_branch 9 | 10 | def initialize(args={}) 11 | @v2 = args[:v2] 12 | @depth = args[:depth] || 0 13 | 14 | @common = {} 15 | @hashes = {} 16 | @children = [] 17 | @is_branch = 0 18 | end 19 | 20 | def v2? 21 | @v2 22 | end 23 | 24 | def inner? 25 | true 26 | end 27 | 28 | def common_prefix?(key) 29 | hd = depth/2 30 | 0.upto(hd) do |d| 31 | return false if common[d] != key[d] 32 | end 33 | 34 | return (common[hd] & 0xF0) && 35 | (key[hd] & 0xF0) if depth & 1 36 | 37 | return true 38 | end 39 | 40 | # Returns true if node has no children 41 | def empty? 42 | is_branch == 0 43 | end 44 | 45 | # Return true if specified branch is empty, 46 | # else false 47 | def empty_branch?(branch) 48 | (is_branch & (1 << branch)) == 0 49 | end 50 | 51 | # Returns hash of child on given branch 52 | def child_hash(branch) 53 | raise ArgumentError unless branch >= 0 && 54 | branch < 16 55 | hashes[branch] 56 | end 57 | 58 | # Returns child containing in given branch 59 | def child(branch) 60 | raise ArgumentError unless branch >= 0 && 61 | branch < 16 62 | @children[branch] 63 | end 64 | 65 | # Canonicalize and store child node at branch 66 | def canonicalize_child(branch, node) 67 | raise ArgumentError unless branch >= 0 && 68 | branch < 16 69 | raise unless node 70 | raise unless node.hash == hashes[branch] 71 | 72 | if @children[branch] 73 | return @children[branch] 74 | else 75 | return @children[branch] = node 76 | end 77 | end 78 | 79 | # Update this node's hash from child hashes 80 | def update_hash 81 | nh = nil 82 | 83 | if is_branch != 0 84 | sha512 = OpenSSL::Digest::SHA512.new 85 | sha512 << HASH_+PREFIXES[:inner_node] 86 | hashes.each { |k,h| 87 | sha512 << v 88 | } 89 | nh = sha512.digest 90 | end 91 | 92 | return false if nh == self.hash 93 | self.hash = nh 94 | return true 95 | end 96 | end # class InnerNode 97 | end # class SHAMap 98 | end # module XRBP 99 | -------------------------------------------------------------------------------- /lib/xrbp/webclient/connection.rb: -------------------------------------------------------------------------------- 1 | require 'curb' 2 | require_relative '../thread_registry' 3 | 4 | module XRBP 5 | module WebClient 6 | # HTTP interface, use Connection to perform web requests. 7 | # 8 | # @example retrieve data from the web 9 | # connection = WebClient::Connection.new 10 | # connection.url = "https://devnull.network" 11 | # connection.perform 12 | class Connection 13 | include EventEmitter 14 | include HasPlugin 15 | include HasResultParsers 16 | include ThreadRegistry 17 | 18 | DELEGATED_METHODS = [:url=, 19 | :timeout=, 20 | :ssl_verify_peer=, 21 | :ssl_verify_host=] 22 | 23 | # @private 24 | def plugin_namespace 25 | WebClient 26 | end 27 | 28 | # @private 29 | def parsing_plugins 30 | plugins 31 | end 32 | 33 | # Return current url 34 | def url 35 | c.url 36 | end 37 | 38 | # delegated methods 39 | DELEGATED_METHODS.each { |m| 40 | define_method(m) do |v| 41 | c.send(m, v) 42 | end 43 | } 44 | 45 | def initialize(url=nil) 46 | self.url = url 47 | @force_quit = false 48 | 49 | yield self if block_given? 50 | end 51 | 52 | def force_quit? 53 | @force_quit 54 | end 55 | 56 | # Immediate terminate outstanding requests 57 | def force_quit! 58 | @force_quit = true 59 | wake_all 60 | # TODO immediate terminate outstanding requests 61 | end 62 | 63 | private 64 | 65 | def c 66 | @curl ||= Curl::Easy.new 67 | end 68 | 69 | def handle_error 70 | plugins.select { |plg| 71 | plg.respond_to?(:handle_error) 72 | }.last&.handle_error 73 | end 74 | 75 | public 76 | 77 | # Execute web request, retrieving results and returning 78 | def perform 79 | # TODO fault tolerance plugins: 80 | # configurable timeout, 81 | # round-robin urls, 82 | # redirect handling, etc 83 | begin 84 | c.perform 85 | rescue => e 86 | emit :error, e 87 | return handle_error 88 | end 89 | 90 | if c.response_code != 200 91 | emit :http_error, c.response_code 92 | return handle_error 93 | end 94 | 95 | emit :success, c.body_str 96 | begin 97 | parse_result(c.body_str, c) 98 | rescue Exception => e 99 | emit :error, e 100 | return nil 101 | end 102 | end 103 | end # class Connection 104 | end # module WebClient 105 | end # module XRBP 106 | -------------------------------------------------------------------------------- /spec/xrbp/websocket/connection_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::WebSocket::Connection do 2 | subject { XRBP::WebSocket::Connection.new 'wss://test.com:443' } 3 | let(:client){ subject.client } 4 | 5 | it 'is not initialized' do 6 | expect(subject).to_not be_initialized 7 | end 8 | 9 | it 'is not open' do 10 | expect(subject).to_not be_open 11 | end 12 | 13 | it 'is closed' do 14 | expect(subject).to be_closed 15 | end 16 | 17 | it 'is completed' do 18 | expect(subject).to be_completed 19 | end 20 | 21 | context "client exists" do 22 | before(:each) do 23 | subject.client 24 | end 25 | 26 | it "is initialized" do 27 | expect(subject).to be_initialized 28 | end 29 | end 30 | 31 | context "client is open" do 32 | before(:each) do 33 | expect(client).to receive(:open?).and_return true 34 | end 35 | 36 | it "is open" do 37 | expect(subject).to be_open 38 | end 39 | 40 | it "is not closed" do 41 | expect(subject).to_not be_closed 42 | end 43 | end 44 | 45 | context "client is not completed" do 46 | before(:each) do 47 | expect(client).to receive(:completed?).and_return false 48 | end 49 | 50 | it "is not completed" do 51 | expect(subject).to_not be_completed 52 | end 53 | end 54 | 55 | describe "#connect" do 56 | it "connects to client" do 57 | expect(client).to receive(:connect) 58 | subject.connect 59 | end 60 | end 61 | 62 | describe "#close!" do 63 | it "closes client" do 64 | expect(client).to receive(:open?).and_return true 65 | expect(client).to receive(:close) 66 | subject.close! 67 | end 68 | end 69 | 70 | describe "#close!" do 71 | it "async closes client" do 72 | expect(client).to receive(:open?).and_return true 73 | expect(client).to receive(:async_close) 74 | subject.async_close! 75 | end 76 | end 77 | 78 | describe "#send_data" do 79 | it "sends data via client" do 80 | expect(client).to receive(:send_data).with("foo") 81 | subject.send_data("foo") 82 | end 83 | end 84 | 85 | describe "#add_work" do 86 | it "adds work to client pool" do 87 | expect(client).to receive(:add_work) 88 | subject.add_work do 89 | end 90 | end 91 | end 92 | 93 | describe "#next_connection" do 94 | context "no parent" do 95 | it "returns nil" do 96 | expect(subject.next_connection(nil)).to be_nil 97 | end 98 | end 99 | 100 | it "returns parent's next connection" do 101 | parent = double 102 | expect(parent).to receive(:next_connection).with(:foo).and_return :bar 103 | 104 | subject.parent = parent 105 | expect(subject.next_connection(:foo)).to eq(:bar) 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/xrbp/crypto/account.rb: -------------------------------------------------------------------------------- 1 | require 'base58' 2 | 3 | module XRBP 4 | module Crypto 5 | # Generate a new XRPL account. 6 | # 7 | # @param key [Symbol, Hash] key type to generate or key itself (optional) 8 | # @return [Hash] account details containing id and pub/priv key pair 9 | def self.account(key=nil) 10 | pub = nil 11 | account_id = nil 12 | if key == :secp256k1 || key.nil? 13 | key = Key::secp256k1 14 | pub = key[:public] 15 | 16 | elsif key == :ed25519 17 | key = Key::ed25519 18 | pub = "\xED" + key[:public] 19 | 20 | elsif key.is_a?(Hash) 21 | if key[:account_id] 22 | account_id = key[:account_id] 23 | elsif key[:public] 24 | pub = key[:public] 25 | end 26 | 27 | else 28 | pub = key 29 | key = {:public => pub} 30 | end 31 | 32 | raise "must specify pub or account_id" unless pub || account_id 33 | 34 | sha256 = OpenSSL::Digest::SHA256.new 35 | ripemd160 = OpenSSL::Digest::RIPEMD160.new 36 | account_id = [Key::TOKEN_TYPES[:account_id]].pack("C") + ripemd160.digest(sha256.digest([pub].pack("H*"))) unless account_id 37 | chksum = sha256.digest(sha256.digest(account_id))[0..3] 38 | 39 | { :account => Base58.binary_to_base58(account_id + chksum, :ripple) }.merge(key) 40 | end 41 | 42 | # Account Zero: https://xrpl.org/accounts.html#special-addresses 43 | def self.xrp_account 44 | @xrp_account ||= account(:account_id => ([0] * 21).pack("C*"))[:account] 45 | end 46 | 47 | # Account One: https://xrpl.org/accounts.html#special-addresses 48 | def self.no_account 49 | @no_account ||= account(:account_id => ([0] * 20 + [1]).pack("C*"))[:account] 50 | end 51 | 52 | # Return the account id for the specified XRP account. 53 | # This is a simpler version of {parse_account} that just 54 | # returns the binary account, _skipping_ the token and 55 | # checksum verifications. 56 | def self.account_id(account) 57 | Base58.base58_to_binary(account, :ripple)[1..-5] 58 | end 59 | 60 | # Extract Account ID from Address. 61 | # 62 | # @param account [String] Base58 encoded account address 63 | # @return [String, nil] unique account id or nil if input 64 | # if not an account 65 | def self.parse_account(account) 66 | begin 67 | bin = Base58.base58_to_binary(account, :ripple) 68 | rescue ArgumentError 69 | return nil 70 | end 71 | 72 | typ = bin[0] 73 | chk = bin[-4..-1] 74 | bin = bin[1...-4] 75 | 76 | return nil unless typ.unpack("C*").first == Key::TOKEN_TYPES[:account_id] 77 | 78 | sha256 = OpenSSL::Digest::SHA256.new 79 | return nil unless sha256.digest(sha256.digest(typ + bin))[0..3] == chk 80 | 81 | return bin 82 | end 83 | 84 | # Return boolean indicating if the specified account is valid 85 | def self.account?(account) 86 | parse_account(account) != nil 87 | end 88 | end # module Crypto 89 | end # module XRBP 90 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sqldb.rb: -------------------------------------------------------------------------------- 1 | require 'sqlite3' 2 | 3 | module XRBP 4 | module NodeStore 5 | # Wraps sqlite3 database created/maintianed by rippled. Allows client 6 | # to query for data stored in sql database. 7 | class SQLDB 8 | 9 | # SQL DB intializer 10 | # 11 | # @param dir [String] directory containing binary nodestore. For consistency 12 | # with other nodestore paths this should be set to the directory containing 13 | # the actual 'nudb' or 'rocksdb' datafiles, as the sqlite3 databases will be 14 | # inferred from the parent directory. 15 | def initialize(dir) 16 | @dir = dir 17 | end 18 | 19 | def ledger_db 20 | @ledger_db ||= SQLite3::Database.new File.join(@dir, "..", "ledger.db") 21 | end 22 | 23 | def tx_db 24 | @ledger_db ||= SQLite3::Database.new File.join(@dir, "..", "transaction.db") 25 | end 26 | 27 | def ledgers 28 | @ledgers ||= Ledgers.new(self) 29 | end 30 | 31 | class Ledgers 32 | include Enumerable 33 | 34 | def initialize(sql_db) 35 | @sql_db = sql_db 36 | end 37 | 38 | def between(before, after) 39 | @sql_db.ledger_db.execute("select * from Ledgers where ClosingTime >= ? and ClosingTime <= ?", 40 | before.to_xrp_time, 41 | after.to_xrp_time) 42 | .collect { |row| from_db(row) } 43 | end 44 | 45 | def hash_for_seq(seq) 46 | @sql_db.ledger_db.execute("select LedgerHash from Ledgers where LedgerSeq = ?", seq).first.first 47 | end 48 | 49 | def size 50 | @sql_db.ledger_db.execute("select count(ROWID) from Ledgers").first.first 51 | end 52 | 53 | def first 54 | @sql_db.ledger_db.execute("select LedgerSeq from Ledgers order by LedgerSeq asc limit 1").first.first 55 | end 56 | 57 | def last 58 | @sql_db.ledger_db.execute("select LedgerSeq from Ledgers order by LedgerSeq desc limit 1").first.first 59 | end 60 | 61 | alias :count :size 62 | 63 | def each 64 | all.each do |row| 65 | yield row 66 | end 67 | end 68 | 69 | # TODO: remove memoization, define first(n), last(n) methods 70 | def all 71 | @all ||= @sql_db.ledger_db.execute("select * from Ledgers order by LedgerSeq asc") 72 | .collect { |row| from_db(row) } 73 | end 74 | 75 | private 76 | 77 | def from_db(row) 78 | {:hash => row[0], 79 | :seq => row[1], 80 | :prev_hash => row[2], 81 | :total_coins => row[3], 82 | :closing_time => row[4].from_xrp_time, 83 | :prev_closing_time => row[5].from_xrp_time, 84 | :close_time_res => row[6], 85 | :close_flags => row[7], 86 | :account_set_hash => row[8], 87 | :trans_set_hash => row[9]} 88 | end 89 | end # class Ledgers 90 | end # class SQLDB 91 | end # module NodeStore 92 | end # module XRBP 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### XRBP - Ruby XRP Library 2 | 3 |

4 | 5 |

6 | 7 | 8 | XRBP is a rubygem which provides a fault-tolerant interface to the [XRP](https://en.wikipedia.org/wiki/XRP) ledger. 9 | 10 | With XRP you can connect to one or more [rippled](https://github.com/ripple/rippled) servers and use them to transparently read and write data to/from the XRP Ledger: 11 | 12 | ```ruby 13 | require 'xrbp' 14 | 15 | ws = XRBP::WebSocket::Connection.new "wss://s1.ripple.com:443" 16 | ws.add_plugin :autoconnect, :command_dispatcher 17 | 18 | ws.cmd XRBP::WebSocket::Cmds::ServerInfo.new 19 | ``` 20 | 21 | XRBP provides fully-object-oriented mechanisms to interact with the ledger: 22 | 23 | ```ruby 24 | ws.on :ledger do |l| 25 | puts "Ledger received: " 26 | puts l 27 | end 28 | 29 | XRBP::Model::Ledger.subscribe(:connection => ws) 30 | ``` 31 | 32 | #### Supported Features: 33 | 34 | Other data types besides ledgers may be syncronized: 35 | 36 | ```ruby 37 | puts XRBP::Model::Account.new("rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B").info 38 | ``` 39 | 40 | Also data from other sources, such as the [Ripple DataV2 API](https://developers.ripple.com/data-api.html) 41 | 42 | ```ruby 43 | connection = XRBP::WebClient::Connection.new 44 | XRBP::Model::Validator.all(:connection => connection) 45 | .each do |v| 46 | puts v 47 | end 48 | ``` 49 | 50 | XRPB allows easy access to the following data: 51 | 52 | - XRP ledgers, transactions, account, and objects 53 | - Network nodes, validators, gateways 54 | - Markets with quotes 55 | - & more (see *examples/* for more use cases) 56 | 57 | #### Multiple Connections 58 | 59 | XRBP facilitates fault-tolerant applications by providing customizable strategies which to leverage multiple rippled servers in communications. 60 | 61 | ```ruby 62 | ws = XRBP::WebSocket::RoundRobin.new "wss://s1.ripple.com:443", 63 | "wss://s2.ripple.com:443" 64 | 65 | ws.add_plugin :command_dispatcher 66 | ws.connect 67 | 68 | puts ws.cmd(XRBP::WebSocket::Cmds::ServerInfo.new) 69 | puts ws.cmd(XRBP::WebSocket::Cmds::ServerInfo.new) 70 | ``` 71 | 72 | In this case the first **ServerInfo** command will be sent to *s1.ripple.com* while the second will be sent to *s2.ripple.com*. 73 | 74 | The following demonstrates prioritized connections: 75 | 76 | ```ruby 77 | ws = XRBP::WebSocket::Prioritized.new "wss://s1.ripple.com:443", 78 | "wss://s2.ripple.com:443" 79 | 80 | ws.add_plugin :command_dispatcher, :result_parser 81 | ws.parse_results { |res| 82 | res["result"]["ledger"] 83 | } 84 | ws.connect 85 | 86 | puts ws.cmd(XRBP::WebSocket::Cmds::Ledger.new(28327070)) 87 | ``` 88 | 89 | *s1.ripple.com* will be queried for the specified ledger. If not present *s2.ripple.com* will be queried. 90 | 91 | #### Installation / Documentation 92 | 93 | XRPB may be installed with the following command: 94 | 95 | ```ruby 96 | $ gem install xrbp 97 | ``` 98 | 99 | Documentation is available [online](https://www.rubydoc.info/gems/xrbp) 100 | 101 | #### License 102 | 103 | Copyright (C) 2019 Dev Null Productions 104 | 105 | Made available under the MIT License 106 | -------------------------------------------------------------------------------- /spec/xrbp/websocket/client_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../helpers/test_handshake' 2 | require_relative '../../helpers/force_serializable' 3 | 4 | describe XRBP::WebSocket::Client do 5 | subject { described_class.new "wss://s1.ripple.com:443" } 6 | let(:socket) { subject.send(:socket) } 7 | let(:pool) { subject.send(:pool) } 8 | 9 | before(:each) do 10 | subject.stub_handshake! 11 | subject.force_serializable! 12 | Camcorder.intercept_constructor XRBP::WebSocket::Socket 13 | end 14 | 15 | after(:each) do 16 | Camcorder.deintercept_constructor XRBP::WebSocket::Socket 17 | end 18 | 19 | describe '#connect' do 20 | after(:each) do 21 | pool.kill 22 | pool.wait_for_termination 23 | end 24 | 25 | it "connects to client" do 26 | # XXX we are recording socket, cannot add expections to it 27 | socket = double 28 | subject.instance_variable_set(:@socket, socket) 29 | expect(socket).to receive(:connect) 30 | expect(socket).to receive(:write) 31 | expect(socket).to receive(:read_next) 32 | 33 | subject.connect 34 | end 35 | 36 | it "performs handshake" do 37 | expect(subject).to receive(:handshake!) 38 | subject.connect 39 | end 40 | 41 | it "starts reading" do 42 | expect(subject).to receive(:start_read) 43 | subject.connect 44 | end 45 | 46 | it "is open" do 47 | subject.connect 48 | expect(subject).to be_open 49 | end 50 | 51 | context "connection error" do 52 | it "is closed" 53 | end 54 | 55 | context "handshake error" do 56 | it "is closed" 57 | end 58 | 59 | context "read error" do 60 | it "is closed" 61 | end 62 | end 63 | 64 | it 'is not opened' do 65 | expect(subject).to_not be_open 66 | end 67 | 68 | it 'is closed' do 69 | expect(subject).to be_closed 70 | end 71 | 72 | it 'is completed' do 73 | expect(subject).to be_completed 74 | end 75 | 76 | describe "#close" do 77 | it "closes connection" do 78 | subject.connect 79 | subject.close 80 | expect(subject).to be_closed 81 | end 82 | 83 | it "results in completed connection" do 84 | subject.connect 85 | subject.close 86 | expect(subject).to be_completed 87 | end 88 | end 89 | 90 | describe "#send_data" do 91 | let(:socket) { double } 92 | 93 | before(:each) do 94 | # XXX we are recording socket, cannot add expections to it 95 | subject.instance_variable_set(:@socket, socket) 96 | 97 | # XXX stub data_frame as it will contain random data 98 | expect(subject).to receive(:data_frame).with("foobar", :text).and_return "frame" 99 | 100 | # setup connection state 101 | expect(subject).to receive(:handshaked?).and_return true 102 | expect(subject).to receive(:closed?).and_return false 103 | end 104 | 105 | it "sends data" do 106 | expect(socket).to receive(:write_nonblock).with("frame") 107 | 108 | subject.send_data("foobar") 109 | end 110 | 111 | context "error is thrown" do 112 | it "closes the connection asynchronously" do 113 | expect(socket).to receive(:write_nonblock).and_raise Errno::EPIPE 114 | expect(subject).to receive(:async_close).with(Errno::EPIPE) 115 | 116 | subject.send_data("foobar") 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/socket.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'socket' 3 | require 'openssl' 4 | 5 | module XRBP 6 | module WebSocket 7 | # Low level wrapper around TCPSocket operations, providing 8 | # mechanisms to negotiate base websocket connection. 9 | # 10 | # @private 11 | class Socket 12 | DEFAULT_PORTS = {:ws => 80, 13 | :http => 80, 14 | :wss => 443, 15 | :https => 443} 16 | 17 | 18 | attr_reader :pipe_broken 19 | 20 | attr_accessor :client 21 | 22 | private 23 | 24 | attr_reader :socket 25 | 26 | public 27 | 28 | def options 29 | client.options 30 | end 31 | 32 | def initialize(client) 33 | @client = client 34 | @pipe_broken = false 35 | end 36 | 37 | def connect 38 | uri = URI.parse client.url 39 | host = uri.host 40 | port = uri.port || DEFAULT_PORTS[uri.scheme.intern] 41 | 42 | @socket = TCPSocket.new(host, port) 43 | socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) 44 | 45 | init_ssl_socket if ['https', 'wss'].include? uri.scheme 46 | nil 47 | end 48 | 49 | private 50 | 51 | def init_ssl_socket 52 | ssl_context = options[:ssl_context] || begin 53 | ctx = OpenSSL::SSL::SSLContext.new 54 | ctx.ssl_version = options[:ssl_version] || 'SSLv23' 55 | 56 | # use VERIFY_PEER for verification: 57 | ctx.verify_mode = options[:verify_mode] || 58 | OpenSSL::SSL::VERIFY_NONE 59 | 60 | cert_store = OpenSSL::X509::Store.new 61 | cert_store.set_default_paths 62 | ctx.cert_store = cert_store 63 | 64 | ctx 65 | end 66 | 67 | @socket = ::OpenSSL::SSL::SSLSocket.new(socket, ssl_context) 68 | socket.connect 69 | end 70 | 71 | ### 72 | 73 | public 74 | 75 | def close 76 | socket.close if socket 77 | end 78 | 79 | def write(data) 80 | socket.write data 81 | end 82 | 83 | def write_nonblock(data) 84 | begin 85 | socket.write_nonblock(data) 86 | 87 | rescue IO::WaitReadable 88 | IO.select([socket]) # OpenSSL needs to read internally 89 | retry 90 | 91 | rescue IO::WaitWritable, Errno::EINTR 92 | IO.select(nil, [socket]) 93 | retry 94 | 95 | rescue Errno::EPIPE => e 96 | @pipe_broken = true 97 | raise 98 | 99 | rescue OpenSSL::SSL::SSLError => e 100 | @pipe_broken = true 101 | raise 102 | end 103 | end 104 | 105 | def read_next(dest) 106 | begin 107 | read_sockets, _, _ = IO.select([socket], nil, nil, 0.1) 108 | 109 | if read_sockets && read_sockets[0] 110 | dest << socket.read_nonblock(1024) 111 | 112 | if socket.respond_to?(:pending) # SSLSocket 113 | dest << socket.read(socket.pending) while socket.pending > 0 114 | end 115 | end 116 | rescue IO::WaitReadable 117 | # No op 118 | 119 | rescue IO::WaitWritable 120 | IO.select(nil, [socket]) 121 | retry 122 | end 123 | end 124 | end # class Socket 125 | end # module WebSocket 126 | end # module XRBP 127 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/shamap_spec.rb: -------------------------------------------------------------------------------- 1 | describe XRBP::SHAMap do 2 | describe "#each" do 3 | it "iterates over all SHAMap items" 4 | end 5 | 6 | describe "#succ" do 7 | it "returns next key in tree greater than specified one" 8 | 9 | context "next item is map end" do 10 | it "returns nil" 11 | end 12 | 13 | context "next item is >= last" do 14 | it "returns nil" 15 | end 16 | end 17 | 18 | describe "#read" do 19 | it "returns SLE read from database" 20 | 21 | context "specified key is zero" do 22 | it "raises error" 23 | end 24 | 25 | context "not found" do 26 | it "returns nil" 27 | end 28 | end 29 | 30 | describe "#peek_item" do 31 | it "returns item corresponding to database key" 32 | 33 | context "item not found" do 34 | it "returns nil" 35 | end 36 | end 37 | 38 | describe "#peek_first_item" do 39 | it "returns first item below root and stack" 40 | 41 | context "no item" do 42 | it "returns nil" 43 | end 44 | end 45 | 46 | describe "#peek_next_item" do 47 | it "returns next item in map" 48 | 49 | context "specified stack is empty" do 50 | it "raises error" 51 | end 52 | 53 | context "top of stack is not leaf" do 54 | it "raises error" 55 | end 56 | end 57 | 58 | describe "#fetch_node" do 59 | it "retrieves node from db" 60 | 61 | context "node not found" do 62 | it "raises error" 63 | end 64 | end 65 | 66 | describe "#fetch_node_nt" do 67 | it "retrieves node from db" 68 | 69 | context "node not found" do 70 | it "does not raise error" 71 | end 72 | end 73 | 74 | describe "#fetch_root" do 75 | it "sets @root database node and returns true" 76 | 77 | context "root already set" do 78 | it "returns true" 79 | end 80 | 81 | context "node not found" do 82 | it "returns false" 83 | end 84 | end 85 | 86 | describe "#find_key" do 87 | it "finds node corresponding to key in map" 88 | 89 | context "key not found" do 90 | it "returns nil" 91 | end 92 | end 93 | 94 | describe "#fetch_node_from_db" do 95 | it "retrieves key from nodestore" 96 | it "creates corresponding node" 97 | end 98 | 99 | describe "#upper_bound" do 100 | it "returns first item in tree with key > given key" 101 | end 102 | 103 | describe "#walk_towards_key" do 104 | it "returns path stack of node traversal to key" 105 | end 106 | 107 | describe "#descend_throw" do 108 | it "descends to parent branch" 109 | 110 | context "could not descend to non-empty branch" do 111 | it "raises error" 112 | end 113 | end 114 | 115 | describe "#descend" do 116 | it "retrieve cached child node at parent branch" 117 | 118 | context "parent branch child node not set" do 119 | it "fetches and returns node" 120 | 121 | context "not not able to be retrieved" do 122 | it "returns nil" 123 | end 124 | 125 | context "node is inconsistent" do 126 | it "returns nil" 127 | end 128 | 129 | it "stores child in parent" 130 | end 131 | end 132 | 133 | describe "#first_below" do 134 | it "returns first leaf node at or below the specified node" 135 | end 136 | 137 | describe "#cdir_first" do 138 | it "returns the first directory index in the node specified by index" 139 | end 140 | 141 | describe "#cdir_next" do 142 | it "returns the next directory index in the node specified by index" 143 | end 144 | end # describe XRBP::SHAMap 145 | -------------------------------------------------------------------------------- /lib/xrbp/overlay/connection.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | # The Overlay is the Peer-to-Peer (P2P) network established 3 | # by rippled node instances to each other. It is what is used 4 | # to relay transactions and network state as the consensus 5 | # process is executed. 6 | # 7 | # This module facilitates communication with the Overlay P2P 8 | # network from Ruby. 9 | module Overlay 10 | 11 | # Primary Overlay Connection Interface, use Connection 12 | # to send and receive Peer-To-Peer data over the Overlay. 13 | # 14 | # @example establishing a connection, reading frames 15 | # overlay = XRBP::Overlay::Connection.new "127.0.0.1", 51235 16 | # overlay.connect 17 | # 18 | # overlay.read_frames do |frame| 19 | # puts "Message: #{frame.type_name} (#{frame.size} bytes)" 20 | # end 21 | class Connection 22 | attr_reader :host, :port 23 | attr_accessor :node 24 | 25 | def initialize(host, port) 26 | @host = host 27 | @port = port 28 | @node = Crypto.node 29 | end 30 | 31 | # @private 32 | def socket 33 | @socket ||= TCPSocket.open(host, port) 34 | end 35 | 36 | # Indicates if the connection is closed 37 | def closed? 38 | socket.closed? 39 | end 40 | 41 | # @private 42 | def ssl_socket 43 | @ssl_socket ||= begin 44 | ssl_context = OpenSSL::SSL::SSLContext.new 45 | ssl_context.ssl_version = :SSLv23 46 | ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE 47 | 48 | _ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context) 49 | _ssl_socket.sync_close = true 50 | 51 | _ssl_socket 52 | end 53 | end 54 | 55 | # @private 56 | def handshake 57 | @handshake ||= Handshake.new self 58 | end 59 | 60 | ### 61 | 62 | # Initiate new connection to peer 63 | def connect 64 | ssl_socket.connect 65 | handshake.execute! 66 | end 67 | 68 | # Close the connection to peer 69 | def close! 70 | ssl_socket.close 71 | end 72 | 73 | alias :close :close! 74 | 75 | # Send raw data via this connection 76 | def write(data) 77 | ssl_socket.puts(data) 78 | end 79 | 80 | def write_frame(msg) 81 | write(Frame.from_msg(msg)) 82 | end 83 | 84 | def write_msg(data) 85 | write_frame(Overlay.create_msg(data)) 86 | end 87 | 88 | # Read raw data from connection 89 | def read 90 | ssl_socket.gets 91 | end 92 | 93 | # Read frames from connection until closed, invoking 94 | # passed block with each. 95 | def read_frames 96 | frame = nil 97 | remaining = nil 98 | while !closed? 99 | read_sockets, _, _ = IO.select([ssl_socket], nil, nil, 0.1) 100 | if read_sockets && read_sockets[0] 101 | out = ssl_socket.read_nonblock(1024) 102 | 103 | if frame.nil? 104 | type = Frame::TYPE_INFER.decode(out) 105 | frame = Frame.new type["type"], type["size"] 106 | out = out[Frame::TYPE_INFER.size..-1] 107 | end 108 | 109 | _, remaining = frame << out 110 | if frame.complete? 111 | yield frame 112 | frame = nil 113 | end 114 | 115 | # static assertion: should have no more data 116 | raise unless remaining.nil? || remaining.empty? 117 | end 118 | end 119 | end 120 | end # class Connection 121 | end # module Overlay 122 | end # module XRBP 123 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/protocol/indexes.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | # Return DB lookup indices for the following artifacts 4 | module Indexes 5 | 6 | def self.get_quality(base) 7 | # FIXME: assuming native platform is big endian, 8 | # need to account for all platforms 9 | base[-8..-1].to_bn 10 | end 11 | 12 | def self.get_quality_next(base) 13 | nxt = "10000000000000000".to_i(16) 14 | (base.to_bn + nxt).byte_string 15 | end 16 | 17 | ### 18 | 19 | def self.dir_node_index(root, index) 20 | return root if index == 0 21 | 22 | sha512 = OpenSSL::Digest::SHA512.new 23 | sha512 << "\0" 24 | sha512 << Format::LEDGER_NAMESPACE[:dir_node] 25 | sha512 << root 26 | sha512 << index.bytes.rjust!(8, 0).pack("C*") 27 | 28 | sha512.digest[0..31] 29 | end 30 | 31 | def self.page(key, index) 32 | dir_node_index key, index 33 | end 34 | 35 | # Account index from id 36 | def self.account(id) 37 | id = Crypto.account_id(id) 38 | 39 | sha512 = OpenSSL::Digest::SHA512.new 40 | sha512 << "\0" 41 | sha512 << Format::LEDGER_NAMESPACE[:account] 42 | sha512 << id 43 | 44 | sha512.digest[0..31] 45 | end 46 | 47 | # TODO: Account Owner Dir from id 48 | def self.owner_dir(id) 49 | end 50 | 51 | # TODO: Offer Index for account id and seq 52 | def self.offer_index(id, seq) 53 | end 54 | 55 | # Trust line for account/iou 56 | def self.line(account, iou) 57 | account = Crypto.account_id(account) 58 | issuer = Crypto.account_id(iou[:account]) 59 | 60 | sha512 = OpenSSL::Digest::SHA512.new 61 | sha512 << "\0" 62 | sha512 << Format::LEDGER_NAMESPACE[:ripple] 63 | 64 | if account.to_bn < issuer.to_bn 65 | sha512 << account 66 | sha512 << issuer 67 | 68 | else 69 | sha512 << issuer 70 | sha512 << account 71 | end 72 | 73 | sha512 << Format.encode_currency(iou[:currency]) 74 | 75 | sha512.digest[0..31] 76 | end 77 | 78 | # Order book index for given input/output 79 | def self.order_book(input, output) 80 | input = Hash[input] 81 | output = Hash[output] 82 | 83 | # Currency always upcase 84 | input[:currency].upcase! 85 | output[:currency].upcase! 86 | 87 | # If currency == 'XRP' set corresponding issuer 88 | input[:account] = Crypto.xrp_account if input[:currency] == 'XRP' 89 | output[:account] = Crypto.xrp_account if output[:currency] == 'XRP' 90 | 91 | # Convert currency to binary representation 92 | input[:currency] = Format.encode_currency(input[:currency]) 93 | output[:currency] = Format.encode_currency(output[:currency]) 94 | 95 | # convert input / output account to binary representation 96 | input[:account] = Crypto.account_id(input[:account]) 97 | output[:account] = Crypto.account_id(output[:account]) 98 | 99 | book_base = ["\0", Format::LEDGER_NAMESPACE[:book_dir], 100 | input[:currency], output[:currency], 101 | input[:account], output[:account]].join 102 | 103 | sha512 = OpenSSL::Digest::SHA512.new 104 | book_base = sha512.digest(book_base)[0..31] 105 | 106 | # XXX: get_quality_index shorthand: 107 | book_base[-8..-1] = [0, 0, 0, 0, 0, 0, 0, 0].pack("C*") 108 | book_base 109 | end 110 | end # module Indexes 111 | end # module NodeStore 112 | end # module XRBP 113 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/backends/nudb.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | # requires rrudb gem 4 | require "rudb" 5 | 6 | require_relative './decompressor' 7 | 8 | module XRBP 9 | module NodeStore 10 | module Backends 11 | # NuDB nodestore backend, faciliates accessing XRP Ledger data 12 | # in a NuDB database. This module accommodates for compression 13 | # used in rippled's NuDB nodestore backend implementation 14 | # 15 | # @example retrieve data from NuDB backend 16 | # require 'nodestore/backends/nudb' 17 | # nudb = NodeStore::Backends::NuDB.new '/var/lib/rippled/db/nudb' 18 | # puts nudb.ledger('B506ADD630CB707044B4BFFCD943C1395966692A13DD618E5BD0978A006B43BD') 19 | class NuDB < DB 20 | include Decompressor 21 | 22 | attr_reader :path 23 | 24 | KEY_SIZE = 32 25 | 26 | def initialize(path) 27 | @path = path 28 | create! 29 | open 30 | end 31 | 32 | # Retrieve database value for the specified key 33 | # 34 | # @param key [String] binary key to lookup 35 | # @return [String] binary value 36 | def [](key) 37 | fetched = @store.fetch(key)[0] 38 | return nil if fetched.empty? 39 | decompress(fetched) 40 | end 41 | 42 | # Iterate over each database key/value pair, 43 | # invoking callback. During iteration will 44 | # emit signals specific to the DB types being 45 | # parsed 46 | # 47 | # @example iterating over NuDB entries 48 | # nudb.each do |iterator| 49 | # puts "Key/Value: #{iterator.key}/#{iterator.value}" 50 | # end 51 | # 52 | # @example handling ledgers via event callback 53 | # nudb.on(:ledger) do |hash, ledger| 54 | # puts "Ledger #{hash}" 55 | # pp ledger 56 | # end 57 | # 58 | # # Any Enumerable method that invokes #each will 59 | # # have intended effect 60 | # nudb.to_a 61 | def each 62 | dat = File.join(path, "nudb.dat") 63 | 64 | RuDB::each(dat) do |key, val| 65 | val = decompress(val) 66 | type, obj = infer_type(val) 67 | 68 | if type 69 | emit type, key, obj 70 | else 71 | emit :unknown, key, val 72 | end 73 | 74 | # 'mock' iterator 75 | iterator = OpenStruct.new(:key => key, 76 | :value => val) 77 | yield iterator 78 | end 79 | 80 | return self 81 | end 82 | 83 | private 84 | 85 | # Create database if it does not exist 86 | # 87 | # @private 88 | def create! 89 | dat = File.join(path, "nudb.dat") 90 | key = File.join(path, "nudb.key") 91 | log = File.join(path, "nudb.log") 92 | 93 | RuDB::create :dat_path => dat, 94 | :key_path => key, 95 | :log_path => log, 96 | :app_num => 1, 97 | :salt => RuDB::make_salt, 98 | :key_size => KEY_SIZE, 99 | :block_size => RuDB::block_size(key), 100 | :load_factor => 0.5 101 | end 102 | 103 | # Open existing database 104 | # 105 | # @private 106 | def open 107 | dat = File.join(path, "nudb.dat") 108 | key = File.join(path, "nudb.key") 109 | log = File.join(path, "nudb.log") 110 | 111 | @store = RuDB::Store.new 112 | @store.open(dat, key, log) 113 | end 114 | end # class NuDB 115 | end # module Backends 116 | end # module NodeStore 117 | end # module XRBP 118 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/sle/st_amount_arithmatic.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module NodeStore 3 | class STAmount 4 | module Arithmatic 5 | def +(v) 6 | return self + STAmount.new(:mantissa => v) if v.kind_of?(Numeric) 7 | 8 | e1 = exponent 9 | e2 = v.exponent 10 | 11 | m1 = mantissa 12 | m2 = v.mantissa 13 | 14 | m1 *= -1 if neg 15 | m2 *= -1 if v.neg 16 | 17 | while e1 < e2 18 | m1 /= 10 19 | e1 += 1 20 | end 21 | 22 | while e2 < e1 23 | m2 /= 10 24 | e2 += 1 25 | end 26 | 27 | m = m1 + m2 28 | return STAmount.new :issue => issue if m >= -10 && m <= 10 29 | return STAmount.new :mantissa => m, 30 | :exponent => e1, 31 | :issue => issue if m >= 0 32 | return STAmount.new :mantissa => -m, 33 | :exponent => e1, 34 | :issue => issue 35 | end 36 | 37 | def -(v) 38 | self + (-v) 39 | end 40 | 41 | def /(v) 42 | return self / STAmount.new(:mantissa => v) if v.kind_of?(Numeric) 43 | 44 | if v.is_a?(Rate) 45 | return self if v == Rate.parity 46 | return self / v.to_amount 47 | end 48 | 49 | raise "divide by zero" if v.zero? 50 | return STAmount.new :issue => issue if zero? 51 | 52 | nm = mantissa 53 | dm = v.mantissa 54 | 55 | ne = exponent 56 | de = v.exponent 57 | 58 | if native? 59 | while nm < MIN_VAL 60 | nm *= 10 61 | ne -= 1 62 | end 63 | end 64 | 65 | if v.native? 66 | while dm < MIN_VAL 67 | dm *= 10 68 | de -= 1 69 | end 70 | end 71 | 72 | # see note: https://github.com/ripple/rippled/blob/b53fda1e1a7f4d09b766724274329df1c29988ab/src/ripple/protocol/impl/STAmount.cpp#L1075 73 | STAmount.new :issue => issue, 74 | :mantissa => (nm * 10**17)/dm + 5, 75 | :exponent => (ne - de - 17), 76 | :neg => (neg != v.neg) 77 | end 78 | 79 | def *(o) 80 | return self * STAmount.new(:mantissa => o) if o.kind_of?(Numeric) 81 | 82 | return STAmount.new :issue => issue if zero? || o.zero? 83 | 84 | if native? && o.native? 85 | min = sn_value < o.sn_value ? sn_value : o.sn_value 86 | max = sn_value < o.sn_value ? o.sn_value : sn_value 87 | 88 | return STAmount.new :mantissa => min * max 89 | end 90 | 91 | m1 = mantissa 92 | m2 = o.mantissa 93 | e1 = exponent 94 | e2 = o.exponent 95 | 96 | if native? 97 | while nm < MIN_VAL 98 | m1 *= 10 99 | e1 -= 1 100 | end 101 | end 102 | 103 | if o.native? 104 | while dm < MIN_VAL 105 | m2 *= 10 106 | e2 -= 1 107 | end 108 | end 109 | 110 | # see note: https://github.com/ripple/rippled/blob/b53fda1e1a7f4d09b766724274329df1c29988ab/src/ripple/protocol/impl/STAmount.cpp#L1131 111 | STAmount.new :issue => issue, 112 | :mantissa => (m1 * m2)/(10**14) + 7, 113 | :exponent => (e1 + e2 + 14), 114 | :neg => (neg != o.neg) 115 | end 116 | 117 | def -@ 118 | STAmount.new(:mantissa => mantissa, 119 | :exponent => exponent, 120 | :issue => issue, 121 | :neg => !neg) 122 | end 123 | end # module Arithmatic 124 | end # class STAmount 125 | end # module NodeStore 126 | end # module XRBP 127 | -------------------------------------------------------------------------------- /lib/xrbp/nodestore/shamap/node_factory.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | class SHAMap 3 | module NodeFactory 4 | # Create a new node whose type is specified in the binary prefix. 5 | # 6 | # See rippled::SHAMapAbstractNode::make 7 | def make(node, seq, format, hash, hash_valid) 8 | node_id = NodeID.new 9 | 10 | if format == :wire 11 | # TODO 12 | 13 | elsif format == :prefix 14 | raise if node.size < 4 15 | 16 | prefix = node[0].ord 17 | prefix <<= 8 18 | prefix |= node[1].ord 19 | prefix <<= 8 20 | prefix |= node[2].ord 21 | prefix <<= 8 22 | prefix |= node[3].ord 23 | prefix = prefix.to_s(16).upcase 24 | 25 | s = node[4..-1] 26 | 27 | # Transaction node (no metadata) 28 | if prefix == NodeStore::Format::HASH_PREFIXES[:tx_id] 29 | sha512 = OpenSSL::Digest::SHA512.new 30 | sha512 << NodeStore::Format::HASH_PREFIXES[:tx_id] 31 | sta512 << node 32 | key = sha512.digest[0..31] 33 | 34 | item = Item.new(:key => key, 35 | :data => node) 36 | 37 | tree_node = {:item => item, 38 | :seq => seq, 39 | :type => :transaction_nm} 40 | tree_node[:hash] = hash if hash_valid 41 | 42 | return TreeNode.new(tree_node) 43 | 44 | # Leaf node in state tree containing data (account info, order, etc) 45 | elsif prefix == NodeStore::Format::HASH_PREFIXES[:leaf_node] 46 | raise "short PLN node" if s.size < 32 47 | 48 | u = s[-32..-1] 49 | s = s[0..-33] 50 | raise "invalid PLN node" if u.zero? 51 | 52 | item = Item.new(:key => u, 53 | :data => s) 54 | 55 | tree_node = {:item => item, 56 | :seq => seq, 57 | :type => :account_state} 58 | tree_node[:hash] = hash if hash_valid 59 | 60 | return TreeNode.new(tree_node) 61 | 62 | # Inner tree node referencing other nodes 63 | elsif (prefix == NodeStore::Format::HASH_PREFIXES[:inner_node]) || 64 | (prefix == NodeStore::Format::HASH_PREFIXES[:inner_node_v2]) 65 | len = s.size 66 | isv2 = prefix == NodeStore::Format::HASH_PREFIXES[:inner_node_v2] 67 | 68 | raise "invalid PIN node" if len < 512 || 69 | (!isv2 && (len != 512)) || 70 | ( isv2 && (len == 512)) 71 | 72 | ret = InnerNode.new :v2 => isv2 73 | 74 | 0.upto(15) { |i| 75 | ret.hashes[i] = s[i*32...(i+1)*32] 76 | ret.is_branch |= (1 << i) unless ret.hashes[i].zero? 77 | } 78 | 79 | if isv2 80 | ret.depth = s[512] 81 | n = (ret.depth + 1)/2 82 | raise "invalid PIN node" if len != 512 + 1 + n 83 | 84 | 0.upto(n-1) { |i| 85 | ret.common << s[512+1+i] 86 | } 87 | end 88 | 89 | if hash_valid 90 | ret.hash = hash 91 | else 92 | ret.update_hash 93 | end 94 | 95 | return ret 96 | 97 | # Transaction node (with metadata) 98 | elsif prefix == NodeStore::Format::HASH_PREFIXES[:tx_node] 99 | raise "short TXN node" if s.size < 32 100 | 101 | tx_id = s[-32..-1] 102 | # XXX: tx_id is last field in binary transaction, keep so 103 | # it can be parsed w/ other fields later: 104 | #s = s[0..-33] 105 | 106 | item = Item.new(:key => tx_id, 107 | :data => s) 108 | 109 | tree_node = {:item => item, 110 | :seq => seq, 111 | :type => :transaction_md} 112 | tree_node[:hash] = hash if hash_valid 113 | 114 | return TreeNode.new(tree_node) 115 | 116 | else 117 | raise "Unknown prefix #{prefix}" 118 | end 119 | end 120 | 121 | raise "Unknown format" 122 | end 123 | end # module NodeFactory 124 | end # class SHAMap 125 | end # module XRBP 126 | -------------------------------------------------------------------------------- /lib/xrbp/model/node.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'resolv' 3 | 4 | require_relative './parsers/node' 5 | 6 | module XRBP 7 | module Model 8 | class Node < Base 9 | extend Base::ClassMethods 10 | 11 | DEFAULT_CRAWL_PORT = 51235 12 | 13 | attr_accessor :ip, :port 14 | attr_accessor :addr, :version, :uptime, :type, :ledgers 15 | 16 | # Return unique node id 17 | def id 18 | "#{ip}:#{port}" 19 | end 20 | 21 | # Return node url 22 | def url 23 | "https://#{ip}:#{port}/crawl" 24 | end 25 | 26 | # Return bool indicating if this node is valid for crawling 27 | def valid? 28 | return false unless ip && port 29 | 30 | # ensure no parsing errs 31 | begin 32 | # FIXME URI.parse is limiting our ability to traverse entire node-set, 33 | # some nodes are represented as IPv6 addresses which is throwing 34 | # things off. 35 | URI.parse(url) 36 | rescue 37 | false 38 | end 39 | 40 | true 41 | end 42 | 43 | def ==(o) 44 | ip == o.ip && port == o.port 45 | end 46 | 47 | # Return new node from the specified url 48 | # 49 | # @param url [String] node url 50 | # @return [Node] new node instance 51 | def self.parse_url(url) 52 | n = new 53 | 54 | uri = URI.parse(url) 55 | n.ip = Resolv.getaddress(uri.host) 56 | n.port = uri.port 57 | 58 | n 59 | end 60 | 61 | # Return new node from the specified peer object 62 | # 63 | # @param p [Hash] peer data 64 | # @return [Node] new node instance 65 | def self.from_peer(p) 66 | n = new 67 | 68 | n.addr = p["public_key"] 69 | n.ip = p["ip"]&.gsub("::ffff:", "") 70 | n.port = p["port"] || DEFAULT_CRAWL_PORT 71 | n.version = p["version"].split("-").last 72 | n.uptime = p["uptime"] 73 | n.type = p["type"] 74 | n.ledgers = p["complete_ledgers"] 75 | 76 | n 77 | end 78 | 79 | # Crawl nodes via WebClient::Connection 80 | # 81 | # @param opts [Hash] options to crawl nodes with 82 | # @option opts [WebSocket::Connection] :connection Connection 83 | # to use to crawl nodes 84 | # @option opts [Integer] :delay optional delay to wait between 85 | # crawl iterations 86 | def self.crawl(start, opts={}) 87 | set_opts(opts) 88 | delay = opts[:delay] || 1 89 | 90 | queue = Array.new 91 | queue << start 92 | 93 | connection.add_plugin :result_parser unless connection.plugin?(:result_parser) 94 | connection.add_plugin Parsers::NodePeers unless connection.plugin?(Parsers::NodePeers) 95 | 96 | connection.ssl_verify_peer = false 97 | connection.ssl_verify_host = false 98 | 99 | until connection.force_quit? 100 | node = queue.shift 101 | node = parse_url node unless node.is_a?(Node) 102 | 103 | connection.emit :precrawl, node 104 | connection.url = node.url 105 | 106 | peers = connection.perform 107 | if peers.nil? || peers.empty? 108 | queue << node 109 | connection.emit :crawlerr, node 110 | connection.rsleep(delay) unless connection.force_quit? 111 | next 112 | end 113 | 114 | connection.emit :peers, node, peers 115 | peers.each { |peer| 116 | break if connection.force_quit? 117 | 118 | peer = Node.from_peer peer 119 | next unless peer.valid? # skip unless valid 120 | 121 | connection.emit :peer, node, peer 122 | queue << peer unless queue.include?(peer) 123 | } 124 | 125 | queue << node 126 | connection.emit :postcrawl, node 127 | connection.rsleep(delay) unless connection.force_quit? 128 | end 129 | end 130 | 131 | # Retrieve server info via WebSocket::Connection 132 | def server_info(opts={}, &bl) 133 | set_opts(opts) 134 | connection.cmd(WebSocket::Cmds::ServerInfo.new, &bl) 135 | end 136 | 137 | # Retrieve ledgers which this server has 138 | def complete_ledgers(opts={}) 139 | server_info(opts)["result"]["info"]["complete_ledgers"].split("-").collect { |l| l.to_i } 140 | end 141 | end 142 | end # module Model 143 | end # module XRBP 144 | -------------------------------------------------------------------------------- /spec/xrbp/nodestore/ledger_access.rb: -------------------------------------------------------------------------------- 1 | shared_examples "ledger access" do |opts={}| 2 | let(:ledger_hash) { ["32E073D7E4D722D956F7FDE095F756FBB86DC9CA487EB0D9ABF5151A8D88F912"].pack("H*") } 3 | let(:ledger) { XRBP::NodeStore::Ledger.new(:db => db, :hash => ledger_hash) } 4 | 5 | let(:issuer) { 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' } 6 | let(:iou1) { {:currency => 'USD', :account => issuer} } 7 | let(:iou2) { {:currency => 'EUR', :account => issuer} } 8 | 9 | let(:order1) { { 10 | :ledger_entry_type=>:offer, 11 | :flags=>131072, 12 | :sequence=>219, 13 | :previous_txn_lgr_seq=>47926685, 14 | :book_node=>0, 15 | :owner_node=>0, 16 | :previous_txn_id=>"e43add1bd4ac2049e0d9de6bc279b7fd95a99c8de2c4694a4a7623f6d9aaae29", 17 | :book_directory=>"7e5f614417c2d0a7cefeb73c4aa773ed5b078de2b5771f6d56038d7ea4c68000", 18 | :taker_pays=>XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("USD", "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), :mantissa => 2459108753792364, :exponent => -14), 19 | :taker_gets => XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("EUR", "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), :mantissa => 2459108753792364, :exponent => -15), 20 | :account=>"rnixnrMHHvR7ejMpJMRCWkaNrq3qREwMDu", 21 | :owner_funds => XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("EUR", XRBP::Crypto.no_account), :mantissa => 2872409153061363, :exponent => -15), 22 | :quality => XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore.no_issue, :mantissa => 1000000000000000, :exponent => -14), 23 | } } 24 | 25 | let(:order2) { { 26 | :ledger_entry_type=>:offer, 27 | :flags=>131072, 28 | :sequence=>19, 29 | :previous_txn_lgr_seq=>43166305, 30 | :book_node=>0, 31 | :owner_node=>0, 32 | :previous_txn_id=>"b63b2ecd124fe6b02bc2998929517266bd221a02fee51dde4992c1bcb7e86cd3", 33 | :book_directory=>"7e5f614417c2d0a7cefeb73c4aa773ed5b078de2b5771f6d56038d7ea4c68000", 34 | :taker_pays=>XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("USD", "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), :mantissa => 3520000000000000, :exponent => -14), 35 | :taker_pays_funded=>XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("USD", "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), :mantissa => 3516160294182094, :exponent => -14), 36 | :taker_gets => XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("EUR", "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"), :mantissa => 3520000000000000, :exponent => -15), :account=>"rKwjWCKBaASEvtHCxtvReNd2i9n8DxSihk", 37 | :taker_gets_funded => XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("EUR", XRBP::Crypto.no_account), :mantissa => 3516160294182094, :exponent => -15), :account=>"rKwjWCKBaASEvtHCxtvReNd2i9n8DxSihk", 38 | :owner_funds => XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore::Issue.new("EUR", XRBP::Crypto.no_account), :mantissa => 3523192614770459, :exponent => -15), 39 | :quality => XRBP::NodeStore::STAmount.new(:issue => XRBP::NodeStore.no_issue, :mantissa => 1000000000000000, :exponent => -14), 40 | } } 41 | 42 | let(:order_book) { [order1, order2] } 43 | 44 | it "provides access to ledger state map" 45 | 46 | it "provides access to ledger tx map" 47 | 48 | it "provides access to ledger info" 49 | 50 | describe "#global_frozen?" do 51 | context "account globally frozen in ledger" do 52 | it "returns true" 53 | end 54 | 55 | context "account not globally frozen in ledger" do 56 | it "returns false" 57 | end 58 | end 59 | 60 | describe "#frozen?" do 61 | context "account trust line frozen" do 62 | it "returns true" 63 | end 64 | 65 | context "account trust line not frozen" do 66 | it "returns false" 67 | end 68 | end 69 | 70 | describe "#account_holds" do 71 | it "returns balance of account iou trust line" 72 | 73 | context "currency == 'XRP'" do 74 | it "returns liquid xrp" 75 | end 76 | end 77 | 78 | describe "#xrp_liquid" do 79 | context "account not found" do 80 | it "returns zero amount minus reserve" 81 | end 82 | 83 | it "returns xrp account balance minus reserve" 84 | 85 | context "fix1141 in effect" do 86 | it "confines owner account" 87 | end 88 | end 89 | 90 | describe "#confine_owner_account" do 91 | it "adjusts and returns amount" 92 | end 93 | 94 | describe "#transfer_rate" do 95 | it "returns transfer rate for specified issuer" 96 | end 97 | 98 | it "provides access to order book" do 99 | actual = ledger.order_book(iou1, iou2) 100 | expect(actual).to eq(order_book) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/client.rb: -------------------------------------------------------------------------------- 1 | require 'websocket' 2 | 3 | module XRBP 4 | module WebSocket 5 | # Managed socket connection lifecycle and read/write operations 6 | # 7 | # @private 8 | class Client 9 | include EventEmitter 10 | include Terminatable 11 | 12 | attr_reader :url, :options 13 | 14 | def initialize(url, options={}) 15 | @url = url 16 | @options = options 17 | 18 | @handshaked = false 19 | @closed = true 20 | @completed = true 21 | end 22 | 23 | def connect 24 | emit_signal :connecting 25 | 26 | @closed = false 27 | @completed = false 28 | socket.connect 29 | handshake! 30 | 31 | start_read 32 | 33 | self 34 | end 35 | 36 | # Add job to internal thread pool. 37 | def add_work(&bl) 38 | pool.post &bl 39 | end 40 | 41 | ### 42 | 43 | def open? 44 | handshake.finished? and !closed? 45 | end 46 | 47 | def closed? 48 | !!@closed 49 | end 50 | 51 | def completed? 52 | !!@completed 53 | end 54 | 55 | # Allow close to be run via seperate thread so 56 | # as not to block caller 57 | def async_close(err=nil) 58 | Thread.new { close(err) } 59 | end 60 | 61 | def close(err=nil) 62 | return if closed? 63 | 64 | # XXX set closed true first incase callbacks need to check this 65 | @closed = true 66 | @handshake = nil 67 | @handshaked = false 68 | 69 | terminate! 70 | 71 | send_data nil, :type => :close unless socket.pipe_broken 72 | emit_signal :close, err 73 | 74 | socket.close if socket 75 | @socket = nil 76 | 77 | pool.shutdown 78 | pool.wait_for_termination 79 | @pool = nil 80 | 81 | @completed = true 82 | emit :completed 83 | self 84 | end 85 | 86 | private 87 | 88 | ### 89 | 90 | def socket 91 | @socket ||= Socket.new self 92 | end 93 | 94 | def pool 95 | @pool ||= Concurrent::CachedThreadPool.new 96 | end 97 | 98 | ### 99 | 100 | def handshake 101 | @handshake ||= ::WebSocket::Handshake::Client.new :url => url, 102 | :headers => options[:headers] 103 | end 104 | 105 | def handshaked? 106 | !!@handshaked 107 | end 108 | 109 | def handshake! 110 | socket.write handshake.to_s 111 | 112 | until handshaked? || closed? #|| connection.force_quit? 113 | socket.read_next handshake 114 | @handshaked = handshake.finished? 115 | end 116 | end 117 | 118 | ### 119 | 120 | def data_frame(data, type) 121 | ::WebSocket::Frame::Outgoing::Client.new(:data => data, 122 | :type => type, 123 | :version => handshake.version) 124 | end 125 | 126 | public 127 | 128 | def send_data(data, opt={:type => :text}) 129 | return if !handshaked? || closed? 130 | 131 | begin 132 | frame = data_frame(data, opt[:type]) 133 | socket.write_nonblock(frame.to_s) 134 | 135 | rescue Errno::EPIPE, OpenSSL::SSL::SSLError => e 136 | async_close(e) 137 | end 138 | end 139 | 140 | private 141 | 142 | def start_read 143 | add_work do 144 | frame = ::WebSocket::Frame::Incoming::Client.new 145 | emit_signal :open 146 | 147 | cl = trm = eof = false 148 | until (trm = terminate?) || (cl = closed?) do 149 | begin 150 | socket.read_next(frame) 151 | 152 | if msg = frame.next 153 | emit_signal :message, msg 154 | frame = ::WebSocket::Frame::Incoming::Client.new 155 | end 156 | 157 | rescue EOFError => e 158 | emit_signal :error, e 159 | eof = e 160 | 161 | rescue => e 162 | emit_signal :error, e 163 | end 164 | end 165 | 166 | # ... is this right?: 167 | async_close(eof) if !!eof && !cl && !trm 168 | end 169 | end 170 | 171 | def emit_signal(*args) 172 | # TODO add args to queue, and in add_work task, pull 1 item off queue 173 | # & emit it (to enforce signal order) 174 | begin 175 | add_work do 176 | emit *args 177 | end 178 | 179 | # XXX: handle race condition where connection is closed 180 | # between calling emit_signal and pool.post (handle 181 | # error, otherwise a mutex would be needed) 182 | rescue Concurrent::RejectedExecutionError => e 183 | raise e unless closed? 184 | end 185 | end 186 | end # class Client 187 | end # module WebSocket 188 | end # module XRBP 189 | -------------------------------------------------------------------------------- /lib/xrbp/crypto/key.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module XRBP 4 | module Crypto 5 | module Key 6 | TOKEN_TYPES = { 7 | :none => 1, # unused 8 | :node_public => 28, 9 | :node_private => 32, 10 | :account_id => 0, 11 | :account_public => 35, 12 | :account_secret => 34, 13 | :family_generator => 41, 14 | :family_seed => 33 15 | } 16 | 17 | ### 18 | 19 | # @return [Hash] new secp256k1 key pair (both public and private components) 20 | def self.secp256k1(seed=nil) 21 | # XXX: the bitcoin secp256k1 implementation (which rippled pulls in / vendors) 22 | # has alot of nuances which require special configuration in openssl. For 23 | # the time being, mitigate this by pulling in & using the ruby 24 | # btc-secp256k1 bindings: 25 | # https://github.com/cryptape/ruby-bitcoin-secp256k1 26 | # 27 | # Perhaps at some point, we can look into implementing this logic in pure-ruby: 28 | # https://medium.com/coinmonks/introduction-to-blockchains-bedrock-the-elliptic-curve-secp256k1-e4bd3bc17d 29 | require 'secp256k1' 30 | 31 | spk = Secp256k1::PrivateKey.new 32 | 33 | sd, pk = nil 34 | if seed 35 | sd,pk = seed,seed 36 | 37 | else 38 | sd = Crypto.seed[:seed] 39 | pk = Crypto.parse_seed(sd) 40 | end 41 | 42 | # FIXME: rippled & ripple-keypairs (& by extension ripple-lib) repeatedly 43 | # hash seed until certain it is less than the order of the 44 | # curve, we should do this as well (for security) 45 | # 46 | # https://github.com/ripple/rippled/blob/develop/src/ripple/crypto/impl/GenerateDeterministicKey.cpp 47 | # https://github.com/ripple/ripple-keypairs/blob/master/src/secp256k1.js 48 | # 49 | # Also look into if setting raw key here has same effect as 50 | # secp256k1.mul (as invoked in keypairs) 51 | sha512 = OpenSSL::Digest::SHA512.new 52 | pk = sha512.digest(pk)[0..31] 53 | 54 | spk.set_raw_privkey pk 55 | 56 | { :public => spk.pubkey.serialize.unpack("H*").first, 57 | :private => spk.send(:serialize), 58 | :seed => sd, 59 | :type => :secp256k1 } 60 | end 61 | 62 | # @return [Hash] new ed25519 key pair (both public and private components) 63 | def self.ed25519(seed=nil) 64 | # XXX openssl 1.1.1 needed for EdDSA support: 65 | # https://www.openssl.org/blog/blog/2018/09/11/release111/ 66 | # Until then use this: 67 | require "ed25519" 68 | 69 | sd, pk = nil 70 | if seed 71 | sd,pk = seed,seed 72 | 73 | else 74 | sd = Crypto.seed[:seed] 75 | pk = Crypto.parse_seed(sd) 76 | end 77 | 78 | sha512 = OpenSSL::Digest::SHA512.new 79 | pk = sha512.digest(pk)[0..31] 80 | 81 | key = Ed25519::SigningKey.new(pk) 82 | { :public => key.verify_key.to_bytes.unpack("H*").first.upcase, 83 | :private => key.to_bytes.unpack("H*").first.upcase, 84 | :seed => sd, 85 | :type => :ed25519 } 86 | end 87 | 88 | ### 89 | 90 | # Sign the digest using the specified key, returning the result 91 | # 92 | # @param key [Hash] key to sign digest with 93 | # @param data [String] data to sign (must be exactly 32 bytes long!) 94 | # @return [String] signed digest 95 | def self.sign_digest(key, data) 96 | raise "unknown key" unless key.is_a?(Hash) && key[:type] && key[:private] 97 | raise "invalid data" unless data.length == 32 98 | 99 | if key[:type] == :secp256k1 100 | require 'secp256k1' 101 | 102 | pk = Secp256k1::PrivateKey.new 103 | pk.set_raw_privkey [key[:private]].pack("H*") 104 | sig_raw = pk.ecdsa_sign data, raw: true 105 | return pk.ecdsa_serialize sig_raw 106 | 107 | elsif key[:type] == :ed25519 108 | require "ed25519" 109 | 110 | sd = key[:seed] 111 | pk = Crypto.parse_seed(sd) 112 | 113 | sha512 = OpenSSL::Digest::SHA512.new 114 | pk = sha512.digest(pk)[0..31] 115 | 116 | pk = Ed25519::SigningKey.new(pk) 117 | return pk.sign(data) 118 | end 119 | 120 | raise "unknown key type" 121 | end 122 | 123 | # Returns bool indicating if data is the result of 124 | # signing expected value with given key. 125 | # 126 | # @param key [Hash] key to use to verify digest 127 | # @param data [String] signed data 128 | # @param expected [String] original unsigned data 129 | # @return [Bool] indicating if signed digest matches 130 | # original data 131 | def self.verify(key, data, expected) 132 | if key[:type] == :secp256k1 133 | require 'secp256k1' 134 | 135 | pb = Secp256k1::PublicKey.new :pubkey => [key[:public]].pack("H*"), 136 | :raw => true 137 | pv = Secp256k1::PrivateKey.new 138 | 139 | return pb.ecdsa_verify expected, 140 | pv.ecdsa_deserialize(data), raw: true 141 | 142 | elsif key[:type] == :ed25519 143 | require "ed25519" 144 | 145 | pk = Ed25519::VerifyKey.new([key[:public]].pack("H*")) 146 | begin 147 | return pk.verify(data, expected) 148 | rescue Ed25519::VerifyError 149 | return false 150 | end 151 | end 152 | 153 | raise "unknown key type" 154 | end 155 | end # module Key 156 | end # module Crypto 157 | end # module XRBP 158 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/plugins/message_dispatcher.rb: -------------------------------------------------------------------------------- 1 | module XRBP 2 | module WebSocket 3 | module Plugins 4 | # Dispatch messages & wait for responses (w/ optional timeout). 5 | # This module allows the client to track messages sent to the server, 6 | # waiting for responses up to a maximum time. An overridable callback 7 | # method is provided to match responses to messages. Most often the 8 | # end-user will not use this plugin directly but rather through 9 | # CommandDispatcher which inherits it / extends it to issue and 10 | # track structured commands. 11 | # 12 | # @see CommandDispatcher 13 | class MessageDispatcher < PluginBase 14 | include Terminatable 15 | include HasResultParsers 16 | 17 | DEFAULT_TIMEOUT = 10 18 | 19 | def parsing_plugins 20 | connection.plugins 21 | end 22 | 23 | attr_reader :messages 24 | attr_accessor :message_timeout 25 | 26 | def initialize(connection) 27 | super(connection) 28 | @message_timeout = DEFAULT_TIMEOUT 29 | @messages = [] 30 | end 31 | 32 | def added 33 | plugin = self 34 | 35 | connection.define_instance_method(:message_timeout=) do |t| 36 | plugin.message_timeout = t 37 | 38 | connections.each{ |c| 39 | c.plugin(MessageDispatcher) 40 | .message_timeout = t 41 | } if self.kind_of?(MultiConnection) 42 | end 43 | 44 | connection.define_instance_method(:msg) do |msg, &bl| 45 | return next_connection.msg msg, &bl if self.kind_of?(MultiConnection) 46 | 47 | msg = Message.new(msg) unless msg.kind_of?(Message) 48 | msg.connection = self 49 | msg.time = Time.now 50 | msg.bl = bl if bl 51 | 52 | unless self.open? 53 | if plugin.try_next(msg) 54 | return nil if bl 55 | msg.wait 56 | return msg.result 57 | 58 | else 59 | msg.bl.call nil if bl 60 | return nil 61 | end 62 | end 63 | 64 | plugin.messages << msg 65 | 66 | send_data msg.to_s 67 | 68 | return nil if bl 69 | msg.wait 70 | msg.result 71 | end 72 | 73 | connection.on :close do 74 | plugin.cancel_all_messages 75 | end unless connection.kind_of?(MultiConnection) 76 | end 77 | 78 | # Should be overridden in subclass return 79 | # request message & formatted response 80 | # given raw response 81 | def match_message(msg) 82 | nil 83 | end 84 | 85 | # Return bool if message,response is read to be unlocked / returned to client. 86 | # Allows other plugins to block message unlocking 87 | def unlock!(req, res) 88 | !connection.plugins.any? { |plg| 89 | plg != self && plg.respond_to?(:unlock!) && !plg.unlock!(req, res) 90 | } 91 | end 92 | 93 | def message(res) 94 | req, res = match_message(res) 95 | return unless req 96 | messages.delete(req) 97 | 98 | return unless unlock!(req, res) 99 | 100 | begin 101 | res = parse_result(res, req) 102 | rescue Exception => e 103 | if try_next(req) 104 | return 105 | 106 | else 107 | res = nil 108 | end 109 | end 110 | 111 | req.bl.call(res) 112 | end 113 | 114 | def try_next(msg) 115 | conn = connection.next_connection(msg.connection) 116 | return false unless !!conn 117 | messages.delete(msg) 118 | conn.msg(msg, &msg.bl) 119 | true 120 | end 121 | 122 | # FIXME: I *believe* there is issue causing deadlock at process 123 | # termination where subsequent pages in paginated cmds 124 | # are timing out. Since when retrieving messages 125 | # synchronously, the first message block will be used 126 | # to wait for the results and on timeout cancel_message 127 | # will be called with the _latest_ message, the wait 128 | # block never gets unlocked. 129 | def cancel_message(msg) 130 | connection.state_mutex.synchronize { 131 | messages.delete(msg) 132 | msg.signal 133 | } 134 | end 135 | 136 | def cancel_all_messages 137 | # copy array as we modify original during iteration 138 | Array.new(messages).each { |msg| 139 | cancel_message(msg) 140 | } 141 | end 142 | 143 | public 144 | 145 | def opened 146 | connection.add_work do 147 | # XXX remove force_quit? condition check from this loop, 148 | # so we're sure messages always timeout, even on force quit. 149 | # Always ensure close! is called after websocket is no longer 150 | # being used! 151 | until terminate? || connection.closed? 152 | now = Time.now 153 | tmsgs = Array.new(messages) 154 | tmsgs.each { |msg| 155 | if now - msg.time > @message_timeout 156 | connection.emit :timeout, msg 157 | 158 | cancel_message(msg) unless try_next(msg) 159 | 160 | # XXX manually close the connection as 161 | # a broken pipe will not stop websocket polling 162 | connection.async_close! 163 | end 164 | } 165 | 166 | connection.rsleep(0.1) 167 | end 168 | end 169 | end 170 | 171 | def closed 172 | terminate! 173 | end 174 | end # class MessageDispatcher 175 | 176 | WebSocket.register_plugin :message_dispatcher, MessageDispatcher 177 | end # module Plugins 178 | end # module WebSocket 179 | end # module XRBP 180 | -------------------------------------------------------------------------------- /lib/xrbp/websocket/connection.rb: -------------------------------------------------------------------------------- 1 | require_relative '../thread_registry' 2 | 3 | module XRBP 4 | module WebSocket 5 | # Primary websocket interface, use Connection to perform 6 | # websocket requests. 7 | # 8 | # @example retrieve data via a websocket 9 | # connection = WebSocket::Connection.new "wss://s1.ripple.com:443" 10 | # puts connection.send_data('{"command" : "server_info"}') 11 | class Connection 12 | include EventEmitter 13 | include HasPlugin 14 | include ThreadRegistry 15 | 16 | def plugin_namespace 17 | WebSocket 18 | end 19 | 20 | attr_reader :url 21 | attr_accessor :parent 22 | 23 | def initialize(url) 24 | @url = url 25 | @force_quit = false 26 | 27 | yield self if block_given? 28 | end 29 | 30 | ### 31 | 32 | # Initiate new client connection 33 | def connect 34 | client.connect 35 | end 36 | 37 | # Return next connection of parent if applicable 38 | # 39 | # @private 40 | def next_connection(prev) 41 | return nil unless !!parent 42 | parent.next_connection(prev) 43 | end 44 | 45 | # Add work to the internal client thread pool 46 | # 47 | # @private 48 | def add_work(&bl) 49 | client.add_work &bl 50 | end 51 | 52 | # Indicates the connection is initialized 53 | def initialized? 54 | !!@client 55 | end 56 | 57 | # Indicates the connection is open 58 | def open? 59 | initialized? && client.open? 60 | end 61 | 62 | # Indicates the connection is closed 63 | # (may not be completed) 64 | def closed? 65 | !open? 66 | end 67 | 68 | # Indicates if connection is completely 69 | # closed and cleaned up 70 | def completed? 71 | client.completed? 72 | end 73 | 74 | # Close the connection, blocking until completed 75 | def close! 76 | client.close if open? 77 | end 78 | 79 | # Close in a non-blocking way, and immediately return. 80 | def async_close! 81 | client.async_close if open? 82 | end 83 | 84 | # Send raw data via this connection 85 | def send_data(data) 86 | client.send_data(data) 87 | end 88 | 89 | ### 90 | 91 | def force_quit? 92 | @force_quit 93 | end 94 | 95 | # Immediately terminate the connection and all related operations 96 | def force_quit! 97 | @force_quit = true 98 | wake_all 99 | # TODO immediate terminate socket connection 100 | end 101 | 102 | ### 103 | 104 | # Block until connection is open 105 | def wait_for_open 106 | return unless initialized? 107 | 108 | state_mutex.synchronize { 109 | open_cv.wait(state_mutex, 0.1) 110 | } until force_quit? || open? 111 | end 112 | 113 | # Block until connection is closed 114 | def wait_for_close 115 | return unless initialized? 116 | 117 | state_mutex.synchronize { 118 | close_cv.wait(state_mutex, 0.1) 119 | } while !force_quit? && open? 120 | end 121 | 122 | # Block until connection is completed 123 | def wait_for_completed 124 | return unless initialized? 125 | 126 | state_mutex.synchronize { 127 | completed_cv.wait(state_mutex, 0.1) 128 | } while !force_quit? && !completed? 129 | end 130 | 131 | def state_mutex 132 | @state_mutex ||= Mutex.new 133 | end 134 | 135 | def open_cv 136 | @open_cv ||= ConditionVariable.new 137 | end 138 | 139 | def close_cv 140 | @close_cv ||= ConditionVariable.new 141 | end 142 | 143 | def completed_cv 144 | @completed_cv ||= ConditionVariable.new 145 | end 146 | 147 | ### 148 | 149 | # @private 150 | def client 151 | @client ||= begin 152 | client = Client.new(@url) 153 | conn = self 154 | 155 | client.on :connecting do 156 | conn.emit :connecting 157 | conn.parent.emit :connecting, conn if conn.parent 158 | end 159 | 160 | client.on :open do 161 | conn.emit :open 162 | conn.parent.emit :open, conn if conn.parent 163 | 164 | conn.state_mutex.synchronize { 165 | conn.open_cv.signal 166 | } 167 | 168 | conn.plugins.each { |plg| 169 | plg.opened if plg.respond_to?(:opened) 170 | } 171 | end 172 | 173 | client.on :close do 174 | conn.emit :close 175 | conn.parent.emit :close, conn if conn.parent 176 | 177 | conn.state_mutex.synchronize { 178 | conn.close_cv.signal 179 | } 180 | 181 | conn.plugins.each { |plg| 182 | plg.closed if plg.respond_to?(:closed) 183 | } 184 | end 185 | 186 | client.on :completed do |err| 187 | conn.emit :completed 188 | conn.parent.emit :completed, conn if conn.parent 189 | 190 | conn.state_mutex.synchronize { 191 | conn.completed_cv.signal 192 | } 193 | 194 | conn.plugins.each { |plg| 195 | plg.completed if plg.respond_to?(:completed) 196 | } 197 | end 198 | 199 | client.on :error do |err| 200 | conn.emit :error, err 201 | conn.parent.emit :error, conn, err if conn.parent 202 | 203 | conn.plugins.each { |plg| 204 | plg.error err if plg.respond_to?(:error) 205 | } 206 | end 207 | 208 | client.on :message do |msg| 209 | conn.emit :message, msg 210 | conn.parent.emit :message, conn, msg if conn.parent 211 | 212 | conn.plugins.each { |plg| 213 | plg.message msg if plg.respond_to?(:message) 214 | } 215 | end 216 | 217 | client 218 | end 219 | end 220 | end # class Connection 221 | end # module WebSocket 222 | end # module XRBP 223 | --------------------------------------------------------------------------------