FC Updates from BitShares and myself #21

Closed
nathanielhourt wants to merge 687 commits from dapp-support into latest-fc
12 changed files with 184 additions and 107 deletions
Showing only changes of commit 8d0693304e - Show all commits

View file

@ -35,7 +35,8 @@ namespace fc
aes_error_code = 18, aes_error_code = 18,
overflow_code = 19, overflow_code = 19,
underflow_code = 20, underflow_code = 20,
divide_by_zero_code = 21 divide_by_zero_code = 21,
method_not_found_exception_code = 22
}; };
/** /**
@ -273,6 +274,7 @@ namespace fc
FC_DECLARE_EXCEPTION( key_not_found_exception, key_not_found_exception_code, "Key Not Found" ); FC_DECLARE_EXCEPTION( key_not_found_exception, key_not_found_exception_code, "Key Not Found" );
FC_DECLARE_EXCEPTION( bad_cast_exception, bad_cast_exception_code, "Bad Cast" ); FC_DECLARE_EXCEPTION( bad_cast_exception, bad_cast_exception_code, "Bad Cast" );
FC_DECLARE_EXCEPTION( out_of_range_exception, out_of_range_exception_code, "Out of Range" ); FC_DECLARE_EXCEPTION( out_of_range_exception, out_of_range_exception_code, "Out of Range" );
FC_DECLARE_EXCEPTION( method_not_found_exception, method_not_found_exception_code, "Method Not Found" );
/** @brief if an operation is unsupported or not valid this may be thrown */ /** @brief if an operation is unsupported or not valid this may be thrown */
FC_DECLARE_EXCEPTION( invalid_operation_exception, FC_DECLARE_EXCEPTION( invalid_operation_exception,

View file

@ -25,6 +25,7 @@ namespace fc {
enum status_code { enum status_code {
OK = 200, OK = 200,
RecordCreated = 201, RecordCreated = 201,
NoContent = 204,
BadRequest = 400, BadRequest = 400,
NotAuthorized = 401, NotAuthorized = 401,
NotFound = 404, NotFound = 404,
@ -35,6 +36,7 @@ namespace fc {
int status; int status;
std::vector<header> headers; std::vector<header> headers;
std::vector<char> body; std::vector<char> body;
std::string body_as_string;
}; };
struct request struct request
@ -79,4 +81,4 @@ namespace fc {
#include <fc/reflect/reflect.hpp> #include <fc/reflect/reflect.hpp>
FC_REFLECT( fc::http::header, (key)(val) ) FC_REFLECT( fc::http::header, (key)(val) )
FC_REFLECT( fc::http::reply, (status)(headers)(body)(body_as_string) )

View file

@ -4,6 +4,7 @@
#include <string> #include <string>
#include <boost/any.hpp> #include <boost/any.hpp>
#include <fc/network/ip.hpp> #include <fc/network/ip.hpp>
#include <fc/network/http/connection.hpp>
#include <fc/signals.hpp> #include <fc/signals.hpp>
namespace fc { namespace http { namespace fc { namespace http {
@ -12,7 +13,7 @@ namespace fc { namespace http {
class websocket_tls_server_impl; class websocket_tls_server_impl;
class websocket_client_impl; class websocket_client_impl;
class websocket_tls_client_impl; class websocket_tls_client_impl;
} // namespace detail; } // namespace detail
class websocket_connection class websocket_connection
{ {
@ -21,10 +22,10 @@ namespace fc { namespace http {
virtual void send_message( const std::string& message ) = 0; virtual void send_message( const std::string& message ) = 0;
virtual void close( int64_t code, const std::string& reason ){}; virtual void close( int64_t code, const std::string& reason ){};
void on_message( const std::string& message ) { _on_message(message); } void on_message( const std::string& message ) { _on_message(message); }
string on_http( const std::string& message ) { return _on_http(message); } fc::http::reply on_http( const std::string& message ) { return _on_http(message); }
void on_message_handler( const std::function<void(const std::string&)>& h ) { _on_message = h; } void on_message_handler( const std::function<void(const std::string&)>& h ) { _on_message = h; }
void on_http_handler( const std::function<std::string(const std::string&)>& h ) { _on_http = h; } void on_http_handler( const std::function<fc::http::reply(const std::string&)>& h ) { _on_http = h; }
void set_session_data( boost::any d ){ _session_data = std::move(d); } void set_session_data( boost::any d ){ _session_data = std::move(d); }
boost::any& get_session_data() { return _session_data; } boost::any& get_session_data() { return _session_data; }
@ -35,7 +36,7 @@ namespace fc { namespace http {
private: private:
boost::any _session_data; boost::any _session_data;
std::function<void(const std::string&)> _on_message; std::function<void(const std::string&)> _on_message;
std::function<string(const std::string&)> _on_http; std::function<fc::http::reply(const std::string&)> _on_http;
}; };
typedef std::shared_ptr<websocket_connection> websocket_connection_ptr; typedef std::shared_ptr<websocket_connection> websocket_connection_ptr;

View file

@ -112,13 +112,17 @@ namespace fc {
variant call( const string& name, const variants& args ) variant call( const string& name, const variants& args )
{ {
auto itr = _by_name.find(name); auto itr = _by_name.find(name);
FC_ASSERT( itr != _by_name.end(), "no method with name '${name}'", ("name",name)("api",_by_name) ); if( itr == _by_name.end() )
FC_THROW_EXCEPTION( method_not_found_exception, "No method with name '${name}'",
("name",name)("api",_by_name) );
return call( itr->second, args ); return call( itr->second, args );
} }
variant call( uint32_t method_id, const variants& args ) variant call( uint32_t method_id, const variants& args )
{ {
FC_ASSERT( method_id < _methods.size() ); if( method_id >= _methods.size() )
FC_THROW_EXCEPTION( method_not_found_exception, "No method with id '${id}'",
("id",method_id)("api",_by_name) );
return _methods[method_id](args); return _methods[method_id](args);
} }

View file

@ -6,9 +6,10 @@
namespace fc { namespace rpc { namespace fc { namespace rpc {
struct request struct request
{ {
optional<uint64_t> id; optional<variant> id;
std::string method; std::string method;
variants params; variants params;
optional<std::string> jsonrpc;
}; };
struct error_object struct error_object
@ -20,13 +21,15 @@ namespace fc { namespace rpc {
struct response struct response
{ {
response(){} response() {}
response( int64_t i, fc::variant r ):id(i),result(r){} response( const optional<variant>& _id, const variant& _result,
response( int64_t i, error_object r ):id(i),error(r){} const optional<string>& version = optional<string>() )
response( int64_t i, fc::variant r, string j ):id(i),jsonrpc(j),result(r){} : id(_id), jsonrpc(version), result(_result) {}
response( int64_t i, error_object r, string j ):id(i),jsonrpc(j),error(r){} response( const optional<variant>& _id, const error_object& error,
int64_t id = 0; const optional<string>& version = optional<string>() )
optional<std::string> jsonrpc; : id(_id), jsonrpc(version), error(error) {}
optional<variant> id;
optional<std::string> jsonrpc;
optional<fc::variant> result; optional<fc::variant> result;
optional<error_object> error; optional<error_object> error;
}; };
@ -44,7 +47,7 @@ namespace fc { namespace rpc {
void handle_reply( const response& response ); void handle_reply( const response& response );
request start_remote_call( const string& method_name, variants args ); request start_remote_call( const string& method_name, variants args );
variant wait_for_response( uint64_t request_id ); variant wait_for_response( const variant& request_id );
void close(); void close();
@ -52,12 +55,12 @@ namespace fc { namespace rpc {
private: private:
uint64_t _next_id = 1; uint64_t _next_id = 1;
std::unordered_map<uint64_t, fc::promise<variant>::ptr> _awaiting; std::map<variant, fc::promise<variant>::ptr> _awaiting;
std::unordered_map<std::string, method> _methods; std::unordered_map<std::string, method> _methods;
std::function<variant(const string&,const variants&)> _unhandled; std::function<variant(const string&,const variants&)> _unhandled;
}; };
} } // namespace fc::rpc } } // namespace fc::rpc
FC_REFLECT( fc::rpc::request, (id)(method)(params) ); FC_REFLECT( fc::rpc::request, (id)(method)(params)(jsonrpc) );
FC_REFLECT( fc::rpc::error_object, (code)(message)(data) ) FC_REFLECT( fc::rpc::error_object, (code)(message)(data) )
FC_REFLECT( fc::rpc::response, (id)(jsonrpc)(result)(error) ) FC_REFLECT( fc::rpc::response, (id)(jsonrpc)(result)(error) )

View file

@ -1,9 +1,7 @@
#pragma once #pragma once
#include <fc/network/http/websocket.hpp>
#include <fc/rpc/api_connection.hpp> #include <fc/rpc/api_connection.hpp>
#include <fc/rpc/state.hpp> #include <fc/rpc/state.hpp>
#include <fc/network/http/websocket.hpp>
#include <fc/io/json.hpp>
#include <fc/reflect/variant.hpp>
namespace fc { namespace rpc { namespace fc { namespace rpc {
@ -26,9 +24,9 @@ namespace fc { namespace rpc {
variants args = variants() ) override; variants args = variants() ) override;
protected: protected:
std::string on_message( response on_message( const std::string& message );
const std::string& message, response on_request( const variant& message );
bool send_message = true ); void on_response( const variant& message );
std::shared_ptr<fc::http::websocket_connection> _connection; std::shared_ptr<fc::http::websocket_connection> _connection;
fc::rpc::state _rpc_state; fc::rpc::state _rpc_state;

View file

@ -658,11 +658,11 @@ namespace fc
variant operator - ( const variant& a, const variant& b ); variant operator - ( const variant& a, const variant& b );
variant operator * ( const variant& a, const variant& b ); variant operator * ( const variant& a, const variant& b );
variant operator / ( const variant& a, const variant& b ); variant operator / ( const variant& a, const variant& b );
variant operator == ( const variant& a, const variant& b ); bool operator == ( const variant& a, const variant& b );
variant operator != ( const variant& a, const variant& b ); bool operator != ( const variant& a, const variant& b );
variant operator < ( const variant& a, const variant& b ); bool operator < ( const variant& a, const variant& b );
variant operator > ( const variant& a, const variant& b ); bool operator > ( const variant& a, const variant& b );
variant operator ! ( const variant& a ); bool operator ! ( const variant& a );
} // namespace fc } // namespace fc
#include <fc/reflect/reflect.hpp> #include <fc/reflect/reflect.hpp>

View file

@ -12,7 +12,10 @@
#include <websocketpp/extensions/permessage_deflate/disabled.hpp> #include <websocketpp/extensions/permessage_deflate/disabled.hpp>
#endif #endif
#include <fc/io/json.hpp>
#include <fc/optional.hpp> #include <fc/optional.hpp>
#include <fc/reflect/variant.hpp>
#include <fc/rpc/websocket_api.hpp>
#include <fc/variant.hpp> #include <fc/variant.hpp>
#include <fc/thread/thread.hpp> #include <fc/thread/thread.hpp>
#include <fc/asio.hpp> #include <fc/asio.hpp>
@ -239,10 +242,10 @@ namespace fc { namespace http {
wdump(("server")(request_body)); wdump(("server")(request_body));
fc::async([current_con, request_body, con] { fc::async([current_con, request_body, con] {
std::string response = current_con->on_http(request_body); fc::http::reply response = current_con->on_http(request_body);
idump((response)); idump( (response) );
con->set_body( response ); con->set_body( std::move( response.body_as_string ) );
con->set_status( websocketpp::http::status_code::ok ); con->set_status( websocketpp::http::status_code::value(response.status) );
con->send_http_response(); con->send_http_response();
current_con->closed(); current_con->closed();
}, "call on_http"); }, "call on_http");
@ -364,8 +367,8 @@ namespace fc { namespace http {
wdump(("server")(con->get_request_body())); wdump(("server")(con->get_request_body()));
auto response = current_con->on_http( con->get_request_body() ); auto response = current_con->on_http( con->get_request_body() );
idump((response)); idump((response));
con->set_body( response ); con->set_body( std::move( response.body_as_string ) );
con->set_status( websocketpp::http::status_code::ok ); con->set_status( websocketpp::http::status_code::value( response.status ) );
} catch ( const fc::exception& e ) } catch ( const fc::exception& e )
{ {
edump((e.to_detail_string())); edump((e.to_detail_string()));

View file

@ -29,7 +29,8 @@ variant state::local_call( const string& method_name, const variants& args )
void state::handle_reply( const response& response ) void state::handle_reply( const response& response )
{ {
auto await = _awaiting.find( response.id ); FC_ASSERT( response.id, "Response without ID: ${response}", ("response",response) );
auto await = _awaiting.find( *response.id );
FC_ASSERT( await != _awaiting.end(), "Unknown Response ID: ${id}", ("id",response.id)("response",response) ); FC_ASSERT( await != _awaiting.end(), "Unknown Response ID: ${id}", ("id",response.id)("response",response) );
if( response.result ) if( response.result )
await->second->set_value( *response.result ); await->second->set_value( *response.result );
@ -48,7 +49,7 @@ request state::start_remote_call( const string& method_name, variants args )
_awaiting[*request.id] = fc::promise<variant>::ptr( new fc::promise<variant>("json_connection::async_call") ); _awaiting[*request.id] = fc::promise<variant>::ptr( new fc::promise<variant>("json_connection::async_call") );
return request; return request;
} }
variant state::wait_for_response( uint64_t request_id ) variant state::wait_for_response( const variant& request_id )
{ {
auto itr = _awaiting.find(request_id); auto itr = _awaiting.find(request_id);
FC_ASSERT( itr != _awaiting.end() ); FC_ASSERT( itr != _awaiting.end() );

View file

@ -1,5 +1,6 @@
#include <fc/reflect/variant.hpp>
#include <fc/rpc/websocket_api.hpp> #include <fc/rpc/websocket_api.hpp>
#include <fc/io/json.hpp>
namespace fc { namespace rpc { namespace fc { namespace rpc {
@ -49,8 +50,30 @@ websocket_api_connection::websocket_api_connection( const std::shared_ptr<fc::ht
return this->receive_call( 0, method_name, args ); return this->receive_call( 0, method_name, args );
} ); } );
_connection->on_message_handler( [this]( const std::string& msg ){ on_message(msg,true); } ); _connection->on_message_handler( [this]( const std::string& msg ){
_connection->on_http_handler( [this]( const std::string& msg ){ return on_message(msg,false); } ); response reply = on_message(msg);
if( _connection && ( reply.id || reply.result || reply.error || reply.jsonrpc ) )
_connection->send_message( fc::json::to_string( reply, fc::json::stringify_large_ints_and_doubles,
_max_conversion_depth ) );
} );
_connection->on_http_handler( [this]( const std::string& msg ){
response reply = on_message(msg);
fc::http::reply result;
if( reply.error )
{
if( reply.error->code == -32603 )
result.status = fc::http::reply::InternalServerError;
else if( reply.error->code <= -32600 )
result.status = fc::http::reply::BadRequest;
}
if( reply.id || reply.result || reply.error || reply.jsonrpc )
result.body_as_string = fc::json::to_string( reply, fc::json::stringify_large_ints_and_doubles,
_max_conversion_depth );
else
result.status = fc::http::reply::NoContent;
return result;
} );
_connection->closed.connect( [this](){ closed(); } ); _connection->closed.connect( [this](){ closed(); } );
} }
@ -96,81 +119,120 @@ void websocket_api_connection::send_notice(
_max_conversion_depth ) ); _max_conversion_depth ) );
} }
std::string websocket_api_connection::on_message( response websocket_api_connection::on_message( const std::string& message )
const std::string& message,
bool send_message /* = true */ )
{ {
variant var;
try try
{ {
auto var = fc::json::from_string(message, fc::json::legacy_parser, _max_conversion_depth); var = fc::json::from_string( message, fc::json::legacy_parser, _max_conversion_depth );
const auto& var_obj = var.get_object(); }
catch( const fc::exception& e )
{
return response( variant(), { -32700, "Invalid JSON message", variant( e, _max_conversion_depth ) }, "2.0" );
}
if( var_obj.contains( "method" ) ) if( var.is_array() )
{ return response( variant(), { -32600, "Batch requests not supported" }, "2.0" );
auto call = var.as<fc::rpc::request>(_max_conversion_depth);
exception_ptr optexcept; if( !var.is_object() )
try return response( variant(), { -32600, "Invalid JSON request" }, "2.0" );
{
try variant_object var_obj = var.get_object();
{
if( var_obj.contains( "id" )
&& !var_obj["id"].is_string() && !var_obj["id"].is_numeric() && !var_obj["id"].is_null() )
return response( variant(), { -32600, "Invalid id" }, "2.0" );
if( var_obj.contains( "method" ) && ( !var_obj["method"].is_string() || var_obj["method"].get_string() == "" ) )
return response( variant(), { -32600, "Missing or invalid method" }, "2.0" );
if( var_obj.contains( "jsonrpc" ) && ( !var_obj["jsonrpc"].is_string() || var_obj["jsonrpc"] != "2.0" ) )
return response( variant(), { -32600, "Unsupported JSON-RPC version" }, "2.0" );
if( var_obj.contains( "method" ) )
{
if( var_obj.contains( "params" ) && var_obj["params"].is_object() )
return response( variant(), { -32602, "Named parameters not supported" }, "2.0" );
if( var_obj.contains( "params" ) && !var_obj["params"].is_array() )
return response( variant(), { -32600, "Invalid parameters" }, "2.0" );
return on_request( std::move( var ) );
}
if( var_obj.contains( "result" ) || var_obj.contains("error") )
{
if( !var_obj.contains( "id" ) || ( var_obj["id"].is_null() && !var_obj.contains( "jsonrpc" ) ) )
return response( variant(), { -32600, "Missing or invalid id" }, "2.0" );
on_response( std::move( var ) );
return response();
}
return response( variant(), { -32600, "Missing method or result or error" }, "2.0" );
}
void websocket_api_connection::on_response( const variant& var )
{
_rpc_state.handle_reply( var.as<fc::rpc::response>(_max_conversion_depth) );
}
response websocket_api_connection::on_request( const variant& var )
{
request call = var.as<fc::rpc::request>( _max_conversion_depth );
if( var.get_object().contains( "id" ) )
call.id = var.get_object()["id"]; // special handling for null id
// null ID is valid in JSONRPC-2.0 but signals "no id" in JSONRPC-1.0
bool has_id = call.id.valid() && ( call.jsonrpc.valid() || !call.id->is_null() );
try
{
#ifdef LOG_LONG_API #ifdef LOG_LONG_API
auto start = time_point::now(); auto start = time_point::now();
#endif #endif
auto result = _rpc_state.local_call( call.method, call.params ); auto result = _rpc_state.local_call( call.method, call.params );
#ifdef LOG_LONG_API #ifdef LOG_LONG_API
auto end = time_point::now(); auto end = time_point::now();
if( end - start > fc::milliseconds( LOG_LONG_API_MAX_MS ) ) if( end - start > fc::milliseconds( LOG_LONG_API_MAX_MS ) )
elog( "API call execution time limit exceeded. method: ${m} params: ${p} time: ${t}", elog( "API call execution time limit exceeded. method: ${m} params: ${p} time: ${t}",
("m",call.method)("p",call.params)("t", end - start) ); ("m",call.method)("p",call.params)("t", end - start) );
else if( end - start > fc::milliseconds( LOG_LONG_API_WARN_MS ) ) else if( end - start > fc::milliseconds( LOG_LONG_API_WARN_MS ) )
wlog( "API call execution time nearing limit. method: ${m} params: ${p} time: ${t}", wlog( "API call execution time nearing limit. method: ${m} params: ${p} time: ${t}",
("m",call.method)("p",call.params)("t", end - start) ); ("m",call.method)("p",call.params)("t", end - start) );
#endif #endif
if( call.id ) if( has_id )
{ return response( call.id, result, call.jsonrpc );
auto reply = fc::json::to_string( response( *call.id, result, "2.0" ), }
fc::json::stringify_large_ints_and_doubles, catch ( const fc::method_not_found_exception& e )
_max_conversion_depth ); {
if( send_message && _connection ) if( has_id )
_connection->send_message( reply ); return response( call.id, error_object{ -32601, "Method not found",
return reply; variant( (fc::exception) e, _max_conversion_depth ) }, call.jsonrpc );
}
}
FC_CAPTURE_AND_RETHROW( (call.method)(call.params) )
}
catch ( const fc::exception& e )
{
if( call.id )
{
optexcept = e.dynamic_copy_exception();
}
}
if( optexcept ) {
auto reply = fc::json::to_string( variant(response( *call.id, error_object{ 1, optexcept->to_string(), fc::variant(*optexcept, _max_conversion_depth)}, "2.0" ), _max_conversion_depth ),
fc::json::stringify_large_ints_and_doubles, _max_conversion_depth );
if( send_message && _connection )
_connection->send_message( reply );
return reply;
}
}
else
{
auto reply = var.as<fc::rpc::response>(_max_conversion_depth);
_rpc_state.handle_reply( reply );
}
} }
catch ( const fc::exception& e ) catch ( const fc::exception& e )
{ {
wdump((e.to_detail_string())); if( has_id )
return e.to_detail_string(); return response( call.id, error_object{ e.code(), "Execution error", variant( e, _max_conversion_depth ) },
call.jsonrpc );
} }
return string(); catch ( const std::exception& e )
{
elog( "Internal error - ${e}", ("e",e.what()) );
return response( call.id, error_object{ -32603, "Internal error", variant( e.what(), _max_conversion_depth ) },
call.jsonrpc );
}
catch ( ... )
{
elog( "Internal error while processing RPC request" );
throw;
}
return response();
} }
} } // namespace fc::rpc } } // namespace fc::rpc

View file

@ -678,7 +678,7 @@ void from_variant( const variant& var, std::vector<char>& vo, uint32_t max_depth
void to_variant( unsigned long long int s, variant& v, uint32_t max_depth ) { v = variant( uint64_t(s)); } void to_variant( unsigned long long int s, variant& v, uint32_t max_depth ) { v = variant( uint64_t(s)); }
#endif #endif
variant operator == ( const variant& a, const variant& b ) bool operator == ( const variant& a, const variant& b )
{ {
if( a.is_string() || b.is_string() ) return a.as_string() == b.as_string(); if( a.is_string() || b.is_string() ) return a.as_string() == b.as_string();
if( a.is_double() || b.is_double() ) return a.as_double() == b.as_double(); if( a.is_double() || b.is_double() ) return a.as_double() == b.as_double();
@ -687,7 +687,7 @@ void from_variant( const variant& var, std::vector<char>& vo, uint32_t max_depth
return false; return false;
} }
variant operator != ( const variant& a, const variant& b ) bool operator != ( const variant& a, const variant& b )
{ {
if( a.is_string() || b.is_string() ) return a.as_string() != b.as_string(); if( a.is_string() || b.is_string() ) return a.as_string() != b.as_string();
if( a.is_double() || b.is_double() ) return a.as_double() != b.as_double(); if( a.is_double() || b.is_double() ) return a.as_double() != b.as_double();
@ -696,12 +696,12 @@ void from_variant( const variant& var, std::vector<char>& vo, uint32_t max_depth
return false; return false;
} }
variant operator ! ( const variant& a ) bool operator ! ( const variant& a )
{ {
return !a.as_bool(); return !a.as_bool();
} }
variant operator < ( const variant& a, const variant& b ) bool operator < ( const variant& a, const variant& b )
{ {
if( a.is_string() || b.is_string() ) return a.as_string() < b.as_string(); if( a.is_string() || b.is_string() ) return a.as_string() < b.as_string();
if( a.is_double() || b.is_double() ) return a.as_double() < b.as_double(); if( a.is_double() || b.is_double() ) return a.as_double() < b.as_double();
@ -710,7 +710,7 @@ void from_variant( const variant& var, std::vector<char>& vo, uint32_t max_depth
FC_ASSERT( false, "Invalid operation" ); FC_ASSERT( false, "Invalid operation" );
} }
variant operator > ( const variant& a, const variant& b ) bool operator > ( const variant& a, const variant& b )
{ {
if( a.is_string() || b.is_string() ) return a.as_string() > b.as_string(); if( a.is_string() || b.is_string() ) return a.as_string() > b.as_string();
if( a.is_double() || b.is_double() ) return a.as_double() > b.as_double(); if( a.is_double() || b.is_double() ) return a.as_double() > b.as_double();
@ -719,7 +719,7 @@ void from_variant( const variant& var, std::vector<char>& vo, uint32_t max_depth
FC_ASSERT( false, "Invalid operation" ); FC_ASSERT( false, "Invalid operation" );
} }
variant operator <= ( const variant& a, const variant& b ) bool operator <= ( const variant& a, const variant& b )
{ {
if( a.is_string() || b.is_string() ) return a.as_string() <= b.as_string(); if( a.is_string() || b.is_string() ) return a.as_string() <= b.as_string();
if( a.is_double() || b.is_double() ) return a.as_double() <= b.as_double(); if( a.is_double() || b.is_double() ) return a.as_double() <= b.as_double();

View file

@ -1,6 +1,7 @@
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
#include <fc/api.hpp> #include <fc/api.hpp>
#include <fc/io/json.hpp>
#include <fc/log/logger.hpp> #include <fc/log/logger.hpp>
#include <fc/rpc/api_connection.hpp> #include <fc/rpc/api_connection.hpp>
#include <fc/rpc/websocket_api.hpp> #include <fc/rpc/websocket_api.hpp>