diff --git a/.gitmodules b/.gitmodules index 39e7fbbc..5e1c99b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,8 +1,8 @@ [submodule "docs"] - path = docs - url = https://github.com/cryptonomex/graphene.wiki.git + path = docs + url = https://github.com/bitshares/bitshares-core.wiki.git ignore = dirty [submodule "libraries/fc"] - path = libraries/fc - url = https://github.com/PBSA/peerplays-0.1-fc.git + path = libraries/fc + url = https://github.com/PBSA/peerplays-0.1-fc.git ignore = dirty diff --git a/CMakeLists.txt b/CMakeLists.txt index 443223be..a71bc063 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,13 +32,16 @@ if (USE_PCH) include (cotire) endif(USE_PCH) -list( APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libraries/fc/CMakeModules" ) +IF( NOT WIN32 ) + list( APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libraries/fc/CMakeModules" ) +ENDIF( NOT WIN32 ) list( APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/libraries/fc/GitVersionGen" ) include( GetGitRevisionDescription ) get_git_head_revision( GIT_REFSPEC GIT_SHA2 ) SET(BOOST_COMPONENTS) LIST(APPEND BOOST_COMPONENTS thread + iostreams date_time system filesystem @@ -129,6 +132,10 @@ else( WIN32 ) # Apple AND Linux if( "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-builtin-memcmp" ) + elseif( "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" ) + if( CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 4.0.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.0.0 ) + set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-partial-specialization" ) + endif() endif() if( "${CMAKE_GENERATOR}" STREQUAL "Ninja" ) @@ -143,8 +150,6 @@ else( WIN32 ) # Apple AND Linux endif( WIN32 ) -find_package( BerkeleyDB ) - set(ENABLE_COVERAGE_TESTING FALSE CACHE BOOL "Build BitShares for code coverage analysis") if(ENABLE_COVERAGE_TESTING) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt new file mode 100644 index 00000000..e69de29b diff --git a/Dockerfile b/Dockerfile index 1aa367e5..bac3e2f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,64 @@ -# This will build the witness_node in a docker image. Make sure you've already -# checked out the submodules before building. +FROM phusion/baseimage:0.9.19 +MAINTAINER PeerPlays Blockchain Standards Association -FROM l3iggs/archlinux:latest -MAINTAINER Nathan Hourt +ENV LANG=en_US.UTF-8 +RUN \ + apt-get update -y && \ + apt-get install -y \ + g++ \ + autoconf \ + cmake \ + git \ + libbz2-dev \ + libreadline-dev \ + libboost-all-dev \ + libcurl4-openssl-dev \ + libssl-dev \ + libncurses-dev \ + doxygen \ + libcurl4-openssl-dev \ + && \ + apt-get update -y && \ + apt-get install -y fish && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -RUN pacman -Syu --noconfirm gcc make autoconf automake cmake ninja boost libtool git +ADD . /peerplays-core +WORKDIR /peerplays-core -ADD . /bitshares-2 -WORKDIR /bitshares-2 -RUN cmake -G Ninja -DCMAKE_BUILD_TYPE=Release . -RUN ninja witness_node || ninja -j 1 witness_node +# Compile +RUN \ + git submodule update --init --recursive && \ + cmake \ + -DCMAKE_BUILD_TYPE=Release \ + . && \ + make witness_node && \ + make install && \ + # + # Obtain version + mkdir /etc/peerplays && \ + git rev-parse --short HEAD > /etc/peerplays/version && \ + cd / && \ + rm -rf /peerplays-core -RUN mkdir /data_dir -ADD docker/default_config.ini /default_config.ini -ADD docker/launch /launch -RUN chmod a+x /launch -VOLUME /data_dir +# Home directory $HOME +WORKDIR / +RUN useradd -s /bin/bash -m -d /var/lib/peerplays peerplays +ENV HOME /var/lib/peerplays +RUN chown peerplays:peerplays -R /var/lib/peerplays -EXPOSE 8090 9090 +# Volume +VOLUME ["/var/lib/peerplays", "/etc/peerplays"] -ENTRYPOINT ["/launch"] +# rpc service: +EXPOSE 8090 +# p2p service: +EXPOSE 2001 + +# default exec/config files +ADD docker/default_config.ini /etc/peerplays/config.ini +ADD docker/peerplaysentry.sh /usr/local/bin/peerplaysentry.sh +RUN chmod a+x /usr/local/bin/peerplaysentry.sh + +# default execute entry +CMD /usr/local/bin/peerplaysentry.sh diff --git a/LICENSE.md b/LICENSE.txt similarity index 89% rename from LICENSE.md rename to LICENSE.txt index 0415b22c..b65abcc7 100644 --- a/LICENSE.md +++ b/LICENSE.txt @@ -1,4 +1,5 @@ -Copyright (c) 2015 Cryptonomex, Inc., and contributors. +Copyright (c) 2015-2016 Cryptonomex Inc. +Copyright (c) 2015-2017 contributors The MIT License @@ -19,4 +20,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index 52b730be..bc6cd411 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,3 @@ Running specific tests ---------------------- - `tests/chain_tests -t block_tests/name_of_test` - - - diff --git a/betting2-nhl b/betting2-nhl new file mode 100644 index 00000000..b17ccef7 --- /dev/null +++ b/betting2-nhl @@ -0,0 +1,161 @@ + + +### PRE... + +import_key init0 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init1 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init2 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init3 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init4 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init5 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init6 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init7 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init8 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init9 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 +import_key init10 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 + +#upgrade_account nathan 1 +import_balance nathan [5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3] 1 + + + +transfer nathan init0 125 PPY "" true +transfer nathan init1 125 PPY "" true +#transfer nathan init2 125 PPY "" true +#transfer nathan init3 125 PPY "" true +#transfer nathan init4 125 PPY "" true +#transfer nathan init5 125 PPY "" true +#transfer nathan init6 125 PPY "" true +#transfer nathan init7 125 PPY "" true +#transfer nathan init8 125 PPY "" true +#transfer nathan init9 125 PPY "" true +#transfer nathan init10 125 PPY "" true + +transfer nathan 1.2.0 1250 PPY "" true +transfer nathan 1.2.1 1250 PPY "" true + +transfer nathan 1.2.5 1250 PPY "" true + +vote_for_witness nathan init0 1 1 +vote_for_witness nathan init1 1 1 + +whitelist_account init0 1.2.1 1 1 +whitelist_account init1 1.2.1 1 1 +#whitelist_account init2 1.2.1 1 1 +#whitelist_account init2 1.2.1 1 1 +#whitelist_account init3 1.2.1 1 1 +#whitelist_account init4 1.2.1 1 1 +#whitelist_account init5 1.2.1 1 1 +#whitelist_account init6 1.2.1 1 1 +#whitelist_account init7 1.2.1 1 1 +#whitelist_account init8 1.2.1 1 1 +#whitelist_account init9 1.2.1 1 1 +#whitelist_account init10 1.2.1 1 1 + + +# ALICE & BOB + +register_account alice PPY4zSJHx7D84T1j6HQ7keXWdtabBBWJxvfJw72XmEyqmgdoo1njF PPY4zSJHx7D84T1j6HQ7keXWdtabBBWJxvfJw72XmEyqmgdoo1njF nathan nathan 0 true +register_account bob PPY4zSJHx7D84T1j6HQ7keXWdtabBBWJxvfJw72XmEyqmgdoo1njF PPY4zSJHx7D84T1j6HQ7keXWdtabBBWJxvfJw72XmEyqmgdoo1njF nathan nathan 0 true + +transfer nathan alice 1250 PPY "" true +transfer nathan bob 1250 PPY "" true + +import_key alice 5HuCDiMeESd86xrRvTbexLjkVg2BEoKrb7BAA5RLgXizkgV3shs +import_key bob 5HuCDiMeESd86xrRvTbexLjkVg2BEoKrb7BAA5RLgXizkgV3shs + + +### SPORT + +propose_create_sport nathan "2017-05-16-T07:46:03" [["en","Ice Hockey"],["zh_Hans","冰球"],["ja","アイスホッケー"]] 1 +propose_create_sport nathan "2017-05-16-T07:47:01" [["en","spce balls"], ["pl","gra w kulki"]] 1 + +// proposal +#get_object 1.10.0 +#get_object 1.10.1 + +#approve_proposal nathan 1.10.0 { "active_approvals_to_add" : [ "init0", "init1","init2", "init3", "init4", "init5", "init6", "init7", "init8", "init9", "init10"] } 1 +approve_proposal nathan 1.10.0 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +approve_proposal nathan 1.10.12 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +#get_object 1.16.0 +#get_object 1.16.1 + + +### EVENT GROUOP + +propose_create_event_group nathan "2017-05-16-T08:45:01" [ ["en", "NHL"], ["zh_Hans", "國家冰球聯盟"], ["ja", "ナショナルホッケーリーグ"] ] "1.16.0" 1 +approve_proposal nathan 1.10.2 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.17.0 + + +### EVENT + +propose_create_event nathan "2017-08-07-T11:25:01" [["en", "Washington Capitals/Chicago Blackhawks"], ["zh_Hans", "華盛頓首都隊/芝加哥黑鷹"], ["ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"]] [["en","2016-17"]] "2018-09-16-T07:47:01" "1.17.0" 1 + +propose_create_event nathan "2017-08-07-T11:25:01" [["en", "Washington Capitals/Chicago Blackhawks"], ["zh_Hans", "華盛頓首都隊/芝加哥黑鷹"], ["ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"]] [["en","2017-18"]] "2018-09-16-T07:47:01" "1.17.0" 1 + +//propose_update_event nathan "2017-08-07-T11:25:01" "1.18.0" "1.17.0" [["en", "Washington Capitals/Chicago Blackhawks"], ["zh_Hans", "華盛頓首都隊/芝加哥黑鷹"], ["ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"]] [["en","2016-17"]] "2018-09-16-T07:47:01" 1 + +approve_proposal nathan 1.10.3 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.18.0 + + +# RULES + +propose_create_betting_market_rules nathan "2017-08-31-T10:35:01" [["en","NHL Rules v1.0"]] [["en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."]] 1 + +approve_proposal nathan 1.10.8 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.19.0 + + +### BETTING_MARKET GROUP + +propose_create_betting_market_group nathan "2017-08-31-T10:39:01" [["en", "Moneyline"]] "1.18.0" "1.19.0" "1.3.0" 1 +propose_create_betting_market_group nathan "2017-08-25-T11:10:01" [["en", "Moneyline NHL"]] "1.18.1" "1.19.1" "1.3.0" 1 + +approve_proposal nathan 1.10.9 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.20.0 + + +### BETTING_MARKET + +propose_create_betting_market nathan "2017-08-31-T10:42:01" "1.20.0" [["en","Washington Capitals win"]] [] 1 +propose_create_betting_market nathan "2017-08-31-T10:42:01" "1.20.0" [["en","Chicago Blackhawks win"]] [] 1 + +approve_proposal nathan 1.10.10 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +approve_proposal nathan 1.10.11 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.21.0 +//get_object 1.21.1 + +//list_betting_markets 1.20.0 + + +# PLACE_BET + +//place_bet bob "1.21.0" 1 { "amount" : 100 } 20000 20000 true +//place_bet alice "1.21.0" 0 { "amount" : 100 } 20000 20000 true + +place_bet alice "1.21.0" 1 "123" "PPY" 200 true +//place_bet bob "1.21.0" 0 "123" "PPY" 200 true + + +place_bet carol "1.21.1" 0 "123" "PPY" 200 true +cancel_bet carol 1.22.2 1 + +get_unmatched_bets_for_bettor 1.21.1 1.2.22 + + +# RESOLVE + +propose_resolve_betting_market_group nathan "2017-08-16-T17:50:01" "1.20.0" [ [ "1.21.0" , 0] [ "1.21.1" , 1] ] 1 + +approve_proposal nathan 1.10.9 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + + + diff --git a/betting2-wimbledon b/betting2-wimbledon new file mode 100644 index 00000000..7562ae83 --- /dev/null +++ b/betting2-wimbledon @@ -0,0 +1,165 @@ + + + +http://api.coindesk.com/charts/data?data=close&startdate=2017-01-01&enddate=2017-08-28&exchanges=bpi,bitstamp,coinbase,itbit,okcoin&dev=1&index=USD + + + +select date, b.coin_type as bought_coin_type, bought_quantity, s.coin_type as sold_coin_type, sold_quantity from external_trades as t + where processed = false + left join coin_types as b ON t.bought_coin_type_id = b.coin_type_id + left join coin_types as s ON t.sold_coin_type_id = s.coin_type_id + + +### PRE... + +see betting-nhl + + +### SPORT + +propose_create_sport nathan "2017-08-16-T08:20:01" [["en","Tennis"], ["pl","Tenis"]] 1 + +// proposal +#get_object 1.10.10 + +approve_proposal nathan 1.10.10 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +#get_object 1.16.2 + + + + +### EVENT GROUOP + +propose_create_event_group nathan "2017-08-16-T08:25:01" [ ["en", "Wimbledon"] ] "1.16.2" 1 + +approve_proposal nathan 1.10.12 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +// proposal +#get_object 1.10.11 + + +//get_object 1.17.1 + +propose_update_event_group nathan "2017-08-16-T09:20:01" "1.17.2" "1.16.2" [ ["en", "US Open"] ] 1 +approve_proposal nathan 1.10.12 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +approve_proposal nathan 1.10.13 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + + +//get_object 1.17.2 + + +### EVENT + +propose_create_event nathan "2017-08-16-T16:15:01" [["en", "R. Federer/T. Berdych"]] [["en","2017"]] "2017-09-16-T07:47:01" "1.17.1" 1 +propose_create_event nathan "2017-08-16-T16:15:01" [["en", "M. Cilic/S. Querrye"]] [["en","2017"]] "2017-09-16-T07:47:01" "1.17.1" 1 + +approve_proposal nathan 1.10.14 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +approve_proposal nathan 1.10.15 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.18.1 +//get_object 1.18.2 + + +# RULES + +propose_create_betting_market_rules nathan "2017-08-16-T16:20:01" [["en","Tennis Rules v1.0"]] [["en", "The winner is the player who wins the last ball in the match.."]] 1 + +approve_proposal nathan 1.10.16 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +0.14 +//get_object 1.19.? + + +### BETTING_MARKET GROUP + +propose_create_betting_market_group nathan "2017-08-16-T16:25:01" [["en", "Moneyline 1st sf men"]] "1.18.1" "1.19.1" "1.3.0" 1 +propose_create_betting_market_group nathan "2017-08-16-T16:25:01" [["en", "Moneyline 2nd sf men"]] "1.18.2" "1.19.1" "1.3.0" 1 + +approve_proposal nathan 1.10.17 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +approve_proposal nathan 1.10.18 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.20.0 + + + + + +### FINAL + +propose_create_event nathan "2017-08-16-T16:30:01" [["en", "R. Federer/M. Cilic"]] [["en","2017"]] "2017-09-16-T07:47:01" "1.17.1" 1 +approve_proposal nathan 1.10.19 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + + +propose_create_betting_market_group nathan "2017-08-16-T16:35:01" [["en", "Moneyline final men"]] "1.18.3" "1.19.1" "1.3.0" 1 +approve_proposal nathan 1.10.20 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + + + + + + + + +### BETTING_MARKET + +propose_create_betting_market nathan "2017-08-16-T17:05:01" "1.20.1" [["en", "T. Berdych defeats R. Federer"}]] [] 1 +propose_create_betting_market nathan "2017-08-16-T17:05:02" "1.20.1" [["en","R. Federer defeats T. Berdych"]] [] 1 + +approve_proposal nathan 1.10.21 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +approve_proposal nathan 1.10.22 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.21.2 +//get_object 1.21.3 + +//list_betting_markets 1.20.1 + + + +propose_create_betting_market nathan "2017-08-16-T17:10:01" "1.20.2" [["en", "M. Cilic defeats S. Querrey"]] [] 1 +propose_create_betting_market nathan "2017-08-16-T17:10:02" "1.20.2" [["en","S. Querrey defeats M. Cilic"]] [] 1 + +approve_proposal nathan 1.10.23 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +approve_proposal nathan 1.10.24 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.21.4 +//get_object 1.21.5 + +//list_betting_markets 1.20.2 + + + + +propose_create_betting_market nathan "2017-08-16-T17:15:01" "1.20.3" [["en", "R. Federer defeats M. Cilic"]] [] 1 +propose_create_betting_market nathan "2017-08-16-T17:15:02" "1.20.3" [["en","M. Cilic defeats R. Federer"]] [] 1 + +approve_proposal nathan 1.10.25 { "active_approvals_to_add" : [ "init0", "init1"] } 1 +approve_proposal nathan 1.10.26 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + +//get_object 1.21.6 +//get_object 1.21.7 + +// + + + +# PLACE_BET + +#place_bet bob "1.21.2" 1 { "amount" : 100 } 20000 20000 true +#place_bet alice "1.21.2" 0 { "amount" : 100 } 20000 20000 true + + + place_bet alice "1.21.1" 1 "123" "PPY" 200 true +place_bet bob "1.21.1" 0 "123" "PPY" 200 true + + + +# RESOLVE + +propose_resolve_betting_market_group nathan "2017-08-16-T17:50:01" "1.20.2" [ [ "1.21.4" , 0] [ "1.21.5" , 1] ] 1 + +approve_proposal nathan 1.10.27 { "active_approvals_to_add" : [ "init0", "init1"] } 1 + + + diff --git a/betting_simulator.html b/betting_simulator.html new file mode 100644 index 00000000..014422e7 --- /dev/null +++ b/betting_simulator.html @@ -0,0 +1,1113 @@ + + + + Peerplays Bookie Sandbox + + + + + + + + + + + + + + + + + + + +
+
+ + diff --git a/docker/default_config.ini b/docker/default_config.ini index a10d1da4..fc7c2d20 100644 --- a/docker/default_config.ini +++ b/docker/default_config.ini @@ -47,28 +47,15 @@ required-participation = false # track-account = # Track market history by grouping orders into buckets of equal size measured in seconds specified as a JSON array of numbers -bucket-size = [15,60,300,3600,86400] +# bucket-size = [15,60,300,3600,86400] +bucket-size = [60,300,900,1800,3600,14400,86400] +# for 1 min, 5 mins, 30 mins, 1h, 4 hs and 1 day. i think this should be the default. # How far back in time to track history for each bucket size, measured in the number of buckets (default: 1000) history-per-size = 1000 -# declare an appender named "stderr" that writes messages to the console -[log.console_appender.stderr] -stream=std_error - -# declare an appender named "p2p" that writes messages to p2p.log -[log.file_appender.p2p] -filename=logs/p2p/p2p.log -# filename can be absolute or relative to this config file - -# route any messages logged to the default logger to the "stderr" logger we -# declared above, if they are info level are higher -[logger.default] -level=info -appenders=stderr - -# route messages sent to the "p2p" logger to the p2p appender declared above -[logger.p2p] -level=debug -appenders=p2p +# Max amount of operations to store in the database, per account (drastically reduces RAM requirements) +max-ops-per-account = 1000 +# Remove old operation history # objects from RAM +partial-operations = true diff --git a/docker/launch b/docker/launch deleted file mode 100644 index 69373413..00000000 --- a/docker/launch +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -[ -e /data_dir/config.ini ] || cp /default_config.ini /data_dir/config.ini - -[ -e /data_dir/pre_exec ] && bash /data_dir/pre_exec -if [ -e /data_dir/extra_args ]; then - /bitshares-2/programs/witness_node/witness_node --data-dir /data_dir `cat /data_dir/extra_args` -else - /bitshares-2/programs/witness_node/witness_node --data-dir /data_dir -fi -[ -e /data_dir/post_exec ] && bash /data_dir/post_exec diff --git a/docker/peerplaysentry.sh b/docker/peerplaysentry.sh new file mode 100644 index 00000000..820ad9f4 --- /dev/null +++ b/docker/peerplaysentry.sh @@ -0,0 +1,82 @@ +#!/bin/bash +PEERPLAYSD="/usr/local/bin/witness_node" + +# For blockchain download +VERSION=`cat /etc/peerplays/version` + +## Supported Environmental Variables +# +# * $PEERPLAYSD_SEED_NODES +# * $PEERPLAYSD_RPC_ENDPOINT +# * $PEERPLAYSD_PLUGINS +# * $PEERPLAYSD_REPLAY +# * $PEERPLAYSD_RESYNC +# * $PEERPLAYSD_P2P_ENDPOINT +# * $PEERPLAYSD_WITNESS_ID +# * $PEERPLAYSD_PRIVATE_KEY +# * $PEERPLAYSD_TRACK_ACCOUNTS +# * $PEERPLAYSD_PARTIAL_OPERATIONS +# * $PEERPLAYSD_MAX_OPS_PER_ACCOUNT +# * $PEERPLAYSD_TRUSTED_NODE +# + +ARGS="" +# Translate environmental variables +if [[ ! -z "$PEERPLAYSD_SEED_NODES" ]]; then + for NODE in $PEERPLAYSD_SEED_NODES ; do + ARGS+=" --seed-node=$NODE" + done +fi +if [[ ! -z "$PEERPLAYSD_RPC_ENDPOINT" ]]; then + ARGS+=" --rpc-endpoint=${PEERPLAYSD_RPC_ENDPOINT}" +fi + +if [[ ! -z "$PEERPLAYSD_REPLAY" ]]; then + ARGS+=" --replay-blockchain" +fi + +if [[ ! -z "$PEERPLAYSD_RESYNC" ]]; then + ARGS+=" --resync-blockchain" +fi + +if [[ ! -z "$PEERPLAYSD_P2P_ENDPOINT" ]]; then + ARGS+=" --p2p-endpoint=${PEERPLAYSD_P2P_ENDPOINT}" +fi + +if [[ ! -z "$PEERPLAYSD_WITNESS_ID" ]]; then + ARGS+=" --witness-id=$PEERPLAYSD_WITNESS_ID" +fi + +if [[ ! -z "$PEERPLAYSD_PRIVATE_KEY" ]]; then + ARGS+=" --private-key=$PEERPLAYSD_PRIVATE_KEY" +fi + +if [[ ! -z "$PEERPLAYSD_TRACK_ACCOUNTS" ]]; then + for ACCOUNT in $PEERPLAYSD_TRACK_ACCOUNTS ; do + ARGS+=" --track-account=$ACCOUNT" + done +fi + +if [[ ! -z "$PEERPLAYSD_PARTIAL_OPERATIONS" ]]; then + ARGS+=" --partial-operations=${PEERPLAYSD_PARTIAL_OPERATIONS}" +fi + +if [[ ! -z "$PEERPLAYSD_MAX_OPS_PER_ACCOUNT" ]]; then + ARGS+=" --max-ops-per-account=${PEERPLAYSD_MAX_OPS_PER_ACCOUNT}" +fi + +if [[ ! -z "$PEERPLAYSD_TRUSTED_NODE" ]]; then + ARGS+=" --trusted-node=${PEERPLAYSD_TRUSTED_NODE}" +fi + +## Link the peerplays config file into home +## This link has been created in Dockerfile, already +ln -f -s /etc/peerplays/config.ini /var/lib/peerplays + +# Plugins need to be provided in a space-separated list, which +# makes it necessary to write it like this +if [[ ! -z "$PEERPLAYSD_PLUGINS" ]]; then + $PEERPLAYSD --data-dir ${HOME} ${ARGS} ${PEERPLAYSD_ARGS} --plugins "${PEERPLAYSD_PLUGINS}" +else + $PEERPLAYSD --data-dir ${HOME} ${ARGS} ${PEERPLAYSD_ARGS} +fi diff --git a/gui_version b/gui_version index b16e8d03..f7a4e4ce 100644 --- a/gui_version +++ b/gui_version @@ -1 +1 @@ -2.0.160208 +2.0.170522 diff --git a/libraries/app/CMakeLists.txt b/libraries/app/CMakeLists.txt index 2dd91f63..a6157560 100644 --- a/libraries/app/CMakeLists.txt +++ b/libraries/app/CMakeLists.txt @@ -12,7 +12,8 @@ add_library( graphene_app ) # need to link graphene_debug_witness because plugins aren't sufficiently isolated #246 -target_link_libraries( graphene_app graphene_market_history graphene_account_history graphene_chain fc graphene_db graphene_net graphene_time graphene_utilities graphene_debug_witness ) +#target_link_libraries( graphene_app graphene_market_history graphene_account_history graphene_chain fc graphene_db graphene_net graphene_utilities graphene_debug_witness ) +target_link_libraries( graphene_app graphene_market_history graphene_account_history graphene_accounts_list graphene_chain fc graphene_db graphene_net graphene_time graphene_utilities graphene_debug_witness graphene_bookie ) target_include_directories( graphene_app PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/../egenesis/include" ) @@ -28,3 +29,4 @@ INSTALL( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +INSTALL( FILES ${HEADERS} DESTINATION "include/graphene/app" ) diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index 0559c51f..6c6359c2 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -40,6 +40,7 @@ #include #include +#include namespace graphene { namespace app { @@ -80,6 +81,10 @@ namespace graphene { namespace app { { _database_api = std::make_shared< database_api >( std::ref( *_app.chain_database() ) ); } + else if( api_name == "block_api" ) + { + _block_api = std::make_shared< block_api >( std::ref( *_app.chain_database() ) ); + } else if( api_name == "network_broadcast_api" ) { _network_broadcast_api = std::make_shared< network_broadcast_api >( std::ref( _app ) ); @@ -96,15 +101,39 @@ namespace graphene { namespace app { { _crypto_api = std::make_shared< crypto_api >(); } + else if( api_name == "asset_api" ) + { + _asset_api = std::make_shared< asset_api >( std::ref( *_app.chain_database() ) ); + } else if( api_name == "debug_api" ) { // can only enable this API if the plugin was loaded if( _app.get_plugin( "debug_witness" ) ) _debug_api = std::make_shared< graphene::debug_witness::debug_api >( std::ref(_app) ); } + else if( api_name == "bookie_api" ) + { + // can only enable this API if the plugin was loaded + if( _app.get_plugin( "bookie" ) ) + _bookie_api = std::make_shared(std::ref(_app)); + } return; } + // block_api + block_api::block_api(graphene::chain::database& db) : _db(db) { } + block_api::~block_api() { } + + vector> block_api::get_blocks(uint32_t block_num_from, uint32_t block_num_to)const + { + FC_ASSERT( block_num_to >= block_num_from ); + vector> res; + for(uint32_t block_num=block_num_from; block_num<=block_num_to; block_num++) { + res.push_back(_db.fetch_block_by_number(block_num)); + } + return res; + } + network_broadcast_api::network_broadcast_api(application& a):_app(a) { _applied_block_connection = _app.chain_database()->applied_block.connect([this](const signed_block& b){ on_applied_block(b); }); @@ -138,6 +167,16 @@ namespace graphene { namespace app { _app.p2p_node()->broadcast_transaction(trx); } + fc::variant network_broadcast_api::broadcast_transaction_synchronous(const signed_transaction& trx) + { + fc::promise::ptr prom( new fc::promise() ); + broadcast_transaction_with_callback( [=]( const fc::variant& v ){ + prom->set_value(v); + }, trx ); + + return fc::future(prom).wait(); + } + void network_broadcast_api::broadcast_block( const signed_block& b ) { _app.chain_database()->push_block(b); @@ -194,6 +233,12 @@ namespace graphene { namespace app { return *_network_broadcast_api; } + fc::api login_api::block()const + { + FC_ASSERT(_block_api); + return *_block_api; + } + fc::api login_api::network_node()const { FC_ASSERT(_network_node_api); @@ -218,12 +263,24 @@ namespace graphene { namespace app { return *_crypto_api; } + fc::api login_api::asset() const + { + FC_ASSERT(_asset_api); + return *_asset_api; + } + fc::api login_api::debug() const { FC_ASSERT(_debug_api); return *_debug_api; } + fc::api login_api::bookie() const + { + FC_ASSERT(_bookie_api); + return *_bookie_api; + } + #if 0 vector get_relevant_accounts( const object* obj ) { @@ -306,6 +363,18 @@ namespace graphene { namespace app { } case balance_object_type:{ /** these are free from any accounts */ break; + } + case sport_object_type: + case event_group_object_type: + case event_object_type: + case betting_market_group_object_type: + case betting_market_object_type: + /** these are free from any accounts */ + break; + case bet_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + result.push_back( aobj->bettor_id ); } case tournament_object_type:{ const tournament_object* tournament_obj = dynamic_cast(obj); assert(tournament_obj); @@ -372,6 +441,10 @@ namespace graphene { namespace app { break; case impl_fba_accumulator_object_type: break; + case impl_betting_market_position_object_type: + break; + case impl_global_betting_statistics_object_type: + break; } } return result; @@ -403,13 +476,13 @@ namespace graphene { namespace app { return result; } - vector history_api::get_account_history( account_id_type account, - operation_history_id_type stop, - unsigned limit, + vector history_api::get_account_history( account_id_type account, + operation_history_id_type stop, + unsigned limit, operation_history_id_type start ) const { FC_ASSERT( _app.chain_database() ); - const auto& db = *_app.chain_database(); + const auto& db = *_app.chain_database(); FC_ASSERT( limit <= 100 ); vector result; const auto& stats = account(db).statistics(db); @@ -417,7 +490,7 @@ namespace graphene { namespace app { const account_transaction_history_object* node = &stats.most_recent_op(db); if( start == operation_history_id_type() ) start = node->operation_id; - + while(node && node->operation_id.instance.value > stop.instance.value && result.size() < limit) { if( node->operation_id.instance.value <= start.instance.value ) @@ -426,38 +499,82 @@ namespace graphene { namespace app { node = nullptr; else node = &node->next(db); } - + return result; } - - vector history_api::get_relative_account_history( account_id_type account, - uint32_t stop, - unsigned limit, + + vector history_api::get_account_history_operations( account_id_type account, + int operation_id, + operation_history_id_type start, + operation_history_id_type stop, + unsigned limit) const + { + FC_ASSERT( _app.chain_database() ); + const auto& db = *_app.chain_database(); + FC_ASSERT( limit <= 100 ); + vector result; + const auto& stats = account(db).statistics(db); + if( stats.most_recent_op == account_transaction_history_id_type() ) return result; + const account_transaction_history_object* node = &stats.most_recent_op(db); + if( start == operation_history_id_type() ) + start = node->operation_id; + + while(node && node->operation_id.instance.value > stop.instance.value && result.size() < limit) + { + if( node->operation_id.instance.value <= start.instance.value ) { + + if(node->operation_id(db).op.which() == operation_id) + result.push_back( node->operation_id(db) ); + } + if( node->next == account_transaction_history_id_type() ) + node = nullptr; + else node = &node->next(db); + } + return result; + } + + + vector history_api::get_relative_account_history( account_id_type account, + uint32_t stop, + unsigned limit, uint32_t start) const { FC_ASSERT( _app.chain_database() ); const auto& db = *_app.chain_database(); FC_ASSERT(limit <= 100); vector result; + const auto& stats = account(db).statistics(db); if( start == 0 ) - start = account(db).statistics(db).total_ops; - else start = min( account(db).statistics(db).total_ops, start ); - const auto& hist_idx = db.get_index_type(); - const auto& by_seq_idx = hist_idx.indices().get(); - - auto itr = by_seq_idx.upper_bound( boost::make_tuple( account, start ) ); - auto itr_stop = by_seq_idx.lower_bound( boost::make_tuple( account, stop ) ); - --itr; - - while ( itr != itr_stop && result.size() < limit ) + start = stats.total_ops; + else + start = min( stats.total_ops, start ); + + + if( start >= stop && start > stats.removed_ops && limit > 0 ) { - result.push_back( itr->operation_id(db) ); - --itr; + const auto& hist_idx = db.get_index_type(); + const auto& by_seq_idx = hist_idx.indices().get(); + + auto itr = by_seq_idx.upper_bound( boost::make_tuple( account, start ) ); + auto itr_stop = by_seq_idx.lower_bound( boost::make_tuple( account, stop ) ); + + do + { + --itr; + result.push_back( itr->operation_id(db) ); + } + while ( itr != itr_stop && result.size() < limit ); } - return result; } + vector history_api::list_core_accounts()const + { + auto list = _app.get_plugin( "accounts_list" ); + FC_ASSERT( list ); + return list->list_accounts(); + } + flat_set history_api::get_market_history_buckets()const { auto hist = _app.get_plugin( "market_history" ); @@ -490,14 +607,14 @@ namespace graphene { namespace app { } return result; } FC_CAPTURE_AND_RETHROW( (a)(b)(bucket_seconds)(start)(end) ) } - + crypto_api::crypto_api(){}; - + blind_signature crypto_api::blind_sign( const extended_private_key_type& key, const blinded_hash& hash, int i ) { return fc::ecc::extended_private_key( key ).blind_sign( hash, i ); } - + signature_type crypto_api::unblind_signature( const extended_private_key_type& key, const extended_public_key_type& bob, const blind_signature& sig, @@ -506,32 +623,32 @@ namespace graphene { namespace app { { return fc::ecc::extended_private_key( key ).unblind_signature( extended_public_key( bob ), sig, hash, i ); } - + commitment_type crypto_api::blind( const blind_factor_type& blind, uint64_t value ) { return fc::ecc::blind( blind, value ); } - + blind_factor_type crypto_api::blind_sum( const std::vector& blinds_in, uint32_t non_neg ) { return fc::ecc::blind_sum( blinds_in, non_neg ); } - + bool crypto_api::verify_sum( const std::vector& commits_in, const std::vector& neg_commits_in, int64_t excess ) { return fc::ecc::verify_sum( commits_in, neg_commits_in, excess ); } - + verify_range_result crypto_api::verify_range( const commitment_type& commit, const std::vector& proof ) { verify_range_result result; result.success = fc::ecc::verify_range( result.min_val, result.max_val, commit, proof ); return result; } - - std::vector crypto_api::range_proof_sign( uint64_t min_value, - const commitment_type& commit, - const blind_factor_type& commit_blind, + + std::vector crypto_api::range_proof_sign( uint64_t min_value, + const commitment_type& commit, + const blind_factor_type& commit_blind, const blind_factor_type& nonce, int8_t base10_exp, uint8_t min_bits, @@ -539,26 +656,100 @@ namespace graphene { namespace app { { return fc::ecc::range_proof_sign( min_value, commit, commit_blind, nonce, base10_exp, min_bits, actual_value ); } - + verify_range_proof_rewind_result crypto_api::verify_range_proof_rewind( const blind_factor_type& nonce, - const commitment_type& commit, + const commitment_type& commit, const std::vector& proof ) { verify_range_proof_rewind_result result; - result.success = fc::ecc::verify_range_proof_rewind( result.blind_out, - result.value_out, - result.message_out, - nonce, - result.min_val, - result.max_val, - const_cast< commitment_type& >( commit ), + result.success = fc::ecc::verify_range_proof_rewind( result.blind_out, + result.value_out, + result.message_out, + nonce, + result.min_val, + result.max_val, + const_cast< commitment_type& >( commit ), proof ); return result; } - + range_proof_info crypto_api::range_get_info( const std::vector& proof ) { return fc::ecc::range_get_info( proof ); } + // asset_api + asset_api::asset_api(graphene::chain::database& db) : _db(db) { } + asset_api::~asset_api() { } + + vector asset_api::get_asset_holders( asset_id_type asset_id, uint32_t start, uint32_t limit ) const { + FC_ASSERT(limit <= 100); + + const auto& bal_idx = _db.get_index_type< account_balance_index >().indices().get< by_asset_balance >(); + auto range = bal_idx.equal_range( boost::make_tuple( asset_id ) ); + + vector result; + + uint32_t index = 0; + for( const account_balance_object& bal : boost::make_iterator_range( range.first, range.second ) ) + { + if( result.size() >= limit ) + break; + + if( bal.balance.value == 0 ) + continue; + + if( index++ < start ) + continue; + + const auto account = _db.find(bal.owner); + + account_asset_balance aab; + aab.name = account->name; + aab.account_id = account->id; + aab.amount = bal.balance.value; + + result.push_back(aab); + } + + return result; + } + // get number of asset holders. + int asset_api::get_asset_holders_count( asset_id_type asset_id ) const { + + const auto& bal_idx = _db.get_index_type< account_balance_index >().indices().get< by_asset_balance >(); + auto range = bal_idx.equal_range( boost::make_tuple( asset_id ) ); + + int count = boost::distance(range) - 1; + + return count; + } + // function to get vector of system assets with holders count. + vector asset_api::get_all_asset_holders() const { + + vector result; + + vector total_assets; + for( const asset_object& asset_obj : _db.get_index_type().indices() ) + { + const auto& dasset_obj = asset_obj.dynamic_asset_data_id(_db); + + asset_id_type asset_id; + asset_id = dasset_obj.id; + + const auto& bal_idx = _db.get_index_type< account_balance_index >().indices().get< by_asset_balance >(); + auto range = bal_idx.equal_range( boost::make_tuple( asset_id ) ); + + int count = boost::distance(range) - 1; + + asset_holders ah; + ah.asset_id = asset_id; + ah.count = count; + + result.push_back(ah); + } + + return result; + } + } } // graphene::app diff --git a/libraries/app/application.cpp b/libraries/app/application.cpp index 105a9912..5b079b94 100644 --- a/libraries/app/application.cpp +++ b/libraries/app/application.cpp @@ -28,15 +28,12 @@ #include #include -#include #include #include #include -#include - #include #include @@ -46,10 +43,12 @@ #include #include #include +#include #include #include #include +#include #include @@ -198,6 +197,7 @@ namespace detail { "54.37.235.164:7777", // melea-trust "peerplays-seed.lukestokes.info:7777" // lukestokes-witness }; + for( const string& endpoint_string : seeds ) { try { @@ -254,23 +254,49 @@ namespace detail { FC_CAPTURE_AND_RETHROW((endpoint_string)) } + void new_connection( const fc::http::websocket_connection_ptr& c ) + { + auto wsc = std::make_shared(*c); + auto login = std::make_shared( std::ref(*_self) ); + login->enable_api("database_api"); + + wsc->register_api(login->database()); + wsc->register_api(fc::api(login)); + + wsc->register_api(fc::api(login)); + + c->set_session_data( wsc ); + + std::string username = "*"; + std::string password = "*"; + + // Try to extract login information from "Authorization" header if present + std::string auth = c->get_request_header("Authorization"); + if( boost::starts_with(auth, "Basic ") ) { + + FC_ASSERT( auth.size() > 6 ); + auto user_pass = fc::base64_decode(auth.substr(6)); + + std::vector parts; + boost::split( parts, user_pass, boost::is_any_of(":") ); + + FC_ASSERT(parts.size() == 2); + + username = parts[0]; + password = parts[1]; + } + + login->login(username, password); + } + void reset_websocket_server() { try { if( !_options->count("rpc-endpoint") ) return; - bool enable_deflate_compression = _options->count("enable-permessage-deflate") != 0; + _websocket_server = std::make_shared(); + _websocket_server->on_connection( std::bind(&application_impl::new_connection, this, std::placeholders::_1) ); - _websocket_server = std::make_shared(enable_deflate_compression); - - _websocket_server->on_connection([&]( const fc::http::websocket_connection_ptr& c ){ - auto wsc = std::make_shared(*c); - auto login = std::make_shared( std::ref(*_self) ); - auto db_api = std::make_shared( std::ref(*_self->chain_database()) ); - wsc->register_api(fc::api(db_api)); - wsc->register_api(fc::api(login)); - c->set_session_data( wsc ); - }); ilog("Configured websocket rpc to listen on ${ip}", ("ip",_options->at("rpc-endpoint").as())); _websocket_server->listen( fc::ip::endpoint::from_string(_options->at("rpc-endpoint").as()) ); _websocket_server->start_accept(); @@ -288,17 +314,9 @@ namespace detail { } string password = _options->count("server-pem-password") ? _options->at("server-pem-password").as() : ""; - bool enable_deflate_compression = _options->count("enable-permessage-deflate") != 0; - _websocket_tls_server = std::make_shared( _options->at("server-pem").as(), password, enable_deflate_compression ); + _websocket_tls_server = std::make_shared( _options->at("server-pem").as(), password ); + _websocket_tls_server->on_connection( std::bind(&application_impl::new_connection, this, std::placeholders::_1) ); - _websocket_tls_server->on_connection([&]( const fc::http::websocket_connection_ptr& c ){ - auto wsc = std::make_shared(*c); - auto login = std::make_shared( std::ref(*_self) ); - auto db_api = std::make_shared( std::ref(*_self->chain_database()) ); - wsc->register_api(fc::api(db_api)); - wsc->register_api(fc::api(login)); - c->set_session_data( wsc ); - }); ilog("Configured websocket TLS rpc to listen on ${ip}", ("ip",_options->at("rpc-tls-endpoint").as())); _websocket_tls_server->listen( fc::ip::endpoint::from_string(_options->at("rpc-tls-endpoint").as()) ); _websocket_tls_server->start_accept(); @@ -339,7 +357,7 @@ namespace detail { bool modified_genesis = false; if( _options->count("genesis-timestamp") ) { - genesis.initial_timestamp = fc::time_point_sec( graphene::time::now() ) + genesis.initial_parameters.block_interval + _options->at("genesis-timestamp").as(); + genesis.initial_timestamp = fc::time_point_sec( fc::time_point::now() ) + genesis.initial_parameters.block_interval + _options->at("genesis-timestamp").as(); genesis.initial_timestamp -= genesis.initial_timestamp.sec_since_epoch() % genesis.initial_parameters.block_interval; modified_genesis = true; std::cerr << "Used genesis timestamp: " << genesis.initial_timestamp.to_iso_string() << " (PLEASE RECORD THIS)\n"; @@ -390,79 +408,67 @@ namespace detail { } _chain_db->add_checkpoints( loaded_checkpoints ); - if( _options->count("replay-blockchain") ) + bool replay = false; + std::string replay_reason = "reason not provided"; + + // never replay if data dir is empty + if( fc::exists( _data_dir ) && fc::directory_iterator( _data_dir ) != fc::directory_iterator() ) { - ilog("Replaying blockchain on user request."); - _chain_db->reindex(_data_dir/"blockchain", initial_state()); - } else if( clean ) { - - auto is_new = [&]() -> bool + if( _options->count("replay-blockchain") ) { - // directory doesn't exist - if( !fc::exists( _data_dir ) ) - return true; - // if directory exists but is empty, return true; else false. - return ( fc::directory_iterator( _data_dir ) == fc::directory_iterator() ); - }; - - auto is_outdated = [&]() -> bool + replay = true; + replay_reason = "replay-blockchain argument specified"; + } + else if( !clean ) { - if( !fc::exists( _data_dir / "db_version" ) ) - return true; - std::string version_str; - fc::read_file_contents( _data_dir / "db_version", version_str ); - return (version_str != GRAPHENE_CURRENT_DB_VERSION); - }; - - bool need_reindex = (!is_new() && is_outdated()); - std::string reindex_reason = "version upgrade"; - - if( !need_reindex ) + replay = true; + replay_reason = "unclean shutdown detected"; + } + else if( !fc::exists( _data_dir / "db_version" ) ) { - try + replay = true; + replay_reason = "db_version file not found"; + } + else + { + std::string version_string; + fc::read_file_contents( _data_dir / "db_version", version_string ); + + if( version_string != GRAPHENE_CURRENT_DB_VERSION ) { - _chain_db->open(_data_dir / "blockchain", initial_state); - } - catch( const fc::exception& e ) - { - ilog( "caught exception ${e} in open()", ("e", e.to_detail_string()) ); - need_reindex = true; - reindex_reason = "exception in open()"; + replay = true; + replay_reason = "db_version file content mismatch"; } } - - if( need_reindex ) - { - ilog("Replaying blockchain due to ${reason}", ("reason", reindex_reason) ); - - fc::remove_all( _data_dir / "db_version" ); - _chain_db->reindex(_data_dir / "blockchain", initial_state()); - - // doing this down here helps ensure that DB will be wiped - // if any of the above steps were interrupted on a previous run - if( !fc::exists( _data_dir / "db_version" ) ) - { - std::ofstream db_version( - (_data_dir / "db_version").generic_string().c_str(), - std::ios::out | std::ios::binary | std::ios::trunc ); - std::string version_string = GRAPHENE_CURRENT_DB_VERSION; - db_version.write( version_string.c_str(), version_string.size() ); - db_version.close(); - } - } - } else { - wlog("Detected unclean shutdown. Replaying blockchain..."); - _chain_db->reindex(_data_dir / "blockchain", initial_state()); } - if (!_options->count("genesis-json") && - _chain_db->get_chain_id() != graphene::egenesis::get_egenesis_chain_id()) { - elog("Detected old database. Nuking and starting over."); - _chain_db->wipe(_data_dir / "blockchain", true); - _chain_db.reset(); - _chain_db = std::make_shared(); - _chain_db->add_checkpoints(loaded_checkpoints); - _chain_db->open(_data_dir / "blockchain", initial_state); + if( !replay ) + { + try + { + _chain_db->open( _data_dir / "blockchain", initial_state ); + } + catch( const fc::exception& e ) + { + ilog( "Caught exception ${e} in open()", ("e", e.to_detail_string()) ); + + replay = true; + replay_reason = "exception in open()"; + } + } + + if( replay ) + { + ilog( "Replaying blockchain due to: ${reason}", ("reason", replay_reason) ); + + fc::remove_all( _data_dir / "db_version" ); + _chain_db->reindex( _data_dir / "blockchain", initial_state() ); + + const auto mode = std::ios::out | std::ios::binary | std::ios::trunc; + std::ofstream db_version( (_data_dir / "db_version").generic_string().c_str(), mode ); + std::string version_string = GRAPHENE_CURRENT_DB_VERSION; + db_version.write( version_string.c_str(), version_string.size() ); + db_version.close(); } if( _options->count("force-validate") ) @@ -471,8 +477,6 @@ namespace detail { _force_validate = true; } - graphene::time::now(); - if( _options->count("api-access") ) _apiaccess = fc::json::from_file( _options->at("api-access").as() ) .as(); @@ -488,6 +492,7 @@ namespace detail { wild_access.allowed_apis.push_back( "network_broadcast_api" ); wild_access.allowed_apis.push_back( "history_api" ); wild_access.allowed_apis.push_back( "crypto_api" ); + wild_access.allowed_apis.push_back( "bookie_api" ); _apiaccess.permission_map["*"] = wild_access; } @@ -496,6 +501,7 @@ namespace detail { reset_websocket_tls_server(); } FC_LOG_AND_RETHROW() } + optional< api_access_info > get_api_access_info(const string& username)const { optional< api_access_info > result; @@ -540,21 +546,21 @@ namespace detail { virtual bool handle_block(const graphene::net::block_message& blk_msg, bool sync_mode, std::vector& contained_transaction_message_ids) override { try { - - auto latency = graphene::time::now() - blk_msg.block.timestamp; + auto latency = fc::time_point::now() - blk_msg.block.timestamp; FC_ASSERT( (latency.count()/1000) > -5000, "Rejecting block with timestamp in the future" ); if (!sync_mode || blk_msg.block.block_num() % 10000 == 0) { const auto& witness = blk_msg.block.witness(*_chain_db); const auto& witness_account = witness.witness_account(*_chain_db); auto last_irr = _chain_db->get_dynamic_global_properties().last_irreversible_block_num; - ilog("Got block: #${n} time: ${t} latency: ${l} ms from: ${w} irreversible: ${i} (-${d})", + ilog("Got block: #${n} time: ${t} latency: ${l} ms from: ${w} irreversible: ${i} (-${d})", ("t",blk_msg.block.timestamp) ("n", blk_msg.block.block_num()) ("l", (latency.count()/1000)) ("w",witness_account.name) ("i",last_irr)("d",blk_msg.block.block_num()-last_irr) ); } + FC_ASSERT( (latency.count()/1000) > -5000, "Rejecting block with timestamp in the future" ); try { // TODO: in the case where this block is valid but on a fork that's too old for us to switch to, @@ -643,7 +649,7 @@ namespace detail { result.reserve(limit); block_id_type last_known_block_id; - + if (blockchain_synopsis.empty() || (blockchain_synopsis.size() == 1 && blockchain_synopsis[0] == block_id_type())) { @@ -704,13 +710,13 @@ namespace detail { } /** - * Returns a synopsis of the blockchain used for syncing. This consists of a list of + * Returns a synopsis of the blockchain used for syncing. This consists of a list of * block hashes at intervals exponentially increasing towards the genesis block. * When syncing to a peer, the peer uses this data to determine if we're on the same * fork as they are, and if not, what blocks they need to send us to get us on their * fork. * - * In the over-simplified case, this is a straighforward synopsis of our current + * In the over-simplified case, this is a straighforward synopsis of our current * preferred blockchain; when we first connect up to a peer, this is what we will be sending. * It looks like this: * If the blockchain is empty, it will return the empty list. @@ -726,7 +732,7 @@ namespace detail { * the last item in the list will be the hash of the most recent block on our preferred chain * so if the blockchain had 26 blocks labeled a - z, the synopsis would be: * a n u x z - * the idea being that by sending a small (<30) number of block ids, we can summarize a huge + * the idea being that by sending a small (<30) number of block ids, we can summarize a huge * blockchain. The block ids are more dense near the end of the chain where because we are * more likely to be almost in sync when we first connect, and forks are likely to be short. * If the peer we're syncing with in our example is on a fork that started at block 'v', @@ -741,27 +747,27 @@ namespace detail { * no reason to fetch the blocks. * * Second, when a peer replies to our initial synopsis and gives us a list of the blocks they think - * we are missing, they only send a chunk of a few thousand blocks at once. After we get those + * we are missing, they only send a chunk of a few thousand blocks at once. After we get those * block ids, we need to request more blocks by sending another synopsis (we can't just say "send me * the next 2000 ids" because they may have switched forks themselves and they don't track what * they've sent us). For faster performance, we want to get a fairly long list of block ids first, * then start downloading the blocks. * The peer doesn't handle these follow-up block id requests any different from the initial request; * it treats the synopsis we send as our blockchain and bases its response entirely off that. So to - * get the response we want (the next chunk of block ids following the last one they sent us, or, + * get the response we want (the next chunk of block ids following the last one they sent us, or, * failing that, the shortest fork off of the last list of block ids they sent), we need to construct * a synopsis as if our blockchain was made up of: * 1. the blocks in our block chain up to the fork point (if there is a fork) or the head block (if no fork) * 2. the blocks we've already pushed from their fork (if there's a fork) * 3. the block ids they've previously sent us - * Segment 3 is handled in the p2p code, it just tells us the number of blocks it has (in + * Segment 3 is handled in the p2p code, it just tells us the number of blocks it has (in * number_of_blocks_after_reference_point) so we can leave space in the synopsis for them. * We're responsible for constructing the synopsis of Segments 1 and 2 from our active blockchain and * fork database. The reference_point parameter is the last block from that peer that has been * successfully pushed to the blockchain, so that tells us whether the peer is on a fork or on * the main chain. */ - virtual std::vector get_blockchain_synopsis(const item_hash_t& reference_point, + virtual std::vector get_blockchain_synopsis(const item_hash_t& reference_point, uint32_t number_of_blocks_after_reference_point) override { try { std::vector synopsis; @@ -786,13 +792,13 @@ namespace detail { if (reference_point_block_num < low_block_num) { - // we're on the same fork (at least as far as reference_point) but we've passed - // reference point and could no longer undo that far if we diverged after that + // we're on the same fork (at least as far as reference_point) but we've passed + // reference point and could no longer undo that far if we diverged after that // block. This should probably only happen due to a race condition where - // the network thread calls this function, and then immediately pushes a bunch of blocks, + // the network thread calls this function, and then immediately pushes a bunch of blocks, // then the main thread finally processes this function. // with the current framework, there's not much we can do to tell the network - // thread what our current head block is, so we'll just pretend that + // thread what our current head block is, so we'll just pretend that // our head is actually the reference point. // this *may* enable us to fetch blocks that we're unable to push, but that should // be a rare case (and correctly handled) @@ -836,7 +842,7 @@ namespace detail { if (non_fork_high_block_num < low_block_num) { wlog("Unable to generate a usable synopsis because the peer we're generating it for forked too long ago " - "(our chains diverge after block #${non_fork_high_block_num} but only undoable to block #${low_block_num})", + "(our chains diverge after block #${non_fork_high_block_num} but only undoable to block #${low_block_num})", ("low_block_num", low_block_num) ("non_fork_high_block_num", non_fork_high_block_num)); FC_THROW_EXCEPTION(graphene::net::block_older_than_undo_history, "Peer is are on a fork I'm unable to switch to"); @@ -856,11 +862,11 @@ namespace detail { low_block_num = 1; // at this point: - // low_block_num is the block before the first block we can undo, + // low_block_num is the block before the first block we can undo, // non_fork_high_block_num is the block before the fork (if the peer is on a fork, or otherwise it is the same as high_block_num) // high_block_num is the block number of the reference block, or the end of the chain if no reference provided - // true_high_block_num is the ending block number after the network code appends any item ids it + // true_high_block_num is the ending block number after the network code appends any item ids it // knows about that we don't uint32_t true_high_block_num = high_block_num + number_of_blocks_after_reference_point; do @@ -916,12 +922,6 @@ namespace detail { return fc::time_point_sec::min(); } FC_CAPTURE_AND_RETHROW( (block_id) ) } - /** returns graphene::time::now() */ - virtual fc::time_point_sec get_blockchain_now() override - { - return graphene::time::now(); - } - virtual item_hash_t get_head_block_id() const override { return _chain_db->head_block_id(); @@ -987,8 +987,6 @@ void application::set_program_options(boost::program_options::options_descriptio ("checkpoint,c", bpo::value>()->composing(), "Pairs of [BLOCK_NUM,BLOCK_ID] that should be enforced as checkpoints.") ("rpc-endpoint", bpo::value()->implicit_value("127.0.0.1:8090"), "Endpoint for websocket RPC to listen on") ("rpc-tls-endpoint", bpo::value()->implicit_value("127.0.0.1:8089"), "Endpoint for TLS websocket RPC to listen on") - ("enable-permessage-deflate", "Enable support for per-message deflate compression in the websocket servers " - "(--rpc-endpoint and --rpc-tls-endpoint), disabled by default") ("server-pem,p", bpo::value()->implicit_value("server.pem"), "The TLS certificate file for this server") ("server-pem-password,P", bpo::value()->implicit_value(""), "Password for this certificate") ("genesis-json", bpo::value(), "File to read Genesis State from") diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index 3941d890..d3af2f29 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -43,6 +43,8 @@ #define GET_REQUIRED_FEES_MAX_RECURSION 4 +typedef std::map< std::pair, std::vector > market_queue_type; + namespace graphene { namespace app { class database_api_impl; @@ -58,13 +60,14 @@ class database_api_impl : public std::enable_shared_from_this fc::variants get_objects(const vector& ids)const; // Subscriptions - void set_subscribe_callback( std::function cb, bool clear_filter ); + void set_subscribe_callback( std::function cb, bool notify_remove_create ); void set_pending_transaction_callback( std::function cb ); void set_block_applied_callback( std::function cb ); void cancel_all_subscriptions(); // Blocks and transactions optional get_block_header(uint32_t block_num)const; + map> get_block_header_batch(const vector block_nums)const; optional get_block(uint32_t block_num)const; processed_transaction get_transaction( uint32_t block_num, uint32_t trx_in_block )const; @@ -74,9 +77,11 @@ class database_api_impl : public std::enable_shared_from_this fc::variant_object get_config()const; chain_id_type get_chain_id()const; dynamic_global_property_object get_dynamic_global_properties()const; + global_betting_statistics_object get_global_betting_statistics() const; // Keys vector> get_key_references( vector key )const; + bool is_public_key_registered(string public_key) const; // Accounts vector> get_accounts(const vector& account_ids)const; @@ -99,6 +104,15 @@ class database_api_impl : public std::enable_shared_from_this vector list_assets(const string& lower_bound_symbol, uint32_t limit)const; vector> lookup_asset_symbols(const vector& symbols_or_ids)const; + // Peerplays + vector list_sports() const; + vector list_event_groups(sport_id_type sport_id) const; + vector list_events_in_group(event_group_id_type event_group_id) const; + vector list_betting_market_groups(event_id_type) const; + vector list_betting_markets(betting_market_group_id_type) const; + vector get_unmatched_bets_for_bettor(betting_market_id_type, account_id_type) const; + vector get_all_unmatched_bets_for_bettor(account_id_type) const; + // Markets / feeds vector get_limit_orders(asset_id_type a, asset_id_type b, uint32_t limit)const; vector get_call_orders(asset_id_type a, uint32_t limit)const; @@ -168,22 +182,52 @@ class database_api_impl : public std::enable_shared_from_this { if( !_subscribe_callback ) return false; - return true; + return _subscribe_filter.contains( i ); } + bool is_impacted_account( const flat_set& accounts) + { + if( !_subscribed_accounts.size() || !accounts.size() ) + return false; + + return std::any_of(accounts.begin(), accounts.end(), [this](const account_id_type& account) { + return _subscribed_accounts.find(account) != _subscribed_accounts.end(); + }); + } + + template + void enqueue_if_subscribed_to_market(const object* obj, market_queue_type& queue, bool full_object=true) + { + const T* order = dynamic_cast(obj); + FC_ASSERT( order != nullptr); + + auto market = order->get_market(); + + auto sub = _market_subscriptions.find( market ); + if( sub != _market_subscriptions.end() ) { + queue[market].emplace_back( full_object ? obj->to_variant() : fc::variant(obj->id) ); + } + } + void broadcast_updates( const vector& updates ); + void broadcast_market_updates( const market_queue_type& queue); + void handle_object_changed(bool force_notify, bool full_object, const vector& ids, const flat_set& impacted_accounts, std::function find_object); /** called every time a block is applied to report the objects that were changed */ - void on_objects_changed(const vector& ids); - void on_objects_removed(const vector& objs); + void on_objects_new(const vector& ids, const flat_set& impacted_accounts); + void on_objects_changed(const vector& ids, const flat_set& impacted_accounts); + void on_objects_removed(const vector& ids, const vector& objs, const flat_set& impacted_accounts); void on_applied_block(); - mutable fc::bloom_filter _subscribe_filter; + bool _notify_remove_create = false; + mutable fc::bloom_filter _subscribe_filter; + std::set _subscribed_accounts; std::function _subscribe_callback; std::function _pending_trx_callback; std::function _block_applied_callback; + boost::signals2::scoped_connection _new_connection; boost::signals2::scoped_connection _change_connection; boost::signals2::scoped_connection _removed_connection; boost::signals2::scoped_connection _applied_block_connection; @@ -206,11 +250,14 @@ database_api::~database_api() {} database_api_impl::database_api_impl( graphene::chain::database& db ):_db(db) { wlog("creating database api ${x}", ("x",int64_t(this)) ); - _change_connection = _db.changed_objects.connect([this](const vector& ids) { - on_objects_changed(ids); + _new_connection = _db.new_objects.connect([this](const vector& ids, const flat_set& impacted_accounts) { + on_objects_new(ids, impacted_accounts); }); - _removed_connection = _db.removed_objects.connect([this](const vector& objs) { - on_objects_removed(objs); + _change_connection = _db.changed_objects.connect([this](const vector& ids, const flat_set& impacted_accounts) { + on_objects_changed(ids, impacted_accounts); + }); + _removed_connection = _db.removed_objects.connect([this](const vector& ids, const vector& objs, const flat_set& impacted_accounts) { + on_objects_removed(ids, objs, impacted_accounts); }); _applied_block_connection = _db.applied_block.connect([this](const signed_block&){ on_applied_block(); }); @@ -246,10 +293,6 @@ fc::variants database_api_impl::get_objects(const vector& ids)co this->subscribe_to_item( id ); } } - else - { - elog( "getObjects without subscribe callback??" ); - } fc::variants result; result.reserve(ids.size()); @@ -270,24 +313,24 @@ fc::variants database_api_impl::get_objects(const vector& ids)co // // ////////////////////////////////////////////////////////////////////// -void database_api::set_subscribe_callback( std::function cb, bool clear_filter ) +void database_api::set_subscribe_callback( std::function cb, bool notify_remove_create ) { - my->set_subscribe_callback( cb, clear_filter ); + my->set_subscribe_callback( cb, notify_remove_create ); } -void database_api_impl::set_subscribe_callback( std::function cb, bool clear_filter ) +void database_api_impl::set_subscribe_callback( std::function cb, bool notify_remove_create ) { - edump((clear_filter)); + //edump((clear_filter)); _subscribe_callback = cb; - if( clear_filter || !cb ) - { - static fc::bloom_parameters param; - param.projected_element_count = 10000; - param.false_positive_probability = 1.0/10000; - param.maximum_size = 1024*8*8*2; - param.compute_optimal_parameters(); - _subscribe_filter = fc::bloom_filter(param); - } + _notify_remove_create = notify_remove_create; + _subscribed_accounts.clear(); + + static fc::bloom_parameters param; + param.projected_element_count = 10000; + param.false_positive_probability = 1.0/100; + param.maximum_size = 1024*8*8*2; + param.compute_optimal_parameters(); + _subscribe_filter = fc::bloom_filter(param); } void database_api::set_pending_transaction_callback( std::function cb ) @@ -339,6 +382,20 @@ optional database_api_impl::get_block_header(uint32_t block_num) c return *result; return {}; } +map> database_api::get_block_header_batch(const vector block_nums)const +{ + return my->get_block_header_batch( block_nums ); +} + +map> database_api_impl::get_block_header_batch(const vector block_nums) const +{ + map> results; + for (const uint32_t block_num : block_nums) + { + results[block_num] = get_block_header(block_num); + } + return results; +} optional database_api::get_block(uint32_t block_num)const { @@ -428,6 +485,17 @@ dynamic_global_property_object database_api_impl::get_dynamic_global_properties( return _db.get(dynamic_global_property_id_type()); } +global_betting_statistics_object database_api::get_global_betting_statistics() const +{ + return my->get_global_betting_statistics(); +} + +global_betting_statistics_object database_api_impl::get_global_betting_statistics() const +{ + return _db.get(global_betting_statistics_id_type()); +} + + ////////////////////////////////////////////////////////////////////// // // // Keys // @@ -498,6 +566,35 @@ vector> database_api_impl::get_key_references( vectoris_public_key_registered(public_key); +} + +bool database_api_impl::is_public_key_registered(string public_key) const +{ + // Short-circuit + if (public_key.empty()) { + return false; + } + + // Search among all keys using an existing map of *current* account keys + public_key_type key; + try { + key = public_key_type(public_key); + } catch ( ... ) { + // An invalid public key was detected + return false; + } + const auto& idx = _db.get_index_type(); + const auto& aidx = dynamic_cast&>(idx); + const auto& refs = aidx.get_secondary_index(); + auto itr = refs.account_to_key_memberships.find(key); + bool is_known = itr != refs.account_to_key_memberships.end(); + + return is_known; +} + ////////////////////////////////////////////////////////////////////// // // // Accounts // @@ -551,7 +648,8 @@ std::map database_api_impl::get_full_accounts( const if( subscribe ) { - ilog( "subscribe to ${id}", ("id",account->name) ); + FC_ASSERT( std::distance(_subscribed_accounts.begin(), _subscribed_accounts.end()) <= 100 ); + _subscribed_accounts.insert( account->get_id() ); subscribe_to_item( account->id ); } @@ -613,6 +711,25 @@ std::map database_api_impl::get_full_accounts( const [&acnt] (const call_order_object& call) { acnt.call_orders.emplace_back(call); }); + auto settle_range = _db.get_index_type().indices().get().equal_range(account->id); + std::for_each(settle_range.first, settle_range.second, + [&acnt] (const force_settlement_object& settle) { + acnt.settle_orders.emplace_back(settle); + }); + + // get assets issued by user + auto asset_range = _db.get_index_type().indices().get().equal_range(account->id); + std::for_each(asset_range.first, asset_range.second, + [&acnt] (const asset_object& asset) { + acnt.assets.emplace_back(asset.id); + }); + + // get withdraws permissions + auto withdraw_range = _db.get_index_type().indices().get().equal_range(account->id); + std::for_each(withdraw_range.first, withdraw_range.second, + [&acnt] (const withdraw_permission_object& withdraw) { + acnt.withdraws.emplace_back(withdraw); + }); auto pending_payouts_range = _db.get_index_type().indices().get().equal_range(boost::make_tuple(account->id)); @@ -895,6 +1012,86 @@ vector> database_api_impl::lookup_asset_symbols(const vec return result; } +////////////////////////////////////////////////////////////////////// +// Peerplays // +////////////////////////////////////////////////////////////////////// +vector database_api::list_sports() const +{ + return my->list_sports(); +} + +vector database_api_impl::list_sports() const +{ + const auto& sport_object_idx = _db.get_index_type().indices().get(); + return boost::copy_range >(sport_object_idx); +} + +vector database_api::list_event_groups(sport_id_type sport_id) const +{ + return my->list_event_groups(sport_id); +} + +vector database_api_impl::list_event_groups(sport_id_type sport_id) const +{ + const auto& event_group_idx = _db.get_index_type().indices().get(); + return boost::copy_range >(event_group_idx.equal_range(sport_id)); +} + +vector database_api::list_events_in_group(event_group_id_type event_group_id) const +{ + return my->list_events_in_group(event_group_id); +} + +vector database_api_impl::list_events_in_group(event_group_id_type event_group_id) const +{ + const auto& event_idx = _db.get_index_type().indices().get(); + return boost::copy_range >(event_idx.equal_range(event_group_id)); +} + +vector database_api::list_betting_market_groups(event_id_type event_id) const +{ + return my->list_betting_market_groups(event_id); +} + +vector database_api_impl::list_betting_market_groups(event_id_type event_id) const +{ + const auto& betting_market_group_idx = _db.get_index_type().indices().get(); + return boost::copy_range >(betting_market_group_idx.equal_range(event_id)); +} + +vector database_api::list_betting_markets(betting_market_group_id_type betting_market_group_id) const +{ + return my->list_betting_markets(betting_market_group_id); +} + +vector database_api_impl::list_betting_markets(betting_market_group_id_type betting_market_group_id) const +{ + const auto& betting_market_idx = _db.get_index_type().indices().get(); + return boost::copy_range >(betting_market_idx.equal_range(betting_market_group_id)); +} + +vector database_api::get_unmatched_bets_for_bettor(betting_market_id_type betting_market_id, account_id_type bettor_id) const +{ + return my->get_unmatched_bets_for_bettor(betting_market_id, bettor_id); +} + +vector database_api_impl::get_unmatched_bets_for_bettor(betting_market_id_type betting_market_id, account_id_type bettor_id) const +{ + const auto& bet_idx = _db.get_index_type().indices().get(); + return boost::copy_range >(bet_idx.equal_range(std::make_tuple(bettor_id, betting_market_id))); +} + +vector database_api::get_all_unmatched_bets_for_bettor(account_id_type bettor_id) const +{ + return my->get_all_unmatched_bets_for_bettor(bettor_id); +} + +vector database_api_impl::get_all_unmatched_bets_for_bettor(account_id_type bettor_id) const +{ + const auto& bet_idx = _db.get_index_type().indices().get(); + return boost::copy_range >(bet_idx.equal_range(std::make_tuple(bettor_id))); +} + ////////////////////////////////////////////////////////////////////// // // // Markets / feeds // @@ -1015,117 +1212,84 @@ void database_api_impl::unsubscribe_from_market(asset_id_type a, asset_id_type b market_ticker database_api::get_ticker( const string& base, const string& quote )const { - return my->get_ticker( base, quote ); + return my->get_ticker( base, quote ); } market_ticker database_api_impl::get_ticker( const string& base, const string& quote )const { - auto assets = lookup_asset_symbols( {base, quote} ); - FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); - FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); + const auto assets = lookup_asset_symbols( {base, quote} ); + FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); + FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); - auto base_id = assets[0]->id; - auto quote_id = assets[1]->id; + market_ticker result; + result.base = base; + result.quote = quote; + result.latest = 0; + result.lowest_ask = 0; + result.highest_bid = 0; + result.percent_change = 0; + result.base_volume = 0; + result.quote_volume = 0; - market_ticker result; + try { + const fc::time_point_sec now = fc::time_point::now(); + const fc::time_point_sec yesterday = fc::time_point_sec( now.sec_since_epoch() - 86400 ); + const auto batch_size = 100; - result.base = base; - result.quote = quote; - result.base_volume = 0; - result.quote_volume = 0; - result.percent_change = 0; - result.lowest_ask = 0; - result.highest_bid = 0; + vector trades = get_trade_history( base, quote, now, yesterday, batch_size ); + if( !trades.empty() ) + { + result.latest = trades[0].price; - auto price_to_real = [&]( const share_type a, int p ) { return double( a.value ) / pow( 10, p ); }; + while( !trades.empty() ) + { + for( const market_trade& t: trades ) + { + result.base_volume += t.value; + result.quote_volume += t.amount; + } - try { - if( base_id > quote_id ) std::swap(base_id, quote_id); + trades = get_trade_history( base, quote, trades.back().date, yesterday, batch_size ); + } - uint32_t day = 86400; - auto now = fc::time_point_sec( fc::time_point::now() ); - auto orders = get_order_book( base, quote, 1 ); - auto trades = get_trade_history( base, quote, now, fc::time_point_sec( now.sec_since_epoch() - day ), 100 ); + const auto last_trade_yesterday = get_trade_history( base, quote, yesterday, fc::time_point_sec(), 1 ); + if( !last_trade_yesterday.empty() ) + { + const auto price_yesterday = last_trade_yesterday[0].price; + result.percent_change = ( (result.latest / price_yesterday) - 1 ) * 100; + } + } + else + { + const auto last_trade = get_trade_history( base, quote, now, fc::time_point_sec(), 1 ); + if( !last_trade.empty() ) + result.latest = last_trade[0].price; + } - result.latest = trades[0].price; + const auto orders = get_order_book( base, quote, 1 ); + if( !orders.asks.empty() ) result.lowest_ask = orders.asks[0].price; + if( !orders.bids.empty() ) result.highest_bid = orders.bids[0].price; + } FC_CAPTURE_AND_RETHROW( (base)(quote) ) - for ( market_trade t: trades ) - { - result.base_volume += t.value; - result.quote_volume += t.amount; - } - - while (trades.size() == 100) - { - trades = get_trade_history( base, quote, trades[99].date, fc::time_point_sec( now.sec_since_epoch() - day ), 100 ); - - for ( market_trade t: trades ) - { - result.base_volume += t.value; - result.quote_volume += t.amount; - } - } - - trades = get_trade_history( base, quote, trades.back().date, fc::time_point_sec(), 1 ); - result.percent_change = trades.size() > 0 ? ( ( result.latest / trades.back().price ) - 1 ) * 100 : 0; - - //if (assets[0]->id == base_id) - { - result.lowest_ask = orders.asks[0].price; - result.highest_bid = orders.bids[0].price; - } - - return result; - } FC_CAPTURE_AND_RETHROW( (base)(quote) ) + return result; } market_volume database_api::get_24_volume( const string& base, const string& quote )const { - return my->get_24_volume( base, quote ); + return my->get_24_volume( base, quote ); } market_volume database_api_impl::get_24_volume( const string& base, const string& quote )const { - auto assets = lookup_asset_symbols( {base, quote} ); - FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); - FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); + const auto ticker = get_ticker( base, quote ); - auto base_id = assets[0]->id; - auto quote_id = assets[1]->id; + market_volume result; + result.base = ticker.base; + result.quote = ticker.quote; + result.base_volume = ticker.base_volume; + result.quote_volume = ticker.quote_volume; - market_volume result; - result.base = base; - result.quote = quote; - result.base_volume = 0; - result.quote_volume = 0; - - try { - if( base_id > quote_id ) std::swap(base_id, quote_id); - - uint32_t bucket_size = 86400; - auto now = fc::time_point_sec( fc::time_point::now() ); - - auto trades = get_trade_history( base, quote, now, fc::time_point_sec( now.sec_since_epoch() - bucket_size ), 100 ); - - for ( market_trade t: trades ) - { - result.base_volume += t.value; - result.quote_volume += t.amount; - } - - while (trades.size() == 100) - { - trades = get_trade_history( base, quote, trades[99].date, fc::time_point_sec( now.sec_since_epoch() - bucket_size ), 100 ); - - for ( market_trade t: trades ) - { - result.base_volume += t.value; - result.quote_volume += t.amount; - } - } - - return result; - } FC_CAPTURE_AND_RETHROW( (base)(quote) ) + return result; } order_book database_api::get_order_book( const string& base, const string& quote, unsigned limit )const @@ -1175,7 +1339,7 @@ order_book database_api_impl::get_order_book( const string& base, const string& order ord; ord.price = price_to_real( o.sell_price ); ord.quote = asset_to_real( o.for_sale, assets[1]->precision ); - ord.base = asset_to_real( share_type( ( uint64_t( o.for_sale.value ) * o.sell_price.quote.amount.value ) / o.sell_price.base.amount.value ), assets[0]->precision ); + ord.base = asset_to_real( share_type( ( uint128_t( o.for_sale.value ) * o.sell_price.quote.amount.value ) / o.sell_price.base.amount.value ), assets[0]->precision ); result.asks.push_back( ord ); } } @@ -1865,109 +2029,113 @@ vector database_api_impl::get_registered_tournaments(account void database_api_impl::broadcast_updates( const vector& updates ) { - if( updates.size() ) { + if( updates.size() && _subscribe_callback ) { auto capture_this = shared_from_this(); fc::async([capture_this,updates](){ - capture_this->_subscribe_callback( fc::variant(updates) ); + if(capture_this->_subscribe_callback) + capture_this->_subscribe_callback( fc::variant(updates) ); }); } } -void database_api_impl::on_objects_removed( const vector& objs ) +void database_api_impl::broadcast_market_updates( const market_queue_type& queue) +{ + if( queue.size() ) + { + auto capture_this = shared_from_this(); + fc::async([capture_this, this, queue](){ + for( const auto& item : queue ) + { + auto sub = _market_subscriptions.find(item.first); + if( sub != _market_subscriptions.end() ) + sub->second( fc::variant(item.second ) ); + } + }); + } +} + +void database_api_impl::on_objects_removed( const vector& ids, const vector& objs, const flat_set& impacted_accounts) +{ + handle_object_changed(_notify_remove_create, false, ids, impacted_accounts, + [objs](object_id_type id) -> const object* { + auto it = std::find_if( + objs.begin(), objs.end(), + [id](const object* o) {return o != nullptr && o->id == id;}); + + if (it != objs.end()) + return *it; + + return nullptr; + } + ); +} + +void database_api_impl::on_objects_new(const vector& ids, const flat_set& impacted_accounts) +{ + handle_object_changed(_notify_remove_create, true, ids, impacted_accounts, + std::bind(&object_database::find_object, &_db, std::placeholders::_1) + ); +} + +void database_api_impl::on_objects_changed(const vector& ids, const flat_set& impacted_accounts) +{ + handle_object_changed(false, true, ids, impacted_accounts, + std::bind(&object_database::find_object, &_db, std::placeholders::_1) + ); +} + +void database_api_impl::handle_object_changed(bool force_notify, bool full_object, const vector& ids, const flat_set& impacted_accounts, std::function find_object) { - /// we need to ensure the database_api is not deleted for the life of the async operation if( _subscribe_callback ) { - vector updates; - updates.reserve(objs.size()); + vector updates; - for( auto obj : objs ) - updates.emplace_back( obj->id ); - broadcast_updates( updates ); + for(auto id : ids) + { + if( force_notify || is_subscribed_to_item(id) || is_impacted_account(impacted_accounts) ) + { + if( full_object ) + { + auto obj = find_object(id); + if( obj ) + { + updates.emplace_back( obj->to_variant() ); + } + } + else + { + updates.emplace_back( id ); + } + } + } + + broadcast_updates(updates); } if( _market_subscriptions.size() ) { - map< pair, vector > broadcast_queue; - for( const auto& obj : objs ) - { - const limit_order_object* order = dynamic_cast(obj); - if( order ) - { - auto sub = _market_subscriptions.find( order->get_market() ); - if( sub != _market_subscriptions.end() ) - broadcast_queue[order->get_market()].emplace_back( order->id ); - } - } - if( broadcast_queue.size() ) - { - auto capture_this = shared_from_this(); - fc::async([capture_this,this,broadcast_queue](){ - for( const auto& item : broadcast_queue ) - { - auto sub = _market_subscriptions.find(item.first); - if( sub != _market_subscriptions.end() ) - sub->second( fc::variant(item.second ) ); - } - }); - } - } -} - -void database_api_impl::on_objects_changed(const vector& ids) -{ - vector updates; - map< pair, vector > market_broadcast_queue; - - for(auto id : ids) - { - const object* obj = nullptr; - if( _subscribe_callback ) - { - obj = _db.find_object( id ); - if( obj ) - { - updates.emplace_back( obj->to_variant() ); - } - else - { - updates.emplace_back(id); // send just the id to indicate removal - } - } - - if( _market_subscriptions.size() ) - { - if( !_subscribe_callback ) - obj = _db.find_object( id ); - if( obj ) - { - const limit_order_object* order = dynamic_cast(obj); - if( order ) - { - auto sub = _market_subscriptions.find( order->get_market() ); - if( sub != _market_subscriptions.end() ) - market_broadcast_queue[order->get_market()].emplace_back( order->id ); - } - } - } - } - - auto capture_this = shared_from_this(); - + market_queue_type broadcast_queue; /// pushing the future back / popping the prior future if it is complete. /// if a connection hangs then this could get backed up and result in /// a failure to exit cleanly. - fc::async([capture_this,this,updates,market_broadcast_queue](){ - if( _subscribe_callback ) - _subscribe_callback( updates ); + //fc::async([capture_this,this,updates,market_broadcast_queue](){ + //if( _subscribe_callback ) + // _subscribe_callback( updates ); - for( const auto& item : market_broadcast_queue ) + for(auto id : ids) { - auto sub = _market_subscriptions.find(item.first); - if( sub != _market_subscriptions.end() ) - sub->second( fc::variant(item.second ) ); + if( id.is() ) + { + enqueue_if_subscribed_to_market( find_object(id), broadcast_queue, full_object ); + } + else if( id.is() ) + { + enqueue_if_subscribed_to_market( find_object(id), broadcast_queue, full_object ); + } } - }); + + broadcast_market_updates(broadcast_queue); + } } /** note: this method cannot yield because it is called in the middle of diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index 55613d27..c765de39 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -208,6 +208,48 @@ struct get_impacted_account_visitor { _impacted.insert( op.account_id ); } + + void operator()( const sport_create_operation& op ) {} + void operator()( const sport_update_operation& op ) {} + void operator()( const event_group_create_operation& op ) {} + void operator()( const event_group_update_operation& op ) {} + void operator()( const event_create_operation& op ) {} + void operator()( const event_update_operation& op ) {} + void operator()( const event_update_status_operation& op ) {} + void operator()( const betting_market_rules_create_operation& op ) {} + void operator()( const betting_market_rules_update_operation& op ) {} + void operator()( const betting_market_group_create_operation& op ) {} + void operator()( const betting_market_group_update_operation& op ) {} + void operator()( const betting_market_create_operation& op ) {} + void operator()( const betting_market_update_operation& op ) {} + void operator()( const betting_market_group_resolve_operation& op ) {} + void operator()( const betting_market_group_cancel_unmatched_bets_operation& op ) {} + + void operator()( const bet_place_operation& op ) + { + _impacted.insert( op.bettor_id ); + } + void operator()( const bet_cancel_operation& op ) + { + _impacted.insert( op.bettor_id ); + } + void operator()( const bet_canceled_operation& op ) + { + _impacted.insert( op.bettor_id ); + } + void operator()( const bet_adjusted_operation& op ) + { + _impacted.insert( op.bettor_id ); + } + void operator()( const bet_matched_operation& op ) + { + _impacted.insert( op.bettor_id ); + } + void operator()( const betting_market_group_resolved_operation& op ) + { + _impacted.insert( op.bettor_id ); + } + void operator()( const tournament_create_operation& op ) { _impacted.insert( op.creator ); diff --git a/libraries/app/include/graphene/app/api.hpp b/libraries/app/include/graphene/app/api.hpp index eef2b6d9..d4532b42 100644 --- a/libraries/app/include/graphene/app/api.hpp +++ b/libraries/app/include/graphene/app/api.hpp @@ -29,8 +29,10 @@ #include #include +#include #include +#include #include @@ -49,6 +51,7 @@ namespace graphene { namespace app { using namespace graphene::chain; using namespace graphene::market_history; + using namespace graphene::accounts_list; using namespace fc::ecc; using namespace std; @@ -71,6 +74,18 @@ namespace graphene { namespace app { string message_out; }; + struct account_asset_balance + { + string name; + account_id_type account_id; + share_type amount; + }; + struct asset_holders + { + asset_id_type asset_id; + int count; + }; + /** * @brief The history_api class implements the RPC API for account history * @@ -93,6 +108,22 @@ namespace graphene { namespace app { operation_history_id_type stop = operation_history_id_type(), unsigned limit = 100, operation_history_id_type start = operation_history_id_type())const; + + /** + * @brief Get only asked operations relevant to the specified account + * @param account The account whose history should be queried + * @param operation_id The ID of the operation we want to get operations in the account( 0 = transfer , 1 = limit order create, ...) + * @param stop ID of the earliest operation to retrieve + * @param limit Maximum number of operations to retrieve (must not exceed 100) + * @param start ID of the most recent operation to retrieve + * @return A list of operations performed by account, ordered from most recent to oldest. + */ + vector get_account_history_operations(account_id_type account, + int operation_id, + operation_history_id_type start = operation_history_id_type(), + operation_history_id_type stop = operation_history_id_type(), + unsigned limit = 100)const; + /** * @breif Get operations relevant to the specified account referenced * by an event numbering specific to the account. The current number of operations @@ -113,11 +144,28 @@ namespace graphene { namespace app { vector get_fill_order_history( asset_id_type a, asset_id_type b, uint32_t limit )const; vector get_market_history( asset_id_type a, asset_id_type b, uint32_t bucket_seconds, fc::time_point_sec start, fc::time_point_sec end )const; + vector list_core_accounts()const; flat_set get_market_history_buckets()const; private: application& _app; }; + /** + * @brief Block api + */ + class block_api + { + public: + block_api(graphene::chain::database& db); + ~block_api(); + + vector> get_blocks(uint32_t block_num_from, uint32_t block_num_to)const; + + private: + graphene::chain::database& _db; + }; + + /** * @brief The network_broadcast_api class allows broadcasting of transactions. */ @@ -151,6 +199,12 @@ namespace graphene { namespace app { */ void broadcast_transaction_with_callback( confirmation_callback cb, const signed_transaction& trx); + /** this version of broadcast transaction registers a callback method that will be called when the transaction is + * included into a block. The callback method includes the transaction id, block number, and transaction number in the + * block. + */ + fc::variant broadcast_transaction_synchronous(const signed_transaction& trx); + void broadcast_block( const signed_block& block ); /** @@ -252,6 +306,23 @@ namespace graphene { namespace app { range_proof_info range_get_info( const std::vector& proof ); }; + /** + * @brief + */ + class asset_api + { + public: + asset_api(graphene::chain::database& db); + ~asset_api(); + + vector get_asset_holders( asset_id_type asset_id, uint32_t start, uint32_t limit )const; + int get_asset_holders_count( asset_id_type asset_id )const; + vector get_all_asset_holders() const; + + private: + graphene::chain::database& _db; + }; + /** * @brief The login_api class implements the bottom layer of the RPC API * @@ -273,6 +344,8 @@ namespace graphene { namespace app { * has sucessfully authenticated. */ bool login(const string& user, const string& password); + /// @brief Retrieve the network block API + fc::api block()const; /// @brief Retrieve the network broadcast API fc::api network_broadcast()const; /// @brief Retrieve the database API @@ -283,20 +356,27 @@ namespace graphene { namespace app { fc::api network_node()const; /// @brief Retrieve the cryptography API fc::api crypto()const; + /// @brief Retrieve the asset API + fc::api asset()const; /// @brief Retrieve the debug API (if available) fc::api debug()const; + /// @brief Retrieve the bookie API (if available) + fc::api bookie()const; - private: /// @brief Called to enable an API, not reflected. void enable_api( const string& api_name ); + private: application& _app; + optional< fc::api > _block_api; optional< fc::api > _database_api; optional< fc::api > _network_broadcast_api; optional< fc::api > _network_node_api; optional< fc::api > _history_api; optional< fc::api > _crypto_api; + optional< fc::api > _asset_api; optional< fc::api > _debug_api; + optional< fc::api > _bookie_api; }; }} // graphene::app @@ -310,16 +390,25 @@ FC_REFLECT( graphene::app::verify_range_proof_rewind_result, //FC_REFLECT_TYPENAME( fc::ecc::compact_signature ); //FC_REFLECT_TYPENAME( fc::ecc::commitment_type ); +FC_REFLECT( graphene::app::account_asset_balance, (name)(account_id)(amount) ); +FC_REFLECT( graphene::app::asset_holders, (asset_id)(count) ); + FC_API(graphene::app::history_api, (get_account_history) + (get_account_history_operations) (get_relative_account_history) (get_fill_order_history) (get_market_history) (get_market_history_buckets) + (list_core_accounts) + ) +FC_API(graphene::app::block_api, + (get_blocks) ) FC_API(graphene::app::network_broadcast_api, (broadcast_transaction) (broadcast_transaction_with_callback) + (broadcast_transaction_synchronous) (broadcast_block) ) FC_API(graphene::app::network_node_api, @@ -341,12 +430,20 @@ FC_API(graphene::app::crypto_api, (verify_range_proof_rewind) (range_get_info) ) +FC_API(graphene::app::asset_api, + (get_asset_holders) + (get_asset_holders_count) + (get_all_asset_holders) + ) FC_API(graphene::app::login_api, (login) + (block) (network_broadcast) (database) (history) (network_node) (crypto) + (asset) (debug) + (bookie) ) diff --git a/libraries/app/include/graphene/app/database_api.hpp b/libraries/app/include/graphene/app/database_api.hpp index bb8c15cf..7b0943e4 100644 --- a/libraries/app/include/graphene/app/database_api.hpp +++ b/libraries/app/include/graphene/app/database_api.hpp @@ -38,6 +38,12 @@ #include #include #include +#include +#include +#include +#include +#include + #include #include #include @@ -159,6 +165,14 @@ class database_api */ optional get_block_header(uint32_t block_num)const; + /** + * @brief Retrieve multiple block header by block numbers + * @param block_num vector containing heights of the block whose header should be returned + * @return array of headers of the referenced blocks, or null if no matching block was found + */ + map> get_block_header_batch(const vector block_nums)const; + + /** * @brief Retrieve a full, signed block * @param block_num Height of the block to be returned @@ -213,6 +227,15 @@ class database_api vector> get_key_references( vector key )const; + /** + * Determine whether a textual representation of a public key + * (in Base-58 format) is *currently* linked + * to any *registered* (i.e. non-stealth) account on the blockchain + * @param public_key Public key + * @return Whether a public key is known + */ + bool is_public_key_registered(string public_key) const; + ////////////// // Accounts // ////////////// @@ -320,6 +343,50 @@ class database_api */ vector> lookup_asset_symbols(const vector& symbols_or_ids)const; + ///////////////////// + // Peerplays // + ///////////////////// + + /** + * @brief Get global betting statistics + */ + global_betting_statistics_object get_global_betting_statistics() const; + + /** + * @brief Get a list of all sports + */ + vector list_sports() const; + + /** + * @brief Return a list of all event groups for a sport (e.g. all soccer leagues in soccer) + */ + vector list_event_groups(sport_id_type sport_id) const; + + /** + * @brief Return a list of all events in an event group + */ + vector list_events_in_group(event_group_id_type event_group_id) const; + + /** + * @brief Return a list of all betting market groups for an event + */ + vector list_betting_market_groups(event_id_type) const; + + /** + * @brief Return a list of all betting markets for a betting market group + */ + vector list_betting_markets(betting_market_group_id_type) const; + + /** + * @brief Return a list of all unmatched bets for a given account on a specific betting market + */ + vector get_unmatched_bets_for_bettor(betting_market_id_type, account_id_type) const; + + /** + * @brief Return a list of all unmatched bets for a given account (includes bets on all markets) + */ + vector get_all_unmatched_bets_for_bettor(account_id_type) const; + ///////////////////// // Markets / feeds // ///////////////////// @@ -602,6 +669,7 @@ FC_API(graphene::app::database_api, // Blocks and transactions (get_block_header) + (get_block_header_batch) (get_block) (get_transaction) (get_recent_transaction_by_id) @@ -615,6 +683,7 @@ FC_API(graphene::app::database_api, // Keys (get_key_references) + (is_public_key_registered) // Accounts (get_accounts) @@ -637,6 +706,16 @@ FC_API(graphene::app::database_api, (list_assets) (lookup_asset_symbols) + // Peerplays + (list_sports) + (get_global_betting_statistics) + (list_event_groups) + (list_events_in_group) + (list_betting_market_groups) + (list_betting_markets) + (get_unmatched_bets_for_bettor) + (get_all_unmatched_bets_for_bettor) + // Markets / feeds (get_order_book) (get_limit_orders) diff --git a/libraries/app/include/graphene/app/full_account.hpp b/libraries/app/include/graphene/app/full_account.hpp index 0d94348f..955857b7 100644 --- a/libraries/app/include/graphene/app/full_account.hpp +++ b/libraries/app/include/graphene/app/full_account.hpp @@ -26,6 +26,7 @@ #include #include #include +#include namespace graphene { namespace app { using namespace graphene::chain; @@ -43,13 +44,17 @@ namespace graphene { namespace app { vector vesting_balances; vector limit_orders; vector call_orders; + vector settle_orders; vector proposals; + vector assets; + vector withdraws; +// vector pending_dividend_payments; vector pending_dividend_payments; }; } } -FC_REFLECT( graphene::app::full_account, +FC_REFLECT( graphene::app::full_account, (account) (statistics) (registrar_name) @@ -61,6 +66,10 @@ FC_REFLECT( graphene::app::full_account, (vesting_balances) (limit_orders) (call_orders) + (settle_orders) + (proposals) + (assets) + (withdraws) (proposals) (pending_dividend_payments) ) diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index e56d0126..399720fa 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -6,10 +6,12 @@ set_source_files_properties( "${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain add_dependencies( build_hardfork_hpp cat-parts ) file(GLOB HEADERS "include/graphene/chain/*.hpp") +file(GLOB PROTOCOL_HEADERS "include/graphene/chain/protocol/*.hpp") if( GRAPHENE_DISABLE_UNITY_BUILD ) set( GRAPHENE_DB_FILES db_balance.cpp + db_bet.cpp db_block.cpp db_debug.cpp db_getter.cpp @@ -94,7 +96,20 @@ add_library( graphene_chain is_authorized_asset.cpp + protocol/sport.cpp + sport_evaluator.cpp + protocol/event_group.cpp + event_group_evaluator.cpp + protocol/event.cpp + event_evaluator.cpp + event_object.cpp + protocol/betting_market.cpp + betting_market_evaluator.cpp + betting_market_object.cpp + betting_market_group_object.cpp + ${HEADERS} + ${PROTOCOL_HEADERS} "${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain/hardfork.hpp" ) @@ -114,3 +129,5 @@ INSTALL( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +INSTALL( FILES ${HEADERS} DESTINATION "include/graphene/chain" ) +INSTALL( FILES ${PROTOCOL_HEADERS} DESTINATION "include/graphene/chain/protocol" ) diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp new file mode 100644 index 00000000..7918ac32 --- /dev/null +++ b/libraries/chain/betting_market_evaluator.cpp @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#define DEFAULT_LOGGER "betting" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + +void_result betting_market_rules_create_evaluator::do_evaluate(const betting_market_rules_create_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +object_id_type betting_market_rules_create_evaluator::do_apply(const betting_market_rules_create_operation& op) +{ try { + const betting_market_rules_object& new_betting_market_rules = + db().create([&](betting_market_rules_object& betting_market_rules_obj) { + betting_market_rules_obj.name = op.name; + betting_market_rules_obj.description = op.description; + }); + return new_betting_market_rules.id; +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_rules_update_evaluator::do_evaluate(const betting_market_rules_update_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + _rules = &op.betting_market_rules_id(db()); + FC_ASSERT(op.new_name.valid() || op.new_description.valid(), "nothing to update"); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_rules_update_evaluator::do_apply(const betting_market_rules_update_operation& op) +{ try { + db().modify(*_rules, [&](betting_market_rules_object& betting_market_rules) { + if (op.new_name.valid()) + betting_market_rules.name = *op.new_name; + if (op.new_description.valid()) + betting_market_rules.description = *op.new_description; + }); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_group_create_evaluator::do_evaluate(const betting_market_group_create_operation& op) +{ try { + database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + + // the event_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly an event + object_id_type resolved_event_id = op.event_id; + if (is_relative(op.event_id)) + resolved_event_id = get_relative_id(op.event_id); + + FC_ASSERT(resolved_event_id.space() == event_id_type::space_id && + resolved_event_id.type() == event_id_type::type_id, + "event_id must refer to a event_id_type"); + _event_id = resolved_event_id; + FC_ASSERT(d.find_object(_event_id), "Invalid event specified"); + + FC_ASSERT(d.find_object(op.asset_id), "Invalid asset specified"); + + // the rules_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly rules + object_id_type resolved_rules_id = op.rules_id; + if (is_relative(op.rules_id)) + resolved_rules_id = get_relative_id(op.rules_id); + + FC_ASSERT(resolved_rules_id.space() == betting_market_rules_id_type::space_id && + resolved_rules_id.type() == betting_market_rules_id_type::type_id, + "rules_id must refer to a betting_market_rules_id_type"); + _rules_id = resolved_rules_id; + FC_ASSERT(d.find_object(_rules_id), "Invalid rules specified"); + return void_result(); +} FC_CAPTURE_AND_RETHROW((op)) } + +object_id_type betting_market_group_create_evaluator::do_apply(const betting_market_group_create_operation& op) +{ try { + const betting_market_group_object& new_betting_market_group = + db().create([&](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.event_id = _event_id; + betting_market_group_obj.rules_id = _rules_id; + betting_market_group_obj.description = op.description; + betting_market_group_obj.asset_id = op.asset_id; + betting_market_group_obj.never_in_play = op.never_in_play; + betting_market_group_obj.delay_before_settling = op.delay_before_settling; + }); + return new_betting_market_group.id; +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_group_update_evaluator::do_evaluate(const betting_market_group_update_operation& op) +{ try { + database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + _betting_market_group = &op.betting_market_group_id(d); + + FC_ASSERT(op.new_description || op.new_rules_id || op.status, "nothing to change"); + + if (op.new_rules_id) + { + // the rules_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly rules + object_id_type resolved_rules_id = *op.new_rules_id; + if (is_relative(*op.new_rules_id)) + resolved_rules_id = get_relative_id(*op.new_rules_id); + + FC_ASSERT(resolved_rules_id.space() == betting_market_rules_id_type::space_id && + resolved_rules_id.type() == betting_market_rules_id_type::type_id, + "rules_id must refer to a betting_market_rules_id_type"); + _rules_id = resolved_rules_id; + FC_ASSERT(d.find_object(_rules_id), "invalid rules specified"); + } + + if (op.status) + FC_ASSERT(_betting_market_group->get_status() != *op.status, "status would not change the state of the betting market group"); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_group_update_evaluator::do_apply(const betting_market_group_update_operation& op) +{ try { + database& d = db(); + d.modify(*_betting_market_group, [&](betting_market_group_object& betting_market_group) { + if (op.new_description) + betting_market_group.description = *op.new_description; + if (op.new_rules_id) + betting_market_group.rules_id = _rules_id; + + bool bets_were_delayed = betting_market_group.bets_are_delayed(); + if (op.status) + betting_market_group.dispatch_new_status(d, *op.status); + + bool bets_are_delayed = betting_market_group.bets_are_delayed(); + + // if we have transitioned from in-play to not-in-play-but-still-accepting-bets, + // place all delayed bets now + if (betting_market_group.bets_are_allowed() && + bets_were_delayed && !bets_are_delayed) + { + const auto& bet_odds_idx = d.get_index_type().indices().get(); + auto bet_iter = bet_odds_idx.begin(); + bool last = bet_iter == bet_odds_idx.end() || !bet_iter->end_of_delay; + while (!last) + { + const bet_object& delayed_bet = *bet_iter; + ++bet_iter; + last = bet_iter == bet_odds_idx.end() || !bet_iter->end_of_delay; + + const betting_market_object& betting_market = delayed_bet.betting_market_id(d); + if (betting_market.group_id == op.betting_market_group_id) + { + d.modify(delayed_bet, [](bet_object& bet_obj) { + // clear the end_of_delay, which will re-sort the bet into its place in the book + bet_obj.end_of_delay.reset(); + }); + + d.place_bet(delayed_bet); + } + } + } + }); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_create_evaluator::do_evaluate(const betting_market_create_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + + // the betting_market_group_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly an betting_market_group + object_id_type resolved_betting_market_group_id = op.group_id; + if (is_relative(op.group_id)) + resolved_betting_market_group_id = get_relative_id(op.group_id); + + FC_ASSERT(resolved_betting_market_group_id.space() == betting_market_group_id_type::space_id && + resolved_betting_market_group_id.type() == betting_market_group_id_type::type_id, + "betting_market_group_id must refer to a betting_market_group_id_type"); + _group_id = resolved_betting_market_group_id; + FC_ASSERT(db().find_object(_group_id), "Invalid betting_market_group specified"); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +object_id_type betting_market_create_evaluator::do_apply(const betting_market_create_operation& op) +{ try { + const betting_market_object& new_betting_market = + db().create([&](betting_market_object& betting_market_obj) { + betting_market_obj.group_id = _group_id; + betting_market_obj.description = op.description; + betting_market_obj.payout_condition = op.payout_condition; + }); + return new_betting_market.id; +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_update_evaluator::do_evaluate(const betting_market_update_operation& op) +{ try { + database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + _betting_market = &op.betting_market_id(d); + FC_ASSERT(op.new_group_id.valid() || op.new_description.valid() || op.new_payout_condition.valid(), "nothing to change"); + + if (op.new_group_id.valid()) + { + // the betting_market_group_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly an betting_market_group + object_id_type resolved_betting_market_group_id = *op.new_group_id; + if (is_relative(*op.new_group_id)) + resolved_betting_market_group_id = get_relative_id(*op.new_group_id); + + FC_ASSERT(resolved_betting_market_group_id.space() == betting_market_group_id_type::space_id && + resolved_betting_market_group_id.type() == betting_market_group_id_type::type_id, + "betting_market_group_id must refer to a betting_market_group_id_type"); + _group_id = resolved_betting_market_group_id; + FC_ASSERT(d.find_object(_group_id), "invalid betting_market_group specified"); + } + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_update_evaluator::do_apply(const betting_market_update_operation& op) +{ try { + db().modify(*_betting_market, [&](betting_market_object& betting_market) { + if (op.new_group_id.valid()) + betting_market.group_id = _group_id; + if (op.new_payout_condition.valid()) + betting_market.payout_condition = *op.new_payout_condition; + if (op.new_description.valid()) + betting_market.description = *op.new_description; + }); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) +{ try { + const database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); + _betting_market = &op.betting_market_id(d); + _betting_market_group = &_betting_market->group_id(d); + + FC_ASSERT( op.amount_to_bet.asset_id == _betting_market_group->asset_id, + "Asset type bet does not match the market's asset type" ); + + ddump((_betting_market_group->get_status())); + FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::frozen, + "Unable to place bets while the market is frozen" ); + FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::closed, + "Unable to place bets while the market is closed" ); + FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::graded, + "Unable to place bets while the market is graded" ); + FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::re_grading, + "Unable to place bets while the market is re-grading" ); + FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::settled, + "Unable to place bets while the market is settled" ); + + _asset = &_betting_market_group->asset_id(d); + FC_ASSERT( is_authorized_asset( d, *fee_paying_account, *_asset ) ); + + _current_params = &d.get_global_properties().parameters; + + // are their odds valid + FC_ASSERT( op.backer_multiplier >= _current_params->min_bet_multiplier && + op.backer_multiplier <= _current_params->max_bet_multiplier, + "Bet odds are outside the blockchain's limits" ); + if (!_current_params->permitted_betting_odds_increments.empty()) + { + bet_multiplier_type allowed_increment; + const auto iter = _current_params->permitted_betting_odds_increments.upper_bound(op.backer_multiplier); + if (iter == _current_params->permitted_betting_odds_increments.end()) + allowed_increment = std::prev(_current_params->permitted_betting_odds_increments.end())->second; + else + allowed_increment = iter->second; + FC_ASSERT(op.backer_multiplier % allowed_increment == 0, "Bet odds must be a multiple of ${allowed_increment}", ("allowed_increment", allowed_increment)); + } + + FC_ASSERT(op.amount_to_bet.amount > share_type(), "Cannot place a bet with zero amount"); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +object_id_type bet_place_evaluator::do_apply(const bet_place_operation& op) +{ try { + database& d = db(); + const bet_object& new_bet = + d.create([&](bet_object& bet_obj) { + bet_obj.bettor_id = op.bettor_id; + bet_obj.betting_market_id = op.betting_market_id; + bet_obj.amount_to_bet = op.amount_to_bet; + bet_obj.backer_multiplier = op.backer_multiplier; + bet_obj.back_or_lay = op.back_or_lay; + if (_betting_market_group->bets_are_delayed()) { + // the bet will be included in the block at time `head_block_time() + block_interval`, so make the delay relative + // to the time it's included in a block + bet_obj.end_of_delay = d.head_block_time() + _current_params->block_interval + _current_params->live_betting_delay_time; + } + }); + + bet_id_type new_bet_id = new_bet.id; // save the bet id here, new_bet may be deleted during place_bet() + + // place the bet, this may return guaranteed winnings + ddump((_betting_market_group->bets_are_delayed())(_current_params->live_betting_delay_time)); + if (!_betting_market_group->bets_are_delayed() || _current_params->live_betting_delay_time <= 0) + d.place_bet(new_bet); + + // now that their guaranteed winnings have been returned, check whether they have enough in their account to place the bet + FC_ASSERT( d.get_balance( *fee_paying_account, *_asset ).amount >= op.amount_to_bet.amount, "insufficient balance", + ("balance", d.get_balance(*fee_paying_account, *_asset))("amount_to_bet", op.amount_to_bet.amount) ); + + // pay for it + d.adjust_balance(fee_paying_account->id, -op.amount_to_bet); + + return new_bet_id; +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result bet_cancel_evaluator::do_evaluate(const bet_cancel_operation& op) +{ try { + const database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); + _bet_to_cancel = &op.bet_to_cancel(d); + FC_ASSERT( op.bettor_id == _bet_to_cancel->bettor_id, "You can only cancel your own bets" ); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result bet_cancel_evaluator::do_apply(const bet_cancel_operation& op) +{ try { + db().cancel_bet(*_bet_to_cancel); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_group_resolve_evaluator::do_evaluate(const betting_market_group_resolve_operation& op) +{ try { + database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); + _betting_market_group = &op.betting_market_group_id(d); + d.validate_betting_market_group_resolutions(*_betting_market_group, op.resolutions); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_group_resolve_evaluator::do_apply(const betting_market_group_resolve_operation& op) +{ try { + db().resolve_betting_market_group(*_betting_market_group, op.resolutions); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_group_cancel_unmatched_bets_evaluator::do_evaluate(const betting_market_group_cancel_unmatched_bets_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + _betting_market_group = &op.betting_market_group_id(db()); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_group_cancel_unmatched_bets_evaluator::do_apply(const betting_market_group_cancel_unmatched_bets_operation& op) +{ try { + db().cancel_all_unmatched_bets_on_betting_market_group(*_betting_market_group); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + + +} } // graphene::chain diff --git a/libraries/chain/betting_market_group_object.cpp b/libraries/chain/betting_market_group_object.cpp new file mode 100644 index 00000000..b980ae12 --- /dev/null +++ b/libraries/chain/betting_market_group_object.cpp @@ -0,0 +1,547 @@ +#define DEFAULT_LOGGER "betting" +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + enum class betting_market_group_state { + upcoming, + frozen_upcoming, + in_play, + frozen_in_play, + closed, + graded, + canceled, + settled + }; +} } + +FC_REFLECT_ENUM(graphene::chain::betting_market_group_state, + (upcoming) + (frozen_upcoming) + (in_play) + (frozen_in_play) + (closed) + (graded) + (canceled) + (settled)) + +namespace graphene { namespace chain { + +namespace msm = boost::msm; +namespace mpl = boost::mpl; + +// betting market object implementation +namespace +{ + // Events -- most events happen when the witnesses publish an update operation with a new + // status, so if they publish an event with the status set to `frozen`, we'll generate a `frozen_event` + struct upcoming_event + { + database& db; + upcoming_event(database& db) : db(db) {} + }; + struct frozen_event + { + database& db; + frozen_event(database& db) : db(db) {} + }; + struct in_play_event + { + database& db; + in_play_event(database& db) : db(db) {} + }; + struct closed_event + { + database& db; + bool closed_by_event; + closed_event(database& db, bool closed_by_event) : db(db), closed_by_event(closed_by_event) {} + }; + struct graded_event + { + database& db; + graded_event(database& db) : db(db) {} + }; + struct re_grading_event + { + database& db; + re_grading_event(database& db) : db(db) {} + }; + struct settled_event + { + database& db; + settled_event(database& db) : db(db) {} + }; + struct canceled_event + { + database& db; + + // true if this was triggered by setting event to canceled state, + // false if this was triggered directly on this betting market group + bool canceled_by_event; + + canceled_event(database& db, bool canceled_by_event = false) : db(db), canceled_by_event(canceled_by_event) {} + }; + + // Events + struct betting_market_group_state_machine_ : public msm::front::state_machine_def + { + // disable a few state machine features we don't use for performance + typedef int no_exception_thrown; + typedef int no_message_queue; + + // States + struct upcoming : public msm::front::state<> + { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> upcoming", ("id", fsm.betting_market_group_obj->id)); + // when a betting market group goes from frozen -> upcoming, transition the markets from frozen -> unresolved + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + try + { + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_unresolved_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + } + } + }; + struct frozen_upcoming : public msm::front::state<> + { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> frozen_upcoming", ("id", fsm.betting_market_group_obj->id)); + + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_frozen_event(event.db); + }); + } + }; + + struct in_play : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> in_play", ("id", fsm.betting_market_group_obj->id)); + // when an event goes in-play, cancel all unmatched bets in its betting markets + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.cancel_all_unmatched_bets(event.db); + try { + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_unresolved_event(event.db); + }); + } catch (const graphene::chain::no_transition&) { + // if this wasn't a transition from frozen state, this wasn't necessary + } + }); + } + }; + struct frozen_in_play : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> frozen_in_play", ("id", fsm.betting_market_group_obj->id)); + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_frozen_event(event.db); + }); + } + }; + struct closed : public msm::front::state<> { + template + void on_entry(const Event& fsm_event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> closed", ("id", fsm.betting_market_group_obj->id)); + auto& betting_market_index = fsm_event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + fsm_event.db.modify(betting_market, [&fsm_event](betting_market_object& betting_market) { + betting_market.cancel_all_unmatched_bets(fsm_event.db); + betting_market.on_closed_event(fsm_event.db); + }); + + // then notify the event that this betting market is now closed so it can change its status accordingly + if (!fsm_event.closed_by_event) { + const event_object& event = fsm.betting_market_group_obj->event_id(fsm_event.db); + fsm_event.db.modify(event, [&fsm_event,&fsm](event_object& event_obj) { + event_obj.on_betting_market_group_closed(fsm_event.db, fsm.betting_market_group_obj->id); + }); + } + } + }; + struct graded : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> graded", ("id", fsm.betting_market_group_obj->id)); + fsm.betting_market_group_obj->settling_time = event.db.head_block_time() + fsm.betting_market_group_obj->delay_before_settling; + dlog("grading complete, setting settling time to ${settling_time}", ("settling_time", fsm.betting_market_group_obj->settling_time)); + } + }; + struct re_grading : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> re_grading", ("id", fsm.betting_market_group_obj->id)); + } + }; + struct settled : public msm::front::state<> { + template + void on_entry(const Event& fsm_event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> settled", ("id", fsm.betting_market_group_obj->id)); + // TODO: what triggers this? I guess it will be the blockchain when its settling delay expires. So in that case, it should + // trigger the payout in the betting markets + auto& betting_market_index = fsm_event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + fsm_event.db.modify(betting_market, [&fsm_event](betting_market_object& betting_market) { + betting_market.on_settled_event(fsm_event.db); + }); + + // then notify the event that this betting market is now resolved so it can change its status accordingly + const event_object& event = fsm.betting_market_group_obj->event_id(fsm_event.db); + fsm_event.db.modify(event, [&fsm_event,&fsm](event_object& event_obj) { + event_obj.on_betting_market_group_resolved(fsm_event.db, fsm.betting_market_group_obj->id, false); + }); + } + }; + struct canceled : public msm::front::state<>{ + void on_entry(const canceled_event& fsm_event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> canceled", ("id", fsm.betting_market_group_obj->id)); + auto& betting_market_index = fsm_event.db.get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id)); + + for (const betting_market_object& betting_market : betting_markets_in_group) + fsm_event.db.modify(betting_market, [&fsm_event](betting_market_object& betting_market_obj) { + betting_market_obj.on_canceled_event(fsm_event.db); + }); + + if (!fsm_event.canceled_by_event) { + const event_object& event = fsm.betting_market_group_obj->event_id(fsm_event.db); + fsm_event.db.modify(event, [&fsm_event,&fsm](event_object& event_obj) { + event_obj.on_betting_market_group_resolved(fsm_event.db, fsm.betting_market_group_obj->id, true); + }); + } + + fsm.betting_market_group_obj->settling_time = fsm_event.db.head_block_time(); + dlog("cancel complete, setting settling time to ${settling_time}", ("settling_time", fsm.betting_market_group_obj->settling_time)); + } + }; + + typedef upcoming initial_state; + + // actions + void cancel_all_unmatched_bets(const in_play_event& event) { + event.db.cancel_all_unmatched_bets_on_betting_market_group(*betting_market_group_obj); + } + + // guards + bool in_play_is_allowed(const in_play_event& event) { + return !betting_market_group_obj->never_in_play; + } + + typedef betting_market_group_state_machine_ x; // makes transition table cleaner + + // Transition table for betting market + struct transition_table : mpl::vector< + // Start Event Next Action Guard + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < upcoming, frozen_event, frozen_upcoming >, + row < upcoming, in_play_event, in_play, &x::cancel_all_unmatched_bets, &x::in_play_is_allowed >, + _row < upcoming, closed_event, closed >, + _row < upcoming, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < frozen_upcoming, upcoming_event, upcoming >, + _row < frozen_upcoming, in_play_event, upcoming >, + row < frozen_upcoming, in_play_event, in_play, &x::cancel_all_unmatched_bets, &x::in_play_is_allowed >, + _row < frozen_upcoming, closed_event, closed >, + _row < frozen_upcoming, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < in_play, frozen_event, frozen_in_play >, + _row < in_play, closed_event, closed >, + _row < in_play, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < frozen_in_play, in_play_event, in_play >, + _row < frozen_in_play, closed_event, closed >, + _row < frozen_in_play, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < closed, graded_event, graded >, + _row < closed, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + //_row < graded re_grading_event, re_grading >, + _row < graded, settled_event, settled >, + _row < graded, canceled_event, canceled > + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + //_row < re_grading, graded_event, graded >, + //_row < re_grading, canceled_event, canceled > + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + > {}; + + template + void no_transition(Event const& e, Fsm& ,int state) + { + FC_THROW_EXCEPTION(graphene::chain::no_transition, "No transition"); + } + + betting_market_group_object* betting_market_group_obj; + betting_market_group_state_machine_(betting_market_group_object* betting_market_group_obj) : betting_market_group_obj(betting_market_group_obj) {} + }; + typedef msm::back::state_machine betting_market_group_state_machine; + +} // end anonymous namespace + +class betting_market_group_object::impl { +public: + betting_market_group_state_machine state_machine; + + impl(betting_market_group_object* self) : state_machine(self) {} +}; + +betting_market_group_object::betting_market_group_object() : + my(new impl(this)) +{ + dlog("betting_market_group_object ctor"); +} + +betting_market_group_object::betting_market_group_object(const betting_market_group_object& rhs) : + graphene::db::abstract_object(rhs), + description(rhs.description), + event_id(rhs.event_id), + rules_id(rhs.rules_id), + asset_id(rhs.asset_id), + total_matched_bets_amount(rhs.total_matched_bets_amount), + never_in_play(rhs.never_in_play), + delay_before_settling(rhs.delay_before_settling), + settling_time(rhs.settling_time), + my(new impl(this)) +{ + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_group_obj = this; +} + +betting_market_group_object& betting_market_group_object::operator=(const betting_market_group_object& rhs) +{ + //graphene::db::abstract_object::operator=(rhs); + id = rhs.id; + description = rhs.description; + event_id = rhs.event_id; + rules_id = rhs.rules_id; + asset_id = rhs.asset_id; + total_matched_bets_amount = rhs.total_matched_bets_amount; + never_in_play = rhs.never_in_play; + delay_before_settling = rhs.delay_before_settling; + settling_time = rhs.settling_time; + + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_group_obj = this; + + return *this; +} + +betting_market_group_object::~betting_market_group_object() +{ +} + +namespace { + + bool verify_betting_market_group_status_constants() + { + unsigned error_count = 0; + typedef msm::back::generate_state_set::type all_states; + static char const* filled_state_names[mpl::size::value]; + mpl::for_each > + (msm::back::fill_state_names(filled_state_names)); + for (unsigned i = 0; i < mpl::size::value; ++i) + { + try + { + // this is an approximate test, the state name provided by typeinfo will be mangled, but should + // at least contain the string we're looking for + const char* fc_reflected_value_name = fc::reflector::to_string((betting_market_group_state)i); + if (!strstr(filled_state_names[i], fc_reflected_value_name)) + { + fc_elog(fc::logger::get("default"), + "Error, state string mismatch between fc and boost::msm for int value ${int_value}: boost::msm -> ${boost_string}, fc::reflect -> ${fc_string}", + ("int_value", i)("boost_string", filled_state_names[i])("fc_string", fc_reflected_value_name)); + ++error_count; + } + } + catch (const fc::bad_cast_exception&) + { + fc_elog(fc::logger::get("default"), + "Error, no reflection for value ${int_value} in enum betting_market_group_status", + ("int_value", i)); + ++error_count; + } + } + if (error_count == 0) + dlog("Betting market group status constants are correct"); + else + wlog("There were ${count} errors in the betting market group status constants", ("count", error_count)); + + return error_count == 0; + } +} // end anonymous namespace + +betting_market_group_status betting_market_group_object::get_status() const +{ + static bool state_constants_are_correct = verify_betting_market_group_status_constants(); + (void)&state_constants_are_correct; + betting_market_group_state state = (betting_market_group_state)my->state_machine.current_state()[0]; + + ddump((state)); + + switch (state) + { + case betting_market_group_state::upcoming: + return betting_market_group_status::upcoming; + case betting_market_group_state::frozen_upcoming: + return betting_market_group_status::frozen; + case betting_market_group_state::in_play: + return betting_market_group_status::in_play; + case betting_market_group_state::frozen_in_play: + return betting_market_group_status::frozen; + case betting_market_group_state::closed: + return betting_market_group_status::closed; + case betting_market_group_state::graded: + return betting_market_group_status::graded; + case betting_market_group_state::canceled: + return betting_market_group_status::canceled; + case betting_market_group_state::settled: + return betting_market_group_status::settled; + default: + FC_THROW("Unexpected betting market group state"); + }; +} + +void betting_market_group_object::pack_impl(std::ostream& stream) const +{ + boost::archive::binary_oarchive oa(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + oa << my->state_machine; +} + +void betting_market_group_object::unpack_impl(std::istream& stream) +{ + boost::archive::binary_iarchive ia(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + ia >> my->state_machine; +} + +void betting_market_group_object::on_upcoming_event(database& db) +{ + my->state_machine.process_event(upcoming_event(db)); +} + +void betting_market_group_object::on_in_play_event(database& db) +{ + my->state_machine.process_event(in_play_event(db)); +} + +void betting_market_group_object::on_frozen_event(database& db) +{ + my->state_machine.process_event(frozen_event(db)); +} + +void betting_market_group_object::on_closed_event(database& db, bool closed_by_event) +{ + my->state_machine.process_event(closed_event(db, closed_by_event)); +} + +void betting_market_group_object::on_graded_event(database& db) +{ + my->state_machine.process_event(graded_event(db)); +} + +void betting_market_group_object::on_re_grading_event(database& db) +{ + my->state_machine.process_event(re_grading_event(db)); +} + +void betting_market_group_object::on_settled_event(database& db) +{ + my->state_machine.process_event(settled_event(db)); +} + +void betting_market_group_object::on_canceled_event(database& db, bool canceled_by_event) +{ + my->state_machine.process_event(canceled_event(db, canceled_by_event)); +} + +// These are the only statuses that can be explicitly set by witness operations. +// Other states can only be reached indirectly (i.e., settling happens a fixed +// delay after grading) +void betting_market_group_object::dispatch_new_status(database& db, betting_market_group_status new_status) +{ + switch (new_status) { + case betting_market_group_status::upcoming: // by witnesses to unfreeze a bmg + on_upcoming_event(db); + break; + case betting_market_group_status::in_play: // by witnesses to make a bmg in-play + on_in_play_event(db); + break; + case betting_market_group_status::closed: // by witnesses to close a bmg + on_closed_event(db, false); + break; + case betting_market_group_status::frozen: // by witnesses to freeze a bmg + on_frozen_event(db); + break; + case betting_market_group_status::canceled: // by witnesses to cancel a bmg + on_canceled_event(db, false); + break; + default: + FC_THROW("The status ${new_status} cannot be set directly", ("new_status", new_status)); + } +} + + +} } // graphene::chain + +namespace fc { + // Manually reflect betting_market_group_object to variant to properly reflect "state" + void to_variant(const graphene::chain::betting_market_group_object& betting_market_group_obj, fc::variant& v) + { + fc::mutable_variant_object o; + o("id", betting_market_group_obj.id) + ("description", betting_market_group_obj.description) + ("event_id", betting_market_group_obj.event_id) + ("rules_id", betting_market_group_obj.rules_id) + ("asset_id", betting_market_group_obj.asset_id) + ("total_matched_bets_amount", betting_market_group_obj.total_matched_bets_amount) + ("never_in_play", betting_market_group_obj.never_in_play) + ("delay_before_settling", betting_market_group_obj.delay_before_settling) + ("settling_time", betting_market_group_obj.settling_time) + ("status", betting_market_group_obj.get_status()); + + v = o; + } + + // Manually reflect betting_market_group_object to variant to properly reflect "state" + void from_variant(const fc::variant& v, graphene::chain::betting_market_group_object& betting_market_group_obj) + { + betting_market_group_obj.id = v["id"].as(); + betting_market_group_obj.description = v["description"].as(); + betting_market_group_obj.event_id = v["event_id"].as(); + betting_market_group_obj.asset_id = v["asset_id"].as(); + betting_market_group_obj.total_matched_bets_amount = v["total_matched_bets_amount"].as(); + betting_market_group_obj.never_in_play = v["never_in_play"].as(); + betting_market_group_obj.delay_before_settling = v["delay_before_settling"].as(); + betting_market_group_obj.settling_time = v["settling_time"].as>(); + graphene::chain::betting_market_group_status status = v["status"].as(); + const_cast(betting_market_group_obj.my->state_machine.current_state())[0] = (int)status; + } +} //end namespace fc + diff --git a/libraries/chain/betting_market_object.cpp b/libraries/chain/betting_market_object.cpp new file mode 100644 index 00000000..4c3eeae5 --- /dev/null +++ b/libraries/chain/betting_market_object.cpp @@ -0,0 +1,450 @@ +#define DEFAULT_LOGGER "betting" +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + enum class betting_market_state { + unresolved, + frozen, + closed, + graded, + canceled, + settled + }; +} } +FC_REFLECT_ENUM(graphene::chain::betting_market_state, + (unresolved) + (frozen) + (closed) + (graded) + (canceled) + (settled)) + + +namespace graphene { namespace chain { + +namespace msm = boost::msm; +namespace mpl = boost::mpl; + +/* static */ share_type bet_object::get_approximate_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay, bool round_up /* = false */) +{ + fc::uint128_t amount_to_match_128 = bet_amount.value; + + if (back_or_lay == bet_type::back) + { + amount_to_match_128 *= backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION; + if (round_up) + amount_to_match_128 += GRAPHENE_BETTING_ODDS_PRECISION - 1; + amount_to_match_128 /= GRAPHENE_BETTING_ODDS_PRECISION; + } + else + { + amount_to_match_128 *= GRAPHENE_BETTING_ODDS_PRECISION; + if (round_up) + amount_to_match_128 += backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION - 1; + amount_to_match_128 /= backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION; + } + return amount_to_match_128.to_uint64(); +} + +share_type bet_object::get_approximate_matching_amount(bool round_up /* = false */) const +{ + return get_approximate_matching_amount(amount_to_bet.amount, backer_multiplier, back_or_lay, round_up); +} + +/* static */ share_type bet_object::get_exact_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay) +{ + share_type back_ratio; + share_type lay_ratio; + std::tie(back_ratio, lay_ratio) = get_ratio(backer_multiplier); + if (back_or_lay == bet_type::back) + return bet_amount / back_ratio * lay_ratio; + else + return bet_amount / lay_ratio * back_ratio; +} + +share_type bet_object::get_exact_matching_amount() const +{ + return get_exact_matching_amount(amount_to_bet.amount, backer_multiplier, back_or_lay); +} + +/* static */ std::pair bet_object::get_ratio(bet_multiplier_type backer_multiplier) +{ + share_type gcd = boost::math::gcd(GRAPHENE_BETTING_ODDS_PRECISION, backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION); + return std::make_pair(GRAPHENE_BETTING_ODDS_PRECISION / gcd, (backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION) / gcd); +} + +std::pair bet_object::get_ratio() const +{ + return get_ratio(backer_multiplier); +} + +share_type bet_object::get_minimum_matchable_amount() const +{ + share_type gcd = boost::math::gcd(GRAPHENE_BETTING_ODDS_PRECISION, backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION); + return (back_or_lay == bet_type::back ? GRAPHENE_BETTING_ODDS_PRECISION : backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION) / gcd; +} + +share_type bet_object::get_minimum_matching_amount() const +{ + share_type gcd = boost::math::gcd(GRAPHENE_BETTING_ODDS_PRECISION, backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION); + return (back_or_lay == bet_type::lay ? GRAPHENE_BETTING_ODDS_PRECISION : backer_multiplier - GRAPHENE_BETTING_ODDS_PRECISION) / gcd; +} + + +share_type betting_market_position_object::reduce() +{ + share_type additional_not_cancel_balance = std::min(pay_if_payout_condition, pay_if_not_payout_condition); + if (additional_not_cancel_balance == 0) + return 0; + pay_if_payout_condition -= additional_not_cancel_balance; + pay_if_not_payout_condition -= additional_not_cancel_balance; + pay_if_not_canceled += additional_not_cancel_balance; + + share_type immediate_winnings = std::min(pay_if_canceled, pay_if_not_canceled); + if (immediate_winnings == 0) + return 0; + pay_if_canceled -= immediate_winnings; + pay_if_not_canceled -= immediate_winnings; + return immediate_winnings; +} + +// betting market object implementation +namespace +{ + // Events -- most events happen when the witnesses publish an update operation with a new + // status, so if they publish an event with the status set to `frozen`, we'll generate a `frozen_event` + struct unresolved_event + { + database& db; + unresolved_event(database& db) : db(db) {} + }; + struct frozen_event + { + database& db; + frozen_event(database& db) : db(db) {} + }; + struct closed_event + { + database& db; + closed_event(database& db) : db(db) {} + }; + struct graded_event + { + database& db; + betting_market_resolution_type new_grading; + graded_event(database& db, betting_market_resolution_type new_grading) : db(db), new_grading(new_grading) {} + }; + struct settled_event + { + database& db; + settled_event(database& db) : db(db) {} + }; + struct canceled_event + { + database& db; + canceled_event(database& db) : db(db) {} + }; + + // Events + struct betting_market_state_machine_ : public msm::front::state_machine_def + { + // disable a few state machine features we don't use for performance + typedef int no_exception_thrown; + typedef int no_message_queue; + + // States + struct unresolved : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> unresolved", ("id", fsm.betting_market_obj->id)); + } + }; + struct frozen : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> frozen", ("id", fsm.betting_market_obj->id)); + } + }; + struct closed : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> closed", ("id", fsm.betting_market_obj->id)); + } + }; + struct graded : public msm::front::state<>{ + void on_entry(const graded_event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> graded", ("id", fsm.betting_market_obj->id)); + fsm.betting_market_obj->resolution = event.new_grading; + } + }; + struct settled : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> settled", ("id", fsm.betting_market_obj->id)); + } + }; + struct canceled : public msm::front::state<>{ + void on_entry(const canceled_event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> canceled", ("id", fsm.betting_market_obj->id)); + fsm.betting_market_obj->resolution = betting_market_resolution_type::cancel; + } + }; + + typedef unresolved initial_state; + typedef betting_market_state_machine_ x; // makes transition table cleaner + + + // Transition table for betting market + struct transition_table : mpl::vector< + // Start Event Next Action Guard + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < unresolved, frozen_event, frozen >, + _row < unresolved, closed_event, closed >, + _row < unresolved, canceled_event, canceled >, + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < frozen, unresolved_event, unresolved >, + _row < frozen, closed_event, closed >, + _row < frozen, canceled_event, canceled >, + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < closed, graded_event, graded >, + _row < closed, canceled_event, canceled >, + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < graded, settled_event, settled >, + _row < graded, canceled_event, canceled > + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + > {}; + + template + void no_transition(Event const& e, Fsm& ,int state) + { + FC_THROW_EXCEPTION(graphene::chain::no_transition, "No transition"); + } + + betting_market_object* betting_market_obj; + betting_market_state_machine_(betting_market_object* betting_market_obj) : betting_market_obj(betting_market_obj) {} + }; + typedef msm::back::state_machine betting_market_state_machine; + +} // end anonymous namespace + +class betting_market_object::impl { +public: + betting_market_state_machine state_machine; + + impl(betting_market_object* self) : state_machine(self) {} +}; + +betting_market_object::betting_market_object() : + my(new impl(this)) +{ +} + +betting_market_object::betting_market_object(const betting_market_object& rhs) : + graphene::db::abstract_object(rhs), + group_id(rhs.group_id), + description(rhs.description), + payout_condition(rhs.payout_condition), + resolution(rhs.resolution), + my(new impl(this)) +{ + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_obj = this; +} + +betting_market_object& betting_market_object::operator=(const betting_market_object& rhs) +{ + //graphene::db::abstract_object::operator=(rhs); + id = rhs.id; + group_id = rhs.group_id; + description = rhs.description; + payout_condition = rhs.payout_condition; + resolution = rhs.resolution; + + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_obj = this; + + return *this; +} + +betting_market_object::~betting_market_object() +{ +} + +namespace { + + + bool verify_betting_market_status_constants() + { + unsigned error_count = 0; + typedef msm::back::generate_state_set::type all_states; + static char const* filled_state_names[mpl::size::value]; + mpl::for_each > + (msm::back::fill_state_names(filled_state_names)); + for (unsigned i = 0; i < mpl::size::value; ++i) + { + try + { + // this is an approximate test, the state name provided by typeinfo will be mangled, but should + // at least contain the string we're looking for + const char* fc_reflected_value_name = fc::reflector::to_string((betting_market_state)i); + if (!strstr(filled_state_names[i], fc_reflected_value_name)) + { + fc_elog(fc::logger::get("default"), + "Error, state string mismatch between fc and boost::msm for int value ${int_value}: " + "boost::msm -> ${boost_string}, fc::reflect -> ${fc_string}", + ("int_value", i)("boost_string", filled_state_names[i])("fc_string", fc_reflected_value_name)); + ++error_count; + } + } + catch (const fc::bad_cast_exception&) + { + fc_elog(fc::logger::get("default"), + "Error, no reflection for value ${int_value} in enum betting_market_status", + ("int_value", i)); + ++error_count; + } + } + if (error_count == 0) + dlog("Betting market status constants are correct"); + else + wlog("There were ${count} errors in the betting market status constants", ("count", error_count)); + + return error_count == 0; + } +} // end anonymous namespace + + +betting_market_status betting_market_object::get_status() const +{ + static bool state_constants_are_correct = verify_betting_market_status_constants(); + (void)&state_constants_are_correct; + betting_market_state state = (betting_market_state)my->state_machine.current_state()[0]; + + edump((state)); + + switch (state) + { + case betting_market_state::unresolved: + return betting_market_status::unresolved; + case betting_market_state::frozen: + return betting_market_status::frozen; + case betting_market_state::closed: + return betting_market_status::unresolved; + case betting_market_state::canceled: + return betting_market_status::canceled; + case betting_market_state::graded: + return betting_market_status::graded; + case betting_market_state::settled: + return betting_market_status::settled; + default: + FC_THROW("Unexpected betting market state"); + }; +} + +void betting_market_object::cancel_all_unmatched_bets(database& db) const +{ + const auto& bet_odds_idx = db.get_index_type().indices().get(); + + // first, cancel all bets on the active books + auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(id)); + auto book_end = bet_odds_idx.upper_bound(std::make_tuple(id)); + while (book_itr != book_end) + { + auto old_book_itr = book_itr; + ++book_itr; + db.cancel_bet(*old_book_itr, true); + } + + // then, cancel any delayed bets on that market. We don't have an index for + // that, so walk through all delayed bets + book_itr = bet_odds_idx.begin(); + while (book_itr != bet_odds_idx.end() && + book_itr->end_of_delay) + { + auto old_book_itr = book_itr; + ++book_itr; + if (old_book_itr->betting_market_id == id) + db.cancel_bet(*old_book_itr, true); + } +} + +void betting_market_object::pack_impl(std::ostream& stream) const +{ + boost::archive::binary_oarchive oa(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + oa << my->state_machine; +} + +void betting_market_object::unpack_impl(std::istream& stream) +{ + boost::archive::binary_iarchive ia(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + ia >> my->state_machine; +} + +void betting_market_object::on_unresolved_event(database& db) +{ + my->state_machine.process_event(unresolved_event(db)); +} + +void betting_market_object::on_frozen_event(database& db) +{ + my->state_machine.process_event(frozen_event(db)); +} + +void betting_market_object::on_closed_event(database& db) +{ + my->state_machine.process_event(closed_event(db)); +} + +void betting_market_object::on_graded_event(database& db, betting_market_resolution_type new_grading) +{ + my->state_machine.process_event(graded_event(db, new_grading)); +} + +void betting_market_object::on_settled_event(database& db) +{ + my->state_machine.process_event(settled_event(db)); +} + +void betting_market_object::on_canceled_event(database& db) +{ + my->state_machine.process_event(canceled_event(db)); +} + +} } // graphene::chain + +namespace fc { + // Manually reflect betting_market_object to variant to properly reflect "state" + void to_variant(const graphene::chain::betting_market_object& event_obj, fc::variant& v) + { + fc::mutable_variant_object o; + o("id", event_obj.id) + ("group_id", event_obj.group_id) + ("description", event_obj.description) + ("payout_condition", event_obj.payout_condition) + ("resolution", event_obj.resolution) + ("status", event_obj.get_status()); + + v = o; + } + + // Manually reflect betting_market_object to variant to properly reflect "state" + void from_variant(const fc::variant& v, graphene::chain::betting_market_object& event_obj) + { + event_obj.id = v["id"].as(); + event_obj.group_id = v["name"].as(); + event_obj.description = v["description"].as(); + event_obj.payout_condition = v["payout_condition"].as(); + event_obj.resolution = v["resolution"].as>(); + graphene::chain::betting_market_status status = v["status"].as(); + const_cast(event_obj.my->state_machine.current_state())[0] = (int)status; + } +} //end namespace fc + diff --git a/libraries/chain/confidential_evaluator.cpp b/libraries/chain/confidential_evaluator.cpp index 9946b492..9323d2d9 100644 --- a/libraries/chain/confidential_evaluator.cpp +++ b/libraries/chain/confidential_evaluator.cpp @@ -29,6 +29,8 @@ #include #include +#include + namespace graphene { namespace chain { void_result transfer_to_blind_evaluator::do_evaluate( const transfer_to_blind_operation& o ) diff --git a/libraries/chain/database.cpp b/libraries/chain/database.cpp index aa9f6127..7711f543 100644 --- a/libraries/chain/database.cpp +++ b/libraries/chain/database.cpp @@ -23,6 +23,7 @@ */ #include #include "db_balance.cpp" +#include "db_bet.cpp" #include "db_block.cpp" #include "db_debug.cpp" #include "db_getter.cpp" @@ -32,3 +33,4 @@ #include "db_market.cpp" #include "db_update.cpp" #include "db_witness_schedule.cpp" +#include "db_notify.cpp" \ No newline at end of file diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp new file mode 100644 index 00000000..e0bd5a2f --- /dev/null +++ b/libraries/chain/db_bet.cpp @@ -0,0 +1,613 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace graphene { namespace chain { + +void database::cancel_bet( const bet_object& bet, bool create_virtual_op ) +{ + asset amount_to_refund = bet.amount_to_bet; + //TODO: update global statistics + adjust_balance(bet.bettor_id, amount_to_refund); + if (create_virtual_op) + { + bet_canceled_operation bet_canceled_virtual_op(bet.bettor_id, bet.id, + bet.amount_to_bet); + //fc_idump(fc::logger::get("betting"), (bet_canceled_virtual_op)); + push_applied_operation(std::move(bet_canceled_virtual_op)); + } + remove(bet); +} + +void database::cancel_all_unmatched_bets_on_betting_market(const betting_market_object& betting_market) +{ + const auto& bet_odds_idx = get_index_type().indices().get(); + + // first, cancel all bets on the active books + auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(betting_market.id)); + auto book_end = bet_odds_idx.upper_bound(std::make_tuple(betting_market.id)); + while (book_itr != book_end) + { + auto old_book_itr = book_itr; + ++book_itr; + cancel_bet(*old_book_itr, true); + } + + // then, cancel any delayed bets on that market. We don't have an index for + // that, so walk through all delayed bets + book_itr = bet_odds_idx.begin(); + while (book_itr != bet_odds_idx.end() && + book_itr->end_of_delay) + { + auto old_book_itr = book_itr; + ++book_itr; + if (old_book_itr->betting_market_id == betting_market.id) + cancel_bet(*old_book_itr, true); + } +} + +void database::validate_betting_market_group_resolutions(const betting_market_group_object& betting_market_group, + const std::map& resolutions) +{ + auto& betting_market_index = get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + + // we must have one resolution for each betting market + FC_ASSERT(resolutions.size() == boost::size(betting_markets_in_group), + "You must publish resolutions for all ${size} markets in the group, you published ${published}", ("size", boost::size(betting_markets_in_group))("published", resolutions.size())); + + // both are sorted by id, we can walk through both and verify that they match + unsigned number_of_wins = 0; + unsigned number_of_cancels = 0; + for (const auto& zipped : boost::combine(resolutions, betting_markets_in_group)) + { + const auto& resolution = boost::get<0>(zipped); + const auto& betting_market = boost::get<1>(zipped); + FC_ASSERT(resolution.first == betting_market.id, "Missing resolution for betting market ${id}", ("id", betting_market.id)); + if (resolution.second == betting_market_resolution_type::cancel) + ++number_of_cancels; + else if (resolution.second == betting_market_resolution_type::win) + ++number_of_wins; + else + FC_ASSERT(resolution.second == betting_market_resolution_type::not_win); + } + + if (number_of_cancels != 0) + FC_ASSERT(number_of_cancels == resolutions.size(), "You must cancel all betting markets or none of the betting markets in the group"); + else + FC_ASSERT(number_of_wins == 1, "There must be exactly one winning market"); +} + +void database::cancel_all_unmatched_bets_on_betting_market_group(const betting_market_group_object& betting_market_group) +{ + auto& betting_market_index = get_index_type().indices().get(); + auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) + { + const betting_market_object& betting_market = *betting_market_itr; + ++betting_market_itr; + cancel_all_unmatched_bets_on_betting_market(betting_market); + } + +} + +void database::resolve_betting_market_group(const betting_market_group_object& betting_market_group, + const std::map& resolutions) +{ + auto& betting_market_index = get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + + bool group_was_canceled = resolutions.begin()->second == betting_market_resolution_type::cancel; + + if (group_was_canceled) + modify(betting_market_group, [group_was_canceled,this](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_canceled_event(*this, false); // this cancels the betting markets + }); + else { + // TODO: this should be pushed into the bmg's on_graded_event + + // both are sorted by id, we can walk through both and verify that they match + for (const auto& zipped : boost::combine(resolutions, betting_markets_in_group)) + { + const auto& resolution = boost::get<0>(zipped); + const auto& betting_market = boost::get<1>(zipped); + + modify(betting_market, [this,&resolution](betting_market_object& betting_market_obj) { + betting_market_obj.on_graded_event(*this, resolution.second); + }); + } + + modify(betting_market_group, [group_was_canceled,this](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_graded_event(*this); + }); + } +} + +void database::settle_betting_market_group(const betting_market_group_object& betting_market_group) +{ + fc_ilog(fc::logger::get("betting"), "Settling betting market group ${id}", ("id", betting_market_group.id)); + // we pay the rake fee to the dividend distribution account for the core asset, go ahead + // and look up that account now + fc::optional rake_account_id; + const asset_object& core_asset_obj = asset_id_type(0)(*this); + if (core_asset_obj.dividend_data_id) + { + const asset_dividend_data_object& core_asset_dividend_data_obj = (*core_asset_obj.dividend_data_id)(*this); + rake_account_id = core_asset_dividend_data_obj.dividend_distribution_account; + } + + // collect the resolutions of all markets in the BMG: they were previously published and + // stored in the individual betting markets + std::map resolutions_by_market_id; + + // collecting bettors and their positions + std::map > bettor_positions_map; + + auto& betting_market_index = get_index_type().indices().get(); + // [ROL] it seems to be my mistake - wrong index used + //auto& position_index = get_index_type().indices().get(); + auto& position_index = get_index_type().indices().get(); + auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) + { + const betting_market_object& betting_market = *betting_market_itr; + FC_ASSERT(betting_market_itr->resolution, "Unexpected error settling betting market ${market_id}: no published resolution", + ("market_id", betting_market_itr->id)); + resolutions_by_market_id.emplace(betting_market.id, *betting_market_itr->resolution); + + ++betting_market_itr; + cancel_all_unmatched_bets_on_betting_market(betting_market); + + auto position_itr = position_index.lower_bound(betting_market.id); + + while (position_itr != position_index.end() && position_itr->betting_market_id == betting_market.id) + { + const betting_market_position_object& position = *position_itr; + ++position_itr; + + bettor_positions_map[position.bettor_id].push_back(&position); + } + } + + // walking through bettors' positions and collecting winings and fees respecting asset_id + for (const auto& bettor_positions_pair: bettor_positions_map) + { + uint16_t rake_fee_percentage = get_global_properties().parameters.betting_rake_fee_percentage; + share_type net_profits; + share_type payout_amounts; + account_id_type bettor_id = bettor_positions_pair.first; + const std::vector& bettor_positions = bettor_positions_pair.second; + + for (const betting_market_position_object* position : bettor_positions) + { + betting_market_resolution_type resolution; + try + { + resolution = resolutions_by_market_id.at(position->betting_market_id); + } + catch (std::out_of_range&) + { + FC_THROW_EXCEPTION(fc::key_not_found_exception, "Unexpected betting market ID, shouldn't happen"); + } + + ///if (cancel) + /// resolution = betting_market_resolution_type::cancel; + ///else + ///{ + /// // checked in evaluator, should never happen, see above + /// assert(resolutions.count(position->betting_market_id)); + /// resolution = resolutions.at(position->betting_market_id); + ///} + + + switch (resolution) + { + case betting_market_resolution_type::win: + { + share_type total_payout = position->pay_if_payout_condition + position->pay_if_not_canceled; + payout_amounts += total_payout; + net_profits += total_payout - position->pay_if_canceled; + break; + } + case betting_market_resolution_type::not_win: + { + share_type total_payout = position->pay_if_not_payout_condition + position->pay_if_not_canceled; + payout_amounts += total_payout; + net_profits += total_payout - position->pay_if_canceled; + break; + } + case betting_market_resolution_type::cancel: + payout_amounts += position->pay_if_canceled; + break; + default: + continue; + } + remove(*position); + } + + // pay the fees to the dividend-distribution account if net profit + share_type rake_amount; + if (net_profits.value > 0 && rake_account_id) + { + rake_amount = ((fc::uint128_t(net_profits.value) * rake_fee_percentage + GRAPHENE_100_PERCENT - 1) / GRAPHENE_100_PERCENT).to_uint64(); + if (rake_amount.value) + adjust_balance(*rake_account_id, asset(rake_amount, betting_market_group.asset_id)); + } + + // pay winning - rake + adjust_balance(bettor_id, asset(payout_amounts - rake_amount, betting_market_group.asset_id)); + // [ROL] + //fc_idump(fc::logger::get("betting"), (payout_amounts)(net_profits.value)(rake_amount.value)); + + push_applied_operation(betting_market_group_resolved_operation(bettor_id, + betting_market_group.id, + resolutions_by_market_id, + payout_amounts, + rake_amount)); + } + + // At this point, the betting market group will either be in the "graded" or "canceled" state, + // if it was graded, mark it as settled. if it's canceled, let it remain canceled. + + bool was_canceled = betting_market_group.get_status() == betting_market_group_status::canceled; + + if (!was_canceled) + modify(betting_market_group, [&](betting_market_group_object& group) { + group.on_settled_event(*this); + }); + + betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) { + const betting_market_object& betting_market = *betting_market_itr; + + ++betting_market_itr; + fc_dlog(fc::logger::get("betting"), "removing betting market ${id}", ("id", betting_market.id)); + remove(betting_market); + } + + const event_object& event = betting_market_group.event_id(*this); + + fc_dlog(fc::logger::get("betting"), "removing betting market group ${id}", ("id", betting_market_group.id)); + remove(betting_market_group); +} + +void database::remove_completed_events() +{ + const auto& event_index = get_index_type().indices().get(); + + auto canceled_event_iter = event_index.lower_bound(event_status::canceled); + while (canceled_event_iter != event_index.end() && canceled_event_iter->get_status() == event_status::canceled) + { + const event_object& event = *canceled_event_iter; + ++canceled_event_iter; + fc_dlog(fc::logger::get("betting"), "removing canceled event ${id}", ("id", event.id)); + remove(event); + } + + auto settled_event_iter = event_index.lower_bound(event_status::settled); + while (settled_event_iter != event_index.end() && settled_event_iter->get_status() == event_status::settled) + { + const event_object& event = *settled_event_iter; + ++settled_event_iter; + fc_dlog(fc::logger::get("betting"), "removing settled event ${id}", ("id", event.id)); + remove(event); + } +} + +share_type adjust_betting_position(database& db, + account_id_type bettor_id, + betting_market_id_type betting_market_id, + bet_type back_or_lay, + share_type bet_amount, + share_type matched_amount) +{ try { + assert(bet_amount >= 0); + + share_type guaranteed_winnings_returned = 0; + + if (bet_amount == 0) + return guaranteed_winnings_returned; + + auto& index = db.get_index_type().indices().get(); + auto itr = index.find(boost::make_tuple(bettor_id, betting_market_id)); + if (itr == index.end()) + { + db.create([&](betting_market_position_object& position) { + position.bettor_id = bettor_id; + position.betting_market_id = betting_market_id; + position.pay_if_payout_condition = back_or_lay == bet_type::back ? bet_amount + matched_amount : 0; + position.pay_if_not_payout_condition = back_or_lay == bet_type::lay ? bet_amount + matched_amount : 0; + position.pay_if_canceled = bet_amount; + position.pay_if_not_canceled = 0; + // this should not be reducible + }); + } else { + db.modify(*itr, [&](betting_market_position_object& position) { + assert(position.bettor_id == bettor_id); + assert(position.betting_market_id == betting_market_id); + position.pay_if_payout_condition += back_or_lay == bet_type::back ? bet_amount + matched_amount : 0; + position.pay_if_not_payout_condition += back_or_lay == bet_type::lay ? bet_amount + matched_amount : 0; + position.pay_if_canceled += bet_amount; + + guaranteed_winnings_returned = position.reduce(); + }); + } + return guaranteed_winnings_returned; +} FC_CAPTURE_AND_RETHROW((bettor_id)(betting_market_id)(bet_amount)) } + + +// called twice when a bet is matched, once for the taker, once for the maker +bool bet_was_matched(database& db, const bet_object& bet, + share_type amount_bet, share_type amount_matched, + bet_multiplier_type actual_multiplier, + bool refund_unmatched_portion) +{ + // record their bet, modifying their position, and return any winnings + share_type guaranteed_winnings_returned = adjust_betting_position(db, bet.bettor_id, bet.betting_market_id, + bet.back_or_lay, amount_bet, amount_matched); + db.adjust_balance(bet.bettor_id, asset(guaranteed_winnings_returned, bet.amount_to_bet.asset_id)); + + // generate a virtual "match" op + asset asset_amount_bet(amount_bet, bet.amount_to_bet.asset_id); + + bet_matched_operation bet_matched_virtual_op(bet.bettor_id, bet.id, + asset_amount_bet, + actual_multiplier, + guaranteed_winnings_returned); + //fc_edump(fc::logger::get("betting"), (bet_matched_virtual_op)); + db.push_applied_operation(std::move(bet_matched_virtual_op)); + + // update the bet on the books + if (asset_amount_bet == bet.amount_to_bet) + { + db.remove(bet); + return true; + } + else + { + db.modify(bet, [&](bet_object& bet_obj) { + bet_obj.amount_to_bet -= asset_amount_bet; + }); + + if (refund_unmatched_portion) + { + db.cancel_bet(bet); + return true; + } + else + return false; + } +} + +/** + * Matches the two orders, + * + * @return a bit field indicating which orders were filled (and thus removed) + * + * 0 - no bet was matched (this will never happen) + * 1 - taker_bet was filled and removed from the books + * 2 - maker_bet was filled and removed from the books + * 3 - both were filled and removed from the books + */ +int match_bet(database& db, const bet_object& taker_bet, const bet_object& maker_bet ) +{ + //fc_idump(fc::logger::get("betting"), (taker_bet)(maker_bet)); + assert(taker_bet.amount_to_bet.asset_id == maker_bet.amount_to_bet.asset_id); + assert(taker_bet.amount_to_bet.amount > 0 && maker_bet.amount_to_bet.amount > 0); + assert(taker_bet.back_or_lay == bet_type::back ? taker_bet.backer_multiplier <= maker_bet.backer_multiplier : + taker_bet.backer_multiplier >= maker_bet.backer_multiplier); + assert(taker_bet.back_or_lay != maker_bet.back_or_lay); + + int result = 0; + //fc_idump(fc::logger::get("betting"), (taker_bet)(maker_bet)); + + // using the maker's odds, figure out how much of the maker's bet we would match, rounding down + // go ahead and get look up the ratio for the bet (a bet with odds 1.92 will have a ratio 25:23) + share_type back_odds_ratio; + share_type lay_odds_ratio; + std::tie(back_odds_ratio, lay_odds_ratio) = maker_bet.get_ratio(); + + // and make some shortcuts to get to the maker's and taker's side of the ratio + const share_type& maker_odds_ratio = maker_bet.back_or_lay == bet_type::back ? back_odds_ratio : lay_odds_ratio; + const share_type& taker_odds_ratio = maker_bet.back_or_lay == bet_type::back ? lay_odds_ratio : back_odds_ratio; + // we need to figure out how much of the bet matches. the smallest amount + // that could match is one maker_odds_ratio to one taker_odds_ratio, + // but we can match any integer multiple of that ratio (called the 'factor' below), + // limited only by the bet amounts. + // + //fc_idump(fc::logger::get("betting"), (back_odds_ratio)(lay_odds_ratio)); + //fc_idump(fc::logger::get("betting"), (maker_odds_ratio)(taker_odds_ratio)); + + // now figure out how much of the maker bet we'll consume. We don't yet know whether the maker or taker + // will be the limiting factor. + share_type maximum_factor_taker_is_willing_to_pay = taker_bet.amount_to_bet.amount / taker_odds_ratio; + + share_type maximum_taker_factor = maximum_factor_taker_is_willing_to_pay; + if (taker_bet.back_or_lay == bet_type::lay) { + share_type maximum_factor_taker_is_willing_to_receive = taker_bet.get_exact_matching_amount() / maker_odds_ratio; + //fc_idump(fc::logger::get("betting"), (maximum_factor_taker_is_willing_to_pay)); + bool taker_was_limited_by_matching_amount = maximum_factor_taker_is_willing_to_receive < maximum_factor_taker_is_willing_to_pay; + if (taker_was_limited_by_matching_amount) + maximum_taker_factor = maximum_factor_taker_is_willing_to_receive; + } + //fc_idump(fc::logger::get("betting"), (maximum_factor_taker_is_willing_to_pay)(maximum_taker_factor)); + + share_type maximum_maker_factor = maker_bet.amount_to_bet.amount / maker_odds_ratio; + share_type maximum_factor = std::min(maximum_taker_factor, maximum_maker_factor); + share_type maker_amount_to_match = maximum_factor * maker_odds_ratio; + share_type taker_amount_to_match = maximum_factor * taker_odds_ratio; + fc_idump(fc::logger::get("betting"), (maker_amount_to_match)(taker_amount_to_match)); + + // TODO: analyze whether maximum_maker_amount_to_match can ever be zero here + assert(maker_amount_to_match != 0); + if (maker_amount_to_match == 0) + return 0; + +#ifndef NDEBUG + assert(taker_amount_to_match <= taker_bet.amount_to_bet.amount); + assert(taker_amount_to_match / taker_odds_ratio * taker_odds_ratio == taker_amount_to_match); + { + // verify we're getting the odds we expect + fc::uint128_t payout_128 = maker_amount_to_match.value; + payout_128 += taker_amount_to_match.value; + payout_128 *= GRAPHENE_BETTING_ODDS_PRECISION; + payout_128 /= maker_bet.back_or_lay == bet_type::back ? maker_amount_to_match.value : taker_amount_to_match.value; + assert(payout_128.to_uint64() == maker_bet.backer_multiplier); + } +#endif + + //fc_idump(fc::logger::get("betting"), (taker_amount_to_match)(maker_amount_to_match)); + + // maker bets will always be an exact multiple of maker_odds_ratio, so they will either completely match or remain on the books + bool maker_bet_will_completely_match = maker_amount_to_match == maker_bet.amount_to_bet.amount; + + if (maker_bet_will_completely_match && taker_amount_to_match != taker_bet.amount_to_bet.amount) + { + // then the taker bet will stay on the books. If the taker odds != the maker odds, we will + // need to refund the stake the taker was expecting to pay but didn't. + // compute how much of the taker's bet should still be left on the books and how much + // the taker should pay for the remaining amount; refund any amount that won't remain + // on the books and isn't used to pay the bet we're currently matching. + + share_type takers_odds_back_odds_ratio; + share_type takers_odds_lay_odds_ratio; + std::tie(takers_odds_back_odds_ratio, takers_odds_lay_odds_ratio) = taker_bet.get_ratio(); + const share_type& takers_odds_taker_odds_ratio = taker_bet.back_or_lay == bet_type::back ? takers_odds_back_odds_ratio : takers_odds_lay_odds_ratio; + const share_type& takers_odds_maker_odds_ratio = taker_bet.back_or_lay == bet_type::back ? takers_odds_lay_odds_ratio : takers_odds_back_odds_ratio; + share_type taker_refund_amount; + + if (taker_bet.back_or_lay == bet_type::back) + { + // because we matched at the maker's odds and not the taker's odds, the remaining amount to match + // may not be an even multiple of the taker's odds; round it down. + share_type taker_remaining_factor = (taker_bet.amount_to_bet.amount - taker_amount_to_match) / takers_odds_taker_odds_ratio; + share_type taker_remaining_bet_amount = taker_remaining_factor * takers_odds_taker_odds_ratio; + taker_refund_amount = taker_bet.amount_to_bet.amount - taker_amount_to_match - taker_remaining_bet_amount; + //idump((taker_remaining_factor)(taker_remaining_bet_amount)(taker_refund_amount)); + } + else + { + // the taker bet is a lay bet. because we matched at the maker's odds and not the taker's odds, + // there are two things we need to take into account. First, we may have achieved more of a position + // than we expected had we matched at our taker odds. If so, we can refund the unused stake. + // Second, the remaining amount to match may not be an even multiple of the taker's odds; round it down. + share_type unrounded_taker_remaining_amount_to_match = taker_bet.get_exact_matching_amount() - maker_amount_to_match; + //idump((unrounded_taker_remaining_amount_to_match)); + + // because we matched at the maker's odds and not the taker's odds, the remaining amount to match + // may not be an even multiple of the taker's odds; round it down. + share_type taker_remaining_factor = unrounded_taker_remaining_amount_to_match / takers_odds_maker_odds_ratio; + share_type taker_remaining_maker_amount_to_match = taker_remaining_factor * takers_odds_maker_odds_ratio; + share_type taker_remaining_bet_amount = taker_remaining_factor * takers_odds_taker_odds_ratio; + + taker_refund_amount = taker_bet.amount_to_bet.amount - taker_amount_to_match - taker_remaining_bet_amount; + //idump((taker_remaining_factor)(taker_remaining_maker_amount_to_match)(taker_remaining_bet_amount)(taker_refund_amount)); + } + + if (taker_refund_amount > share_type()) + { + db.modify(taker_bet, [&taker_refund_amount](bet_object& taker_bet_object) { + taker_bet_object.amount_to_bet.amount -= taker_refund_amount; + }); + fc_dlog(fc::logger::get("betting"), "Refunding ${taker_refund_amount} to taker because we matched at the maker's odds of " + "${maker_odds} instead of the taker's odds ${taker_odds}", + ("taker_refund_amount", taker_refund_amount) + ("maker_odds", maker_bet.backer_multiplier) + ("taker_odds", taker_bet.backer_multiplier)); + fc_ddump(fc::logger::get("betting"), (taker_bet)); + + db.adjust_balance(taker_bet.bettor_id, asset(taker_refund_amount, taker_bet.amount_to_bet.asset_id)); + // TODO: update global statistics + bet_adjusted_operation bet_adjusted_op(taker_bet.bettor_id, taker_bet.id, + asset(taker_refund_amount, taker_bet.amount_to_bet.asset_id)); + // fc_idump(fc::logger::get("betting"), (bet_adjusted_op)(new_bet_object)); + db.push_applied_operation(std::move(bet_adjusted_op)); + } + } + + // if the maker bet stays on the books, we need to make sure the taker bet is removed from the books (either it fills completely, + // or any un-filled amount is canceled) + result |= bet_was_matched(db, taker_bet, taker_amount_to_match, maker_amount_to_match, maker_bet.backer_multiplier, !maker_bet_will_completely_match); + result |= bet_was_matched(db, maker_bet, maker_amount_to_match, taker_amount_to_match, maker_bet.backer_multiplier, false) << 1; + + assert(result != 0); + return result; +} + + +// called from the bet_place_evaluator +bool database::place_bet(const bet_object& new_bet_object) +{ + // We allow users to place bets for any amount, but only amounts that are exact multiples of the odds + // ratio can be matched. Immediately return any unmatchable amount in this bet. + share_type minimum_matchable_amount = new_bet_object.get_minimum_matchable_amount(); + share_type scale_factor = new_bet_object.amount_to_bet.amount / minimum_matchable_amount; + share_type rounded_bet_amount = scale_factor * minimum_matchable_amount; + + if (rounded_bet_amount == share_type()) + { + // the bet was too small to match at all, cancel the bet + cancel_bet(new_bet_object, true); + return true; + } + else if (rounded_bet_amount != new_bet_object.amount_to_bet.amount) + { + asset stake_returned = new_bet_object.amount_to_bet; + stake_returned.amount -= rounded_bet_amount; + + modify(new_bet_object, [&rounded_bet_amount](bet_object& modified_bet_object) { + modified_bet_object.amount_to_bet.amount = rounded_bet_amount; + }); + + adjust_balance(new_bet_object.bettor_id, stake_returned); + // TODO: update global statistics + bet_adjusted_operation bet_adjusted_op(new_bet_object.bettor_id, new_bet_object.id, + stake_returned); + // fc_idump(fc::logger::get("betting"), (bet_adjusted_op)(new_bet_object)); + push_applied_operation(std::move(bet_adjusted_op)); + + fc_dlog(fc::logger::get("betting"), "Refunded ${refund_amount} to round the bet down to something that can match exactly, new bet: ${new_bet}", + ("refund_amount", stake_returned.amount) + ("new_bet", new_bet_object)); + } + + const auto& bet_odds_idx = get_index_type().indices().get(); + + bet_type bet_type_to_match = new_bet_object.back_or_lay == bet_type::back ? bet_type::lay : bet_type::back; + auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(new_bet_object.betting_market_id, bet_type_to_match)); + auto book_end = bet_odds_idx.upper_bound(std::make_tuple(new_bet_object.betting_market_id, bet_type_to_match, new_bet_object.backer_multiplier)); + + // fc_ilog(fc::logger::get("betting"), ""); + // fc_ilog(fc::logger::get("betting"), "------------ order book ------------------"); + // for (auto itr = book_itr; itr != book_end; ++itr) + // fc_idump(fc::logger::get("betting"), (*itr)); + // fc_ilog(fc::logger::get("betting"), "------------ order book ------------------"); + + int orders_matched_flags = 0; + bool finished = false; + while (!finished && book_itr != book_end) + { + auto old_book_itr = book_itr; + ++book_itr; + + orders_matched_flags = match_bet(*this, new_bet_object, *old_book_itr); + + // we continue if the maker bet was completely consumed AND the taker bet was not + finished = orders_matched_flags != 2; + } + if (!(orders_matched_flags & 1)) + fc_ddump(fc::logger::get("betting"), (new_bet_object)); + + + // return true if the taker bet was completely consumed + return (orders_matched_flags & 1) != 0; +} + +} } diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index 59c5f0cf..b63dfde4 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -29,6 +29,7 @@ #include #include #include + #include #include #include @@ -241,7 +242,7 @@ processed_transaction database::_push_transaction( const signed_transaction& trx auto processed_trx = _apply_transaction( trx ); _pending_tx.push_back(processed_trx); - notify_changed_objects(); + // notify_changed_objects(); // The transaction applied successfully. Merge its changes into the pending block session. temp_session.merge(); @@ -285,13 +286,13 @@ processed_transaction database::push_proposal(const proposal_object& proposal) { _applied_ops.resize( old_applied_ops_size ); } - elog( "e", ("e",e.to_detail_string() ) ); + edump((e)); throw; } ptrx.operation_results = std::move(eval_state.operation_results); return ptrx; -} FC_CAPTURE_AND_RETHROW( (proposal) ) } +} FC_CAPTURE_AND_RETHROW() } signed_block database::generate_block( fc::time_point_sec when, @@ -478,6 +479,10 @@ const vector >& database::get_applied_opera return _applied_ops; } +vector >& database::get_applied_operations() +{ + return _applied_ops; +} //////////////////// private methods //////////////////// void database::apply_block( const signed_block& next_block, uint32_t skip ) @@ -539,12 +544,14 @@ void database::_apply_block( const signed_block& next_block ) perform_chain_maintenance(next_block, global_props); create_block_summary(next_block); + place_delayed_bets(); // must happen after update_global_dynamic_data() updates the time clear_expired_transactions(); clear_expired_proposals(); clear_expired_orders(); update_expired_feeds(); update_withdraw_permissions(); update_tournaments(); + update_betting_markets(next_block.timestamp); // n.b., update_maintenance_flag() happens this late // because get_slot_time() / get_slot_at_time() is needed above @@ -562,27 +569,9 @@ void database::_apply_block( const signed_block& next_block ) _applied_ops.clear(); notify_changed_objects(); - } FC_CAPTURE_AND_RETHROW( (next_block.block_num()) ) } -void database::notify_changed_objects() -{ try { - if( _undo_db.enabled() ) - { - const auto& head_undo = _undo_db.head(); - vector changed_ids; changed_ids.reserve(head_undo.old_values.size()); - for( const auto& item : head_undo.old_values ) changed_ids.push_back(item.first); - for( const auto& item : head_undo.new_ids ) changed_ids.push_back(item); - vector removed; - removed.reserve( head_undo.removed.size() ); - for( const auto& item : head_undo.removed ) - { - changed_ids.push_back( item.first ); - removed.emplace_back( item.second.get() ); - } - changed_objects(changed_ids); - } -} FC_CAPTURE_AND_RETHROW() } + processed_transaction database::apply_transaction(const signed_transaction& trx, uint32_t skip) { diff --git a/libraries/chain/db_getter.cpp b/libraries/chain/db_getter.cpp index 4af2df3e..9516e256 100644 --- a/libraries/chain/db_getter.cpp +++ b/libraries/chain/db_getter.cpp @@ -47,7 +47,7 @@ const chain_property_object& database::get_chain_properties()const return get( chain_property_id_type() ); } -const dynamic_global_property_object&database::get_dynamic_global_properties() const +const dynamic_global_property_object& database::get_dynamic_global_properties() const { return get( dynamic_global_property_id_type() ); } diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 19d32e8c..6ceee041 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -50,6 +50,13 @@ #include #include + +#include +#include +#include +#include +#include + #include #include #include @@ -64,6 +71,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -130,6 +141,33 @@ const uint8_t witness_object::type_id; const uint8_t worker_object::space_id; const uint8_t worker_object::type_id; +const uint8_t sport_object::space_id; +const uint8_t sport_object::type_id; + +const uint8_t event_group_object::space_id; +const uint8_t event_group_object::type_id; + +const uint8_t event_object::space_id; +const uint8_t event_object::type_id; + +const uint8_t betting_market_rules_object::space_id; +const uint8_t betting_market_rules_object::type_id; + +const uint8_t betting_market_group_object::space_id; +const uint8_t betting_market_group_object::type_id; + +const uint8_t betting_market_object::space_id; +const uint8_t betting_market_object::type_id; + +const uint8_t bet_object::space_id; +const uint8_t bet_object::type_id; + +const uint8_t betting_market_position_object::space_id; +const uint8_t betting_market_position_object::type_id; + +const uint8_t global_betting_statistics_object::space_id; +const uint8_t global_betting_statistics_object::type_id; + void database::initialize_evaluators() { @@ -176,6 +214,23 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); register_evaluator(); register_evaluator(); register_evaluator(); @@ -208,6 +263,13 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); + add_index< primary_index >(); add_index< primary_index >(); auto tournament_details_idx = add_index< primary_index >(); @@ -230,8 +292,11 @@ void database::initialize_indexes() add_index< primary_index > >(); add_index< primary_index< special_authority_index > >(); add_index< primary_index< buyback_index > >(); - add_index< primary_index< simple_index< fba_accumulator_object > > >(); + add_index< primary_index< betting_market_position_index > >(); + add_index< primary_index< global_betting_statistics_object_index > >(); + //add_index< primary_index >(); + //add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); } @@ -329,7 +394,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.lifetime_referrer_fee_percentage = GRAPHENE_100_PERCENT; }).get_id() == GRAPHENE_PROXY_TO_SELF_ACCOUNT); FC_ASSERT(create([this](account_object& a) { - a.name = "test-dividend-distribution"; + a.name = "default-dividend-distribution"; a.statistics = create([&](account_statistics_object& s){s.owner = a.id;}).id; a.owner.weight_threshold = 1; a.active.weight_threshold = 1; @@ -337,7 +402,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.membership_expiration_date = time_point_sec::maximum(); a.network_fee_percentage = 0; a.lifetime_referrer_fee_percentage = GRAPHENE_100_PERCENT; - }).get_id() == TOURNAMENT_RAKE_FEE_ACCOUNT_ID); + }).get_id() == GRAPHENE_RAKE_FEE_ACCOUNT_ID); // Create more special accounts while( true ) { @@ -368,9 +433,11 @@ void database::init_genesis(const genesis_state_type& genesis_state) create([&](asset_dividend_data_object& a) { a.options.minimum_distribution_interval = 3*24*60*60; a.options.minimum_fee_percentage = 10*GRAPHENE_1_PERCENT; - a.options.next_payout_time = genesis_state.initial_timestamp + fc::days(1); - a.options.payout_interval = 30*24*60*60; - a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + a.options.next_payout_time = genesis_state.initial_timestamp + fc::hours(1); + a.options.payout_interval = 7*24*60*60; + a.dividend_distribution_account = GRAPHENE_RAKE_FEE_ACCOUNT_ID; + //a.options.next_payout_time = genesis_state.initial_timestamp + fc::days(1); + //a.options.payout_interval = 30*24*60*60; }); const asset_object& core_asset = @@ -387,7 +454,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.options.core_exchange_rate.quote.asset_id = asset_id_type(0); a.dynamic_asset_data_id = dyn_asset.id; a.dividend_data_id = div_asset.id; -}); + }); assert( asset_id_type(core_asset.id) == asset().asset_id ); assert( get_balance(account_id_type(), asset_id_type()) == asset(dyn_asset.current_supply) ); @@ -403,18 +470,18 @@ void database::init_genesis(const genesis_state_type& genesis_state) a.options.minimum_fee_percentage = 10*GRAPHENE_1_PERCENT; a.options.next_payout_time = genesis_state.initial_timestamp + fc::hours(1); a.options.payout_interval = 7*24*60*60; - a.dividend_distribution_account = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + a.dividend_distribution_account = GRAPHENE_RAKE_FEE_ACCOUNT_ID; }); const asset_object& default_asset = create( [&]( asset_object& a ) { - a.symbol = "DEFAULT"; + a.symbol = "DEF"; a.options.max_market_fee = a.options.max_supply = genesis_state.max_core_supply; a.precision = GRAPHENE_BLOCKCHAIN_PRECISION_DIGITS; a.options.flags = 0; a.options.issuer_permissions = 79; - a.issuer = TOURNAMENT_RAKE_FEE_ACCOUNT_ID; + a.issuer = GRAPHENE_RAKE_FEE_ACCOUNT_ID; a.options.core_exchange_rate.base.amount = 1; a.options.core_exchange_rate.base.asset_id = asset_id_type(0); a.options.core_exchange_rate.quote.amount = 1; @@ -424,6 +491,7 @@ void database::init_genesis(const genesis_state_type& genesis_state) }); assert( default_asset.id == asset_id_type(1) ); #endif + // Create more special assets while( true ) { @@ -467,6 +535,9 @@ void database::init_genesis(const genesis_state_type& genesis_state) p.witness_budget = 0; p.recent_slots_filled = fc::uint128::max_value(); }); + create([&](global_betting_statistics_object& betting_statistics) { + betting_statistics.number_of_active_events = 0; + }); FC_ASSERT( (genesis_state.immutable_parameters.min_witness_count & 1) == 1, "min_witness_count must be odd" ); FC_ASSERT( (genesis_state.immutable_parameters.min_committee_member_count & 1) == 1, "min_committee_member_count must be odd" ); diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index a41bc7df..04ea3c59 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -48,6 +48,8 @@ #include #include +#define USE_VESTING_OBJECT_BY_ASSET_BALANCE_INDEX // vesting_balance_object by_asset_balance index needed + namespace graphene { namespace chain { template @@ -226,7 +228,7 @@ void database::update_active_witnesses() { vote_counter vc; for( const witness_object& wit : wits ) - vc.add( wit.witness_account, _vote_tally_buffer[wit.vote_id] ); + vc.add( wit.witness_account, std::max(_vote_tally_buffer[wit.vote_id], UINT64_C(1)) ); vc.finish( a.active ); } } ); @@ -308,7 +310,7 @@ void database::update_active_committee_members() { vote_counter vc; for( const committee_member_object& cm : committee_members ) - vc.add( cm.committee_member_account, _vote_tally_buffer[cm.vote_id] ); + vc.add( cm.committee_member_account, std::max(_vote_tally_buffer[cm.vote_id], UINT64_C(1)) ); vc.finish( a.active ); } } ); @@ -735,7 +737,7 @@ void schedule_pending_dividend_balances(database& db, const vesting_balance_index& vesting_index, const total_distributed_dividend_balance_object_index& distributed_dividend_balance_index, const pending_dividend_payout_balance_for_holder_object_index& pending_payout_balance_index) -{ +{ try { dlog("Processing dividend payments for dividend holder asset type ${holder_asset} at time ${t}", ("holder_asset", dividend_holder_asset_obj.symbol)("t", db.head_block_time())); auto current_distribution_account_balance_range = @@ -759,6 +761,7 @@ void schedule_pending_dividend_balances(database& db, uint64_t total_fee_per_asset_in_core = distribution_base_fee + holder_account_count * (uint64_t)distribution_fee_per_holder; std::map vesting_amounts; +#ifdef USE_VESTING_OBJECT_BY_ASSET_BALANCE_INDEX // get only once a collection of accounts that hold nonzero vesting balances of the dividend asset auto vesting_balances_begin = vesting_index.indices().get().lower_bound(boost::make_tuple(dividend_holder_asset_obj.id)); @@ -771,6 +774,20 @@ void schedule_pending_dividend_balances(database& db, // ("owner", vesting_balance_obj.owner(db).name) // ("amount", vesting_balance_obj.balance.amount)); } +#else + // get only once a collection of accounts that hold nonzero vesting balances of the dividend asset + const auto& vesting_balances = vesting_index.indices().get(); + for (const vesting_balance_object& vesting_balance_obj : vesting_balances) + { + if (vesting_balance_obj.balance.asset_id == dividend_holder_asset_obj.id && vesting_balance_obj.balance.amount) + { + vesting_amounts[vesting_balance_obj.owner] += vesting_balance_obj.balance.amount; + dlog("Vesting balance for account: ${owner}, amount: ${amount}", + ("owner", vesting_balance_obj.owner(db).name) + ("amount", vesting_balance_obj.balance.amount)); + } + } +#endif auto current_distribution_account_balance_iter = current_distribution_account_balance_range.first; auto previous_distribution_account_balance_iter = previous_distribution_account_balance_range.first; @@ -1042,10 +1059,10 @@ void schedule_pending_dividend_balances(database& db, dividend_data_obj.last_distribution_time = current_head_block_time; }); -} +} FC_CAPTURE_AND_RETHROW() } void process_dividend_assets(database& db) -{ +{ try { ilog("In process_dividend_assets time ${time}", ("time", db.head_block_time())); const account_balance_index& balance_index = db.get_index_type(); @@ -1067,143 +1084,146 @@ void process_dividend_assets(database& db) if (dividend_data.options.next_payout_time && db.head_block_time() >= *dividend_data.options.next_payout_time) { - dlog("Dividend payout time has arrived for asset ${holder_asset}", - ("holder_asset", dividend_holder_asset_obj.symbol)); - + try + { + dlog("Dividend payout time has arrived for asset ${holder_asset}", + ("holder_asset", dividend_holder_asset_obj.symbol)); #ifndef NDEBUG - // dump balances before the payouts for debugging - const auto& balance_idx = db.get_index_type().indices().get(); - auto holder_account_balance_range = balance_idx.equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); - for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) - ilog(" Current balance: ${asset}", ("asset", asset(holder_balance_object.balance, holder_balance_object.asset_type))); + // dump balances before the payouts for debugging + const auto& balance_idx = db.get_index_type().indices().get(); + auto holder_account_balance_range = balance_idx.equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) + ilog(" Current balance: ${asset}", ("asset", asset(holder_balance_object.balance, holder_balance_object.asset_type))); #endif - // when we do the payouts, we first increase the balances in all of the receiving accounts - // and use this map to keep track of the total amount of each asset paid out. - // Afterwards, we decrease the distribution account's balance by the total amount paid out, - // and modify the distributed_balances accordingly - std::map amounts_paid_out_by_asset; + // when we do the payouts, we first increase the balances in all of the receiving accounts + // and use this map to keep track of the total amount of each asset paid out. + // Afterwards, we decrease the distribution account's balance by the total amount paid out, + // and modify the distributed_balances accordingly + std::map amounts_paid_out_by_asset; - auto pending_payouts_range = - pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); - // the pending_payouts_range is all payouts for this dividend asset, sorted by the holder's account - // we iterate in this order so we can build up a list of payouts for each account to put in the - // virtual op - flat_set payouts_for_this_holder; - fc::optional last_holder_account_id; + auto pending_payouts_range = + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + // the pending_payouts_range is all payouts for this dividend asset, sorted by the holder's account + // we iterate in this order so we can build up a list of payouts for each account to put in the + // virtual op + vector payouts_for_this_holder; + fc::optional last_holder_account_id; - // cache the assets the distribution account is approved to send, we will be asking - // for these often - flat_map approved_assets; // assets that the dividend distribution account is authorized to send/receive - auto is_asset_approved_for_distribution_account = [&](const asset_id_type& asset_id) { - auto approved_assets_iter = approved_assets.find(asset_id); - if (approved_assets_iter != approved_assets.end()) - return approved_assets_iter->second; - bool is_approved = is_authorized_asset(db, dividend_distribution_account_object, - asset_id(db)); - approved_assets[asset_id] = is_approved; - return is_approved; - }; + // cache the assets the distribution account is approved to send, we will be asking + // for these often + flat_map approved_assets; // assets that the dividend distribution account is authorized to send/receive + auto is_asset_approved_for_distribution_account = [&](const asset_id_type& asset_id) { + auto approved_assets_iter = approved_assets.find(asset_id); + if (approved_assets_iter != approved_assets.end()) + return approved_assets_iter->second; + bool is_approved = is_authorized_asset(db, dividend_distribution_account_object, + asset_id(db)); + approved_assets[asset_id] = is_approved; + return is_approved; + }; - for (auto pending_balance_object_iter = pending_payouts_range.first; pending_balance_object_iter != pending_payouts_range.second; ) - { - const pending_dividend_payout_balance_for_holder_object& pending_balance_object = *pending_balance_object_iter; + for (auto pending_balance_object_iter = pending_payouts_range.first; pending_balance_object_iter != pending_payouts_range.second; ) + { + const pending_dividend_payout_balance_for_holder_object& pending_balance_object = *pending_balance_object_iter; - if (last_holder_account_id && *last_holder_account_id != pending_balance_object.owner && payouts_for_this_holder.size()) + if (last_holder_account_id && *last_holder_account_id != pending_balance_object.owner && payouts_for_this_holder.size()) + { + // we've moved on to a new account, generate the dividend payment virtual op for the previous one + db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, + *last_holder_account_id, + payouts_for_this_holder)); + dlog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); + payouts_for_this_holder.clear(); + last_holder_account_id.reset(); + } + + + if (pending_balance_object.pending_balance.value && + is_authorized_asset(db, pending_balance_object.owner(db), pending_balance_object.dividend_payout_asset_type(db)) && + is_asset_approved_for_distribution_account(pending_balance_object.dividend_payout_asset_type)) + { + dlog("Processing payout of ${asset} to account ${account}", + ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) + ("account", pending_balance_object.owner(db).name)); + + db.adjust_balance(pending_balance_object.owner, + asset(pending_balance_object.pending_balance, + pending_balance_object.dividend_payout_asset_type)); + payouts_for_this_holder.push_back(asset(pending_balance_object.pending_balance, + pending_balance_object.dividend_payout_asset_type)); + last_holder_account_id = pending_balance_object.owner; + amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; + + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_for_holder_object& pending_balance ){ + pending_balance.pending_balance = 0; + }); + } + + ++pending_balance_object_iter; + } + // we will always be left with the last holder's data, generate the virtual op for it now. + if (last_holder_account_id && payouts_for_this_holder.size()) { // we've moved on to a new account, generate the dividend payment virtual op for the previous one db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, *last_holder_account_id, payouts_for_this_holder)); dlog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); - payouts_for_this_holder.clear(); - last_holder_account_id.reset(); } + // now debit the total amount of dividends paid out from the distribution account + // and reduce the distributed_balances accordingly - if (pending_balance_object.pending_balance.value && - is_authorized_asset(db, pending_balance_object.owner(db), pending_balance_object.dividend_payout_asset_type(db)) && - is_asset_approved_for_distribution_account(pending_balance_object.dividend_payout_asset_type)) + for (const auto& value : amounts_paid_out_by_asset) { - dlog("Processing payout of ${asset} to account ${account}", - ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) - ("account", pending_balance_object.owner(db).name)); + const asset_id_type& asset_paid_out = value.first; + const share_type& amount_paid_out = value.second; - db.adjust_balance(pending_balance_object.owner, - asset(pending_balance_object.pending_balance, - pending_balance_object.dividend_payout_asset_type)); - payouts_for_this_holder.insert(asset(pending_balance_object.pending_balance, - pending_balance_object.dividend_payout_asset_type)); - last_holder_account_id = pending_balance_object.owner; - amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; + db.adjust_balance(dividend_data.dividend_distribution_account, + asset(-amount_paid_out, + asset_paid_out)); + auto distributed_balance_iter = + distributed_dividend_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, + asset_paid_out)); + assert(distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()); + if (distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()) + db.modify(*distributed_balance_iter, [&]( total_distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval -= amount_paid_out; // now they've been paid out, reset to zero + }); - db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_for_holder_object& pending_balance ){ - pending_balance.pending_balance = 0; - }); } - ++pending_balance_object_iter; - } - // we will always be left with the last holder's data, generate the virtual op for it now. - if (last_holder_account_id && payouts_for_this_holder.size()) - { - // we've moved on to a new account, generate the dividend payment virtual op for the previous one - db.push_applied_operation(asset_dividend_distribution_operation(dividend_holder_asset_obj.id, - *last_holder_account_id, - payouts_for_this_holder)); - dlog("Just pushed virtual op for payout to ${account}", ("account", (*last_holder_account_id)(db).name)); - } + // now schedule the next payout time + db.modify(dividend_data, [current_head_block_time](asset_dividend_data_object& dividend_data_obj) { + dividend_data_obj.last_scheduled_payout_time = dividend_data_obj.options.next_payout_time; + dividend_data_obj.last_payout_time = current_head_block_time; + fc::optional next_payout_time; + if (dividend_data_obj.options.payout_interval) + { + // if there was a previous payout, make our next payment one interval + uint32_t current_time_sec = current_head_block_time.sec_since_epoch(); + fc::time_point_sec reference_time = *dividend_data_obj.last_scheduled_payout_time; + uint32_t next_possible_time_sec = dividend_data_obj.last_scheduled_payout_time->sec_since_epoch(); + do + next_possible_time_sec += *dividend_data_obj.options.payout_interval; + while (next_possible_time_sec <= current_time_sec); - // now debit the total amount of dividends paid out from the distribution account - // and reduce the distributed_balances accordingly - - for (const auto& value : amounts_paid_out_by_asset) - { - const asset_id_type& asset_paid_out = value.first; - const share_type& amount_paid_out = value.second; - - db.adjust_balance(dividend_data.dividend_distribution_account, - asset(-amount_paid_out, - asset_paid_out)); - auto distributed_balance_iter = - distributed_dividend_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, - asset_paid_out)); - assert(distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()); - if (distributed_balance_iter != distributed_dividend_balance_index.indices().get().end()) - db.modify(*distributed_balance_iter, [&]( total_distributed_dividend_balance_object& obj ){ - obj.balance_at_last_maintenance_interval -= amount_paid_out; // now they've been paid out, reset to zero - }); - - } - - // now schedule the next payout time - db.modify(dividend_data, [current_head_block_time](asset_dividend_data_object& dividend_data_obj) { - dividend_data_obj.last_scheduled_payout_time = dividend_data_obj.options.next_payout_time; - dividend_data_obj.last_payout_time = current_head_block_time; - fc::optional next_payout_time; - if (dividend_data_obj.options.payout_interval) - { - // if there was a previous payout, make our next payment one interval - uint32_t current_time_sec = current_head_block_time.sec_since_epoch(); - fc::time_point_sec reference_time = *dividend_data_obj.last_scheduled_payout_time; - uint32_t next_possible_time_sec = dividend_data_obj.last_scheduled_payout_time->sec_since_epoch(); - do - next_possible_time_sec += *dividend_data_obj.options.payout_interval; - while (next_possible_time_sec <= current_time_sec); - - next_payout_time = next_possible_time_sec; - } - dividend_data_obj.options.next_payout_time = next_payout_time; - idump((dividend_data_obj.last_scheduled_payout_time) - (dividend_data_obj.last_payout_time) - (dividend_data_obj.options.next_payout_time)); - }); + next_payout_time = next_possible_time_sec; + } + dividend_data_obj.options.next_payout_time = next_payout_time; + idump((dividend_data_obj.last_scheduled_payout_time) + (dividend_data_obj.last_payout_time) + (dividend_data_obj.options.next_payout_time)); + }); + } + FC_RETHROW_EXCEPTIONS(error, "Error while paying out dividends for holder asset ${holder_asset}", ("holder_asset", dividend_holder_asset_obj.symbol)) } } -} +} FC_CAPTURE_AND_RETHROW() } void database::perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props) -{ +{ try { const auto& gpo = get_global_properties(); distribute_fba_balances(*this); @@ -1225,6 +1245,7 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g d._total_voting_stake = 0; const vesting_balance_index& vesting_index = d.get_index_type(); +#ifdef USE_VESTING_OBJECT_BY_ASSET_BALANCE_INDEX auto vesting_balances_begin = vesting_index.indices().get().lower_bound(boost::make_tuple(asset_id_type())); auto vesting_balances_end = @@ -1236,6 +1257,19 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g // ("owner", vesting_balance_obj.owner(d).name) // ("amount", vesting_balance_obj.balance.amount)); } +#else + const auto& vesting_balances = vesting_index.indices().get(); + for (const vesting_balance_object& vesting_balance_obj : vesting_balances) + { + if (vesting_balance_obj.balance.asset_id == asset_id_type() && vesting_balance_obj.balance.amount) + { + vesting_amounts[vesting_balance_obj.owner] += vesting_balance_obj.balance.amount; + dlog("Vesting balance for account: ${owner}, amount: ${amount}", + ("owner", vesting_balance_obj.owner(d).name) + ("amount", vesting_balance_obj.balance.amount)); + } + } +#endif } void operator()(const account_object& stake_account) { @@ -1262,7 +1296,6 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g auto itr = vesting_amounts.find(stake_account.id); if (itr != vesting_amounts.end()) voting_stake += itr->second.value; - for( vote_id_type id : opinion_account.options.votes ) { uint32_t offset = id.instance(); @@ -1384,12 +1417,13 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g }); // Reset all BitAsset force settlement volumes to zero - for( const asset_bitasset_data_object* d : get_index_type() ) - modify(*d, [](asset_bitasset_data_object& d) { d.force_settled_volume = 0; }); + //for( const asset_bitasset_data_object* d : get_index_type() ) + for( const auto& d : get_index_type().indices() ) + modify( d, [](asset_bitasset_data_object& o) { o.force_settled_volume = 0; }); // process_budget needs to run at the bottom because // it needs to know the next_maintenance_time process_budget(); -} +} FC_CAPTURE_AND_RETHROW() } } } diff --git a/libraries/chain/db_management.cpp b/libraries/chain/db_management.cpp index 6371c61f..6bcee4bd 100644 --- a/libraries/chain/db_management.cpp +++ b/libraries/chain/db_management.cpp @@ -64,10 +64,16 @@ void database::reindex(fc::path data_dir, const genesis_state_type& initial_allo const auto last_block_num = last_block->block_num(); ilog( "Replaying blocks..." ); - _undo_db.disable(); + // Right now, we leave undo_db enabled when replaying when the bookie plugin is + // enabled. It depends on new/changed/removed object notifications, and those are + // only fired when the undo_db is enabled + if (!_slow_replays) + _undo_db.disable(); for( uint32_t i = 1; i <= last_block_num; ++i ) { - if( i % 2000 == 0 ) std::cerr << " " << double(i*100)/last_block_num << "% "< block = _block_id_to_block.fetch_by_number(i); if( !block.valid() ) { @@ -88,14 +94,24 @@ void database::reindex(fc::path data_dir, const genesis_state_type& initial_allo wlog( "Dropped ${n} blocks from after the gap", ("n", dropped_count) ); break; } - apply_block(*block, skip_witness_signature | - skip_transaction_signatures | - skip_transaction_dupe_check | - skip_tapos_check | - skip_witness_schedule_check | - skip_authority_check); + if (_slow_replays) + push_block(*block, skip_fork_db | + skip_witness_signature | + skip_transaction_signatures | + skip_transaction_dupe_check | + skip_tapos_check | + skip_witness_schedule_check | + skip_authority_check); + else + apply_block(*block, skip_witness_signature | + skip_transaction_signatures | + skip_transaction_dupe_check | + skip_tapos_check | + skip_witness_schedule_check | + skip_authority_check); } - _undo_db.enable(); + if (!_slow_replays) + _undo_db.enable(); auto end = fc::time_point::now(); ilog( "Done reindexing, elapsed time: ${t} sec", ("t",double((end-start).count())/1000000.0 ) ); } FC_CAPTURE_AND_RETHROW( (data_dir) ) } @@ -166,8 +182,9 @@ void database::close(bool rewind) } } } - catch (...) + catch ( const fc::exception& e ) { + wlog( "Database close unexpected exception: ${e}", ("e", e) ); } } @@ -185,4 +202,10 @@ void database::close(bool rewind) _fork_db.reset(); } +void database::force_slow_replays() +{ + ilog("enabling slow replays"); + _slow_replays = true; +} + } } diff --git a/libraries/chain/db_market.cpp b/libraries/chain/db_market.cpp index 2c8251ca..59f77762 100644 --- a/libraries/chain/db_market.cpp +++ b/libraries/chain/db_market.cpp @@ -146,7 +146,7 @@ bool maybe_cull_small_order( database& db, const limit_order_object& order ) */ if( order.amount_to_receive().amount == 0 ) { - ilog( "applied epsilon logic" ); + //ilog( "applied epsilon logic" ); db.cancel_order(order); return true; } diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp new file mode 100644 index 00000000..c90bea08 --- /dev/null +++ b/libraries/chain/db_notify.cpp @@ -0,0 +1,444 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace fc; +using namespace graphene::chain; + +// TODO: Review all of these, especially no-ops +struct get_impacted_account_visitor +{ + flat_set& _impacted; + get_impacted_account_visitor( flat_set& impact ):_impacted(impact) {} + typedef void result_type; + + void operator()( const transfer_operation& op ) + { + _impacted.insert( op.to ); + } + + void operator()( const asset_claim_fees_operation& op ){} + void operator()( const limit_order_create_operation& op ) {} + void operator()( const limit_order_cancel_operation& op ) + { + _impacted.insert( op.fee_paying_account ); + } + void operator()( const call_order_update_operation& op ) {} + void operator()( const fill_order_operation& op ) + { + _impacted.insert( op.account_id ); + } + + void operator()( const account_create_operation& op ) + { + _impacted.insert( op.registrar ); + _impacted.insert( op.referrer ); + add_authority_accounts( _impacted, op.owner ); + add_authority_accounts( _impacted, op.active ); + } + + void operator()( const account_update_operation& op ) + { + _impacted.insert( op.account ); + if( op.owner ) + add_authority_accounts( _impacted, *(op.owner) ); + if( op.active ) + add_authority_accounts( _impacted, *(op.active) ); + } + + void operator()( const account_whitelist_operation& op ) + { + _impacted.insert( op.account_to_list ); + } + + void operator()( const account_upgrade_operation& op ) {} + void operator()( const account_transfer_operation& op ) + { + _impacted.insert( op.new_owner ); + } + + void operator()( const asset_create_operation& op ) {} + void operator()( const asset_update_operation& op ) + { + if( op.new_issuer ) + _impacted.insert( *(op.new_issuer) ); + } + + void operator()( const asset_update_bitasset_operation& op ) {} + void operator()( const asset_update_dividend_operation& op ) {} + void operator()( const asset_dividend_distribution_operation& op ) + { + _impacted.insert( op.account_id ); + } + + void operator()( const asset_update_feed_producers_operation& op ) {} + + void operator()( const asset_issue_operation& op ) + { + _impacted.insert( op.issue_to_account ); + } + + void operator()( const asset_reserve_operation& op ) {} + void operator()( const asset_fund_fee_pool_operation& op ) {} + void operator()( const asset_settle_operation& op ) {} + void operator()( const asset_global_settle_operation& op ) {} + void operator()( const asset_publish_feed_operation& op ) {} + void operator()( const witness_create_operation& op ) + { + _impacted.insert( op.witness_account ); + } + void operator()( const witness_update_operation& op ) + { + _impacted.insert( op.witness_account ); + } + + void operator()( const proposal_create_operation& op ) + { + vector other; + for( const auto& proposed_op : op.proposed_ops ) + operation_get_required_authorities( proposed_op.op, _impacted, _impacted, other ); + for( auto& o : other ) + add_authority_accounts( _impacted, o ); + } + + void operator()( const proposal_update_operation& op ) {} + void operator()( const proposal_delete_operation& op ) {} + + void operator()( const withdraw_permission_create_operation& op ) + { + _impacted.insert( op.authorized_account ); + } + + void operator()( const withdraw_permission_update_operation& op ) + { + _impacted.insert( op.authorized_account ); + } + + void operator()( const withdraw_permission_claim_operation& op ) + { + _impacted.insert( op.withdraw_from_account ); + } + + void operator()( const withdraw_permission_delete_operation& op ) + { + _impacted.insert( op.authorized_account ); + } + + void operator()( const committee_member_create_operation& op ) + { + _impacted.insert( op.committee_member_account ); + } + void operator()( const committee_member_update_operation& op ) + { + _impacted.insert( op.committee_member_account ); + } + void operator()( const committee_member_update_global_parameters_operation& op ) {} + + void operator()( const vesting_balance_create_operation& op ) + { + _impacted.insert( op.owner ); + } + + void operator()( const vesting_balance_withdraw_operation& op ) {} + void operator()( const worker_create_operation& op ) {} + void operator()( const custom_operation& op ) {} + void operator()( const assert_operation& op ) {} + void operator()( const balance_claim_operation& op ) {} + + void operator()( const override_transfer_operation& op ) + { + _impacted.insert( op.to ); + _impacted.insert( op.from ); + _impacted.insert( op.issuer ); + } + + void operator()( const transfer_to_blind_operation& op ) + { + _impacted.insert( op.from ); + for( const auto& out : op.outputs ) + add_authority_accounts( _impacted, out.owner ); + } + + void operator()( const blind_transfer_operation& op ) + { + for( const auto& in : op.inputs ) + add_authority_accounts( _impacted, in.owner ); + for( const auto& out : op.outputs ) + add_authority_accounts( _impacted, out.owner ); + } + + void operator()( const transfer_from_blind_operation& op ) + { + _impacted.insert( op.to ); + for( const auto& in : op.inputs ) + add_authority_accounts( _impacted, in.owner ); + } + + void operator()( const asset_settle_cancel_operation& op ) + { + _impacted.insert( op.account ); + } + + void operator()( const fba_distribute_operation& op ) + { + _impacted.insert( op.account_id ); + } + void operator()(const sport_create_operation&){} + void operator()(const sport_update_operation&){} + void operator()(const event_group_create_operation&){} + void operator()(const event_group_update_operation& op ) {} + void operator()(const event_create_operation&){} + void operator()(const event_update_operation& op ) {} + void operator()(const event_update_status_operation& op ) {} + void operator()(const betting_market_rules_create_operation&){} + void operator()(const betting_market_rules_update_operation& op ) {} + void operator()(const betting_market_group_create_operation&){} + void operator()(const betting_market_group_update_operation& op ) {} + void operator()(const betting_market_create_operation&){} + void operator()(const betting_market_update_operation&){} + void operator()(const bet_place_operation&){} + void operator()(const betting_market_group_resolve_operation&){} + void operator()(const betting_market_group_resolved_operation &){} + void operator()(const betting_market_group_cancel_unmatched_bets_operation&){} + void operator()(const bet_matched_operation &){} + void operator()(const bet_cancel_operation&){} + void operator()(const bet_canceled_operation &){} + void operator()(const bet_adjusted_operation &){} + + void operator()( const tournament_create_operation& op ) + { + _impacted.insert( op.creator ); + _impacted.insert( op.options.whitelist.begin(), op.options.whitelist.end() ); + } + void operator()( const tournament_join_operation& op ) + { + _impacted.insert( op.payer_account_id ); + _impacted.insert( op.player_account_id ); + } + void operator()( const tournament_leave_operation& op ) + { + //if account canceling registration is not the player, it must be the payer + if (op.canceling_account_id != op.player_account_id) + _impacted.erase( op.canceling_account_id ); + _impacted.erase( op.player_account_id ); + } + void operator()( const game_move_operation& op ) + { + _impacted.insert( op.player_account_id ); + } + void operator()( const tournament_payout_operation& op ) + { + _impacted.insert( op.payout_account_id ); + } +}; + +void operation_get_impacted_accounts( const operation& op, flat_set& result ) +{ + get_impacted_account_visitor vtor = get_impacted_account_visitor( result ); + op.visit( vtor ); +} + +void transaction_get_impacted_accounts( const transaction& tx, flat_set& result ) +{ + for( const auto& op : tx.operations ) + operation_get_impacted_accounts( op, result ); +} + +void get_relevant_accounts( const object* obj, flat_set& accounts ) +{ + if( obj->id.space() == protocol_ids ) + { + switch( (object_type)obj->id.type() ) + { + case null_object_type: + case base_object_type: + case OBJECT_TYPE_COUNT: + return; + case account_object_type:{ + accounts.insert( obj->id ); + break; + } case asset_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->issuer ); + break; + } case force_settlement_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->owner ); + break; + } case committee_member_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->committee_member_account ); + break; + } case witness_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->witness_account ); + break; + } case limit_order_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->seller ); + break; + } case call_order_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->borrower ); + break; + } case custom_object_type:{ + break; + } case proposal_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + transaction_get_impacted_accounts( aobj->proposed_transaction, accounts ); + break; + } case operation_history_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + operation_get_impacted_accounts( aobj->op, accounts ); + break; + } case withdraw_permission_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->withdraw_from_account ); + accounts.insert( aobj->authorized_account ); + break; + } case vesting_balance_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->owner ); + break; + } case worker_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->worker_account ); + break; + } case balance_object_type:{ + /** these are free from any accounts */ + break; + } + } + } + else if( obj->id.space() == implementation_ids ) + { + switch( (impl_object_type)obj->id.type() ) + { + case impl_global_property_object_type: + break; + case impl_dynamic_global_property_object_type: + break; + case impl_reserved0_object_type: + break; + case impl_asset_dynamic_data_type: + break; + case impl_asset_bitasset_data_type: + break; + case impl_account_balance_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->owner ); + break; + } case impl_account_statistics_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + accounts.insert( aobj->owner ); + break; + } case impl_transaction_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + transaction_get_impacted_accounts( aobj->trx, accounts ); + break; + } case impl_blinded_balance_object_type:{ + const auto& aobj = dynamic_cast(obj); + assert( aobj != nullptr ); + for( const auto& a : aobj->owner.account_auths ) + accounts.insert( a.first ); + break; + } case impl_block_summary_object_type: + break; + case impl_account_transaction_history_object_type: + break; + case impl_chain_property_object_type: + break; + case impl_witness_schedule_object_type: + break; + case impl_budget_record_object_type: + break; + case impl_special_authority_object_type: + break; + case impl_buyback_object_type: + break; + case impl_fba_accumulator_object_type: + break; + } + } +} // end get_relevant_accounts( const object* obj, flat_set& accounts ) + +namespace graphene { namespace chain { + +void database::notify_changed_objects() +{ try { + if( _undo_db.enabled() ) + { + const auto& head_undo = _undo_db.head(); + + // New + if( !new_objects.empty() ) + { + vector new_ids; new_ids.reserve(head_undo.new_ids.size()); + flat_set new_accounts_impacted; + for( const auto& item : head_undo.new_ids ) + { + new_ids.push_back(item); + auto obj = find_object(item); + if(obj != nullptr) + get_relevant_accounts(obj, new_accounts_impacted); + } + + new_objects(new_ids, new_accounts_impacted); + } + + // Changed + if( !changed_objects.empty() ) + { + vector changed_ids; changed_ids.reserve(head_undo.old_values.size()); + flat_set changed_accounts_impacted; + for( const auto& item : head_undo.old_values ) + { + changed_ids.push_back(item.first); + get_relevant_accounts(item.second.get(), changed_accounts_impacted); + } + + changed_objects(changed_ids, changed_accounts_impacted); + } + + // Removed + if( !removed_objects.empty() ) + { + vector removed_ids; removed_ids.reserve( head_undo.removed.size() ); + vector removed; removed.reserve( head_undo.removed.size() ); + flat_set removed_accounts_impacted; + for( const auto& item : head_undo.removed ) + { + removed_ids.emplace_back( item.first ); + auto obj = item.second.get(); + removed.emplace_back( obj ); + get_relevant_accounts(obj, removed_accounts_impacted); + } + + removed_objects(removed_ids, removed, removed_accounts_impacted); + } + } +} FC_CAPTURE_AND_LOG( (0) ) } + +} } diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index 2ef09ecb..ad98837e 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include @@ -188,6 +189,52 @@ void database::clear_expired_transactions() transaction_idx.remove(*dedupe_index.begin()); } FC_CAPTURE_AND_RETHROW() } +void database::place_delayed_bets() +{ try { + // If any bets have been placed during live betting where bets are delayed for a few seconds, see if there are + // any bets whose delays have expired. + + // Delayed bets are sorted to the beginning of the order book, so if there are any bets that need placing, + // they're right at the front of the book + const auto& bet_odds_idx = get_index_type().indices().get(); + auto iter = bet_odds_idx.begin(); + + // we use an awkward looping mechanism here because there's a case where we are processing the + // last delayed bet before the "real" order book starts and `iter` was pointing at the first + // real order. The place_bet() call can cause the that real order to be deleted, so we need + // to decide whether this is the last delayed bet before `place_bet` is called. + bool last = iter == bet_odds_idx.end() || + !iter->end_of_delay || + *iter->end_of_delay > head_block_time(); + while (!last) + { + const bet_object& bet_to_place = *iter; + ++iter; + + last = iter == bet_odds_idx.end() || + !iter->end_of_delay || + *iter->end_of_delay > head_block_time(); + + // it's possible that the betting market was active when the bet was placed, + // but has been frozen before the delay expired. If that's the case here, + // don't try to match the bet. + // Since this check happens every block, this could impact performance if a + // market with many delayed bets is frozen for a long time. + // Our current understanding is that the witnesses will typically cancel all unmatched + // bets on frozen markets to avoid this. + const betting_market_object& betting_market = bet_to_place.betting_market_id(*this); + if (betting_market.get_status() == betting_market_status::unresolved) + { + modify(bet_to_place, [](bet_object& bet_obj) { + // clear the end_of_delay, which will re-sort the bet into its place in the book + bet_obj.end_of_delay.reset(); + }); + + place_bet(bet_to_place); + } + } +} FC_CAPTURE_AND_RETHROW() } + void database::clear_expired_proposals() { const auto& proposal_expiration_index = get_index_type().indices().get(); @@ -608,4 +655,29 @@ void database::update_tournaments() initiate_next_games(*this); } +void process_settled_betting_markets(database& db, fc::time_point_sec current_block_time) +{ + // after a betting market is graded, it goes through a delay period in which it + // can be flagged for re-grading. If it isn't flagged during this interval, + // it is automatically settled (paid). Process these now. + const auto& betting_market_group_index = db.get_index_type().indices().get(); + + // this index will be sorted with all bmgs with no settling time set first, followed by + // ones with the settling time set by increasing time. Start at the first bmg with a time set + auto betting_market_group_iter = betting_market_group_index.upper_bound(fc::optional()); + while (betting_market_group_iter != betting_market_group_index.end() && + *betting_market_group_iter->settling_time <= current_block_time) + { + auto next_iter = std::next(betting_market_group_iter); + db.settle_betting_market_group(*betting_market_group_iter); + betting_market_group_iter = next_iter; + } +} + +void database::update_betting_markets(fc::time_point_sec current_block_time) +{ + process_settled_betting_markets(*this, current_block_time); + remove_completed_events(); +} + } } diff --git a/libraries/chain/evaluator.cpp b/libraries/chain/evaluator.cpp index a4127c25..2ae8f0e7 100644 --- a/libraries/chain/evaluator.cpp +++ b/libraries/chain/evaluator.cpp @@ -128,4 +128,18 @@ database& generic_evaluator::db()const { return trx_state->db(); } db().adjust_balance(fee_payer, fee_from_account); } + object_id_type generic_evaluator::get_relative_id( object_id_type rel_id )const + { + if (!is_relative(rel_id)) + FC_THROW("get_relative_id() called for non-relative id ${id}", ("id", rel_id)); + if (rel_id.instance() >= trx_state->operation_results.size()) + FC_THROW("get_relative_id() asked for id of operation ${op_num} (zero-based), but we only have ${count} operations", + ("op_num", rel_id.instance())("count", trx_state->operation_results.size())); + if (trx_state->operation_results[rel_id.instance()].which() != operation_result::tag::value) + FC_THROW("get_relative_id() asked for the result of operation ${op_num}, but that operation did not return an object_id", + ("op_num", rel_id.instance())); + return trx_state->operation_results[rel_id.instance()].get(); + } + + } } diff --git a/libraries/chain/event_evaluator.cpp b/libraries/chain/event_evaluator.cpp new file mode 100644 index 00000000..8c7d812c --- /dev/null +++ b/libraries/chain/event_evaluator.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + +void_result event_create_evaluator::do_evaluate(const event_create_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + + //database& d = db(); + // the event_group_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly an event_group + object_id_type resolved_event_group_id = op.event_group_id; + if (is_relative(op.event_group_id)) + resolved_event_group_id = get_relative_id(op.event_group_id); + + FC_ASSERT(resolved_event_group_id.space() == event_group_id_type::space_id && + resolved_event_group_id.type() == event_group_id_type::type_id, + "event_group_id must refer to a event_group_id_type"); + event_group_id = resolved_event_group_id; + //const event_group_object& event_group = event_group_id(d); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +object_id_type event_create_evaluator::do_apply(const event_create_operation& op) +{ try { + database& d = db(); + const event_object& new_event = + d.create( [&]( event_object& event_obj ) { + event_obj.name = op.name; + event_obj.season = op.season; + event_obj.start_time = op.start_time; + event_obj.event_group_id = event_group_id; + }); + //increment number of active events in global betting statistics object + const global_betting_statistics_object& betting_statistics = global_betting_statistics_id_type()(d); + d.modify( betting_statistics, [&](global_betting_statistics_object& bso) { + bso.number_of_active_events += 1; + }); + return new_event.id; +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result event_update_evaluator::do_evaluate(const event_update_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + FC_ASSERT(op.new_event_group_id || op.new_name || op.new_season || + op.new_start_time || op.new_status, "nothing to change"); + + if (op.new_event_group_id) + { + object_id_type resolved_event_group_id = *op.new_event_group_id; + if (is_relative(*op.new_event_group_id)) + resolved_event_group_id = get_relative_id(*op.new_event_group_id); + + FC_ASSERT(resolved_event_group_id.space() == event_group_id_type::space_id && + resolved_event_group_id.type() == event_group_id_type::type_id, + "event_group_id must refer to a event_group_id_type"); + event_group_id = resolved_event_group_id; + } + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result event_update_evaluator::do_apply(const event_update_operation& op) +{ try { + database& _db = db(); + _db.modify(_db.get(op.event_id), + [&](event_object& eo) { + if( op.new_name ) + eo.name = *op.new_name; + if( op.new_season ) + eo.season = *op.new_season; + if( op.new_start_time ) + eo.start_time = *op.new_start_time; + if( op.new_event_group_id ) + eo.event_group_id = event_group_id; + if( op.new_status ) + eo.dispatch_new_status(_db, *op.new_status); + }); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result event_update_status_evaluator::do_evaluate(const event_update_status_operation& op) +{ try { + FC_ASSERT(trx_state->_is_proposed_trx); + + database& d = db(); + FC_ASSERT(d.head_block_time() >= HARDFORK_1000_TIME); + //check that the event to update exists + _event_to_update = &op.event_id(d); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result event_update_status_evaluator::do_apply(const event_update_status_operation& op) +{ try { + database& d = db(); + + d.modify( *_event_to_update, [&](event_object& event_obj) { + if (_event_to_update->get_status() != op.status) + event_obj.dispatch_new_status(d, op.status); + event_obj.scores = op.scores; + }); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +} } // graphene::chain diff --git a/libraries/chain/event_group_evaluator.cpp b/libraries/chain/event_group_evaluator.cpp new file mode 100644 index 00000000..1b0d8195 --- /dev/null +++ b/libraries/chain/event_group_evaluator.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + +void_result event_group_create_evaluator::do_evaluate(const event_group_create_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + + // the sport id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly a sport + object_id_type resolved_id = op.sport_id; + if (is_relative(op.sport_id)) + resolved_id = get_relative_id(op.sport_id); + + FC_ASSERT(resolved_id.space() == sport_id_type::space_id && + resolved_id.type() == sport_id_type::type_id, "sport_id must refer to a sport_id_type"); + sport_id = resolved_id; + + FC_ASSERT( db().find_object(sport_id), "Invalid sport specified" ); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +object_id_type event_group_create_evaluator::do_apply(const event_group_create_operation& op) +{ try { + const event_group_object& new_event_group = + db().create( [&]( event_group_object& event_group_obj ) { + event_group_obj.name = op.name; + event_group_obj.sport_id = sport_id; + }); + return new_event_group.id; +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result event_group_update_evaluator::do_evaluate(const event_group_update_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + FC_ASSERT(op.new_sport_id.valid() || op.new_name.valid(), "nothing to change"); + if( op.new_sport_id.valid() ) + { + object_id_type resolved_id = *op.new_sport_id; + if (is_relative(*op.new_sport_id)) + resolved_id = get_relative_id(*op.new_sport_id); + + FC_ASSERT(resolved_id.space() == sport_id_type::space_id && + resolved_id.type() == sport_id_type::type_id, "sport_id must refer to a sport_id_type"); + sport_id = resolved_id; + + FC_ASSERT( db().find_object(sport_id), "invalid sport specified" ); + } + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result event_group_update_evaluator::do_apply(const event_group_update_operation& op) +{ try { + database& _db = db(); + _db.modify( + _db.get(op.event_group_id), + [&]( event_group_object& ego ) + { + if( op.new_name.valid() ) + ego.name = *op.new_name; + if( op.new_sport_id.valid() ) + ego.sport_id = sport_id; + }); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + + +} } // graphene::chain diff --git a/libraries/chain/event_object.cpp b/libraries/chain/event_object.cpp new file mode 100644 index 00000000..753b4b43 --- /dev/null +++ b/libraries/chain/event_object.cpp @@ -0,0 +1,553 @@ +#define DEFAULT_LOGGER "betting" +#define BOOST_MPL_CFG_NO_PREPROCESSED_HEADERS +#define BOOST_MPL_LIMIT_VECTOR_SIZE 30 +#define BOOST_MPL_LIMIT_MAP_SIZE 30 +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + enum class event_state { + upcoming, + frozen_upcoming, + in_progress, + frozen_in_progress, + finished, + canceled, + settled + }; +} } + +FC_REFLECT_ENUM(graphene::chain::event_state, + (upcoming) + (frozen_upcoming) + (in_progress) + (frozen_in_progress) + (finished) + (canceled) + (settled)) + +namespace graphene { namespace chain { + + namespace msm = boost::msm; + namespace mpl = boost::mpl; + + namespace + { + + // Events -- most events happen when the witnesses publish an event_update operation with a new + // status, so if they publish an event with the status set to `frozen`, we'll generate a `frozen_event` + struct upcoming_event + { + database& db; + upcoming_event(database& db) : db(db) {} + }; + struct in_progress_event + { + database& db; + in_progress_event(database& db) : db(db) {} + }; + struct frozen_event + { + database& db; + frozen_event(database& db) : db(db) {} + }; + struct finished_event + { + database& db; + finished_event(database& db) : db(db) {} + }; + struct canceled_event + { + database& db; + canceled_event(database& db) : db(db) {} + }; + + // event triggered when a betting market group in this event is resolved, + // when we get this, check and see if all betting market groups are now + // canceled/settled; if so, transition the event to canceled/settled, + // otherwise remain in the current state + struct betting_market_group_resolved_event + { + database& db; + betting_market_group_id_type resolved_group; + bool was_canceled; + betting_market_group_resolved_event(database& db, betting_market_group_id_type resolved_group, bool was_canceled) : db(db), resolved_group(resolved_group), was_canceled(was_canceled) {} + }; + + // event triggered when a betting market group is closed. When we get this, + // if all child betting market groups are closed, transition to finished + struct betting_market_group_closed_event + { + database& db; + betting_market_group_id_type closed_group; + betting_market_group_closed_event(database& db, betting_market_group_id_type closed_group) : db(db), closed_group(closed_group) {} + }; + + // Events + struct event_state_machine_ : public msm::front::state_machine_def + + { + // disable a few state machine features we don't use for performance + typedef int no_exception_thrown; + typedef int no_message_queue; + + // States + struct upcoming : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + void on_entry(const upcoming_event& event, event_state_machine_& fsm) { + dlog("event ${id} -> upcoming", ("id", fsm.event_obj->id)); + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(fsm.event_obj->id))) + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_upcoming_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't freeze the group. just ignore the exception + } + } + }; + struct in_progress : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + void on_entry(const in_progress_event& event, event_state_machine_& fsm) { + dlog("event ${id} -> in_progress", ("id", fsm.event_obj->id)); + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(fsm.event_obj->id))) + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_in_play_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't freeze the group. just ignore the exception + } + } + }; + struct frozen_upcoming : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> frozen_upcoming", ("id", fsm.event_obj->id)); + } + }; + struct frozen_in_progress : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> frozen_in_progress", ("id", fsm.event_obj->id)); + } + }; + struct finished : public msm::front::state<> { + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> finished", ("id", fsm.event_obj->id)); + } + }; + struct settled : public msm::front::state<>{ + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> settled", ("id", fsm.event_obj->id)); + } + }; + struct canceled : public msm::front::state<> { + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> canceled", ("id", fsm.event_obj->id)); + } + }; + + // actions + void record_whether_group_settled_or_canceled(const betting_market_group_resolved_event& event) { + if (!event.was_canceled) + event_obj->at_least_one_betting_market_group_settled = true; + } + + void freeze_betting_market_groups(const frozen_event& event) { + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + { + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_frozen_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't freeze the group. just ignore the exception + } + } + } + + void close_all_betting_market_groups(const finished_event& event) { + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + { + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_closed_event(event.db, true); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't close the group. just ignore the exception + } + } + } + + void cancel_all_betting_market_groups(const canceled_event& event) { + auto& betting_market_group_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_canceled_event(event.db, true); + }); + } + + // Guards + bool all_betting_market_groups_are_closed(const betting_market_group_closed_event& event) + { + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + if (betting_market_group.id != event.closed_group) + { + betting_market_group_status status = betting_market_group.get_status(); + if (status != betting_market_group_status::closed && + status != betting_market_group_status::graded && + status != betting_market_group_status::re_grading && + status != betting_market_group_status::settled && + status != betting_market_group_status::canceled) + return false; + } + return true; + } + + bool all_betting_market_groups_are_canceled(const betting_market_group_resolved_event& event) + { + // if the bmg that just resolved was settled, obviously all didn't cancel + if (!event.was_canceled) + return false; + // if a previously-resolved group was settled, all didn't cancel + if (event_obj->at_least_one_betting_market_group_settled) + return false; + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + if (betting_market_group.id != event.resolved_group) + if (betting_market_group.get_status() != betting_market_group_status::canceled) + return false; + return true; + } + + bool all_betting_market_groups_are_resolved(const betting_market_group_resolved_event& event) + { + if (!event.was_canceled) + event_obj->at_least_one_betting_market_group_settled = true; + + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) { + if (betting_market_group.id != event.resolved_group) { + betting_market_group_status status = betting_market_group.get_status(); + if (status != betting_market_group_status::canceled && status != betting_market_group_status::settled) + return false; + } + } + return true; + } + + typedef upcoming initial_state; + typedef event_state_machine_ x; // makes transition table cleaner + + // Transition table for tournament + struct transition_table : mpl::vector< + // Start Event Next Action Guard + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + _row < upcoming, in_progress_event, in_progress >, + a_row< upcoming, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< upcoming, frozen_event, frozen_upcoming, &x::freeze_betting_market_groups >, + a_row< upcoming, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< upcoming, betting_market_group_resolved_event,upcoming, &x::record_whether_group_settled_or_canceled >, + g_row< upcoming, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + _row < frozen_upcoming, upcoming_event, upcoming >, + _row < frozen_upcoming, in_progress_event, in_progress >, + a_row< frozen_upcoming, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< frozen_upcoming, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< frozen_upcoming, betting_market_group_resolved_event,frozen_upcoming, &x::record_whether_group_settled_or_canceled >, + g_row< frozen_upcoming, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + a_row< in_progress, frozen_event, frozen_in_progress, &x::freeze_betting_market_groups >, + a_row< in_progress, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< in_progress, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< in_progress, betting_market_group_resolved_event,in_progress, &x::record_whether_group_settled_or_canceled >, + g_row< in_progress, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + _row < frozen_in_progress, in_progress_event, in_progress >, + a_row< frozen_in_progress, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< frozen_in_progress, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< frozen_in_progress, betting_market_group_resolved_event,frozen_in_progress, &x::record_whether_group_settled_or_canceled >, + g_row< frozen_in_progress, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + a_row< finished, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + g_row< finished, betting_market_group_resolved_event,settled, &x::all_betting_market_groups_are_resolved >, + g_row< finished, betting_market_group_resolved_event,canceled, &x::all_betting_market_groups_are_canceled > + > {}; + + template + void no_transition(Event const& e, Fsm& ,int state) + { + FC_THROW_EXCEPTION(graphene::chain::no_transition, "No transition"); + } + + event_object* event_obj; + event_state_machine_(event_object* event_obj) : event_obj(event_obj) {} + }; + typedef msm::back::state_machine event_state_machine; + + } // end anonymous namespace + + class event_object::impl { + public: + event_state_machine state_machine; + + impl(event_object* self) : state_machine(self) {} + }; + + event_object::event_object() : + at_least_one_betting_market_group_settled(false), + my(new impl(this)) + { + } + + event_object::event_object(const event_object& rhs) : + graphene::db::abstract_object(rhs), + name(rhs.name), + season(rhs.season), + start_time(rhs.start_time), + event_group_id(rhs.event_group_id), + at_least_one_betting_market_group_settled(rhs.at_least_one_betting_market_group_settled), + scores(rhs.scores), + my(new impl(this)) + { + my->state_machine = rhs.my->state_machine; + my->state_machine.event_obj = this; + } + + event_object& event_object::operator=(const event_object& rhs) + { + //graphene::db::abstract_object::operator=(rhs); + id = rhs.id; + name = rhs.name; + season = rhs.season; + start_time = rhs.start_time; + event_group_id = rhs.event_group_id; + at_least_one_betting_market_group_settled = rhs.at_least_one_betting_market_group_settled; + scores = rhs.scores; + + my->state_machine = rhs.my->state_machine; + my->state_machine.event_obj = this; + + return *this; + } + + event_object::~event_object() + { + } + + namespace { + + bool verify_event_status_constants() + { + unsigned error_count = 0; + typedef msm::back::generate_state_set::type all_states; + static char const* filled_state_names[mpl::size::value]; + mpl::for_each > + (msm::back::fill_state_names(filled_state_names)); + for (unsigned i = 0; i < mpl::size::value; ++i) + { + try + { + // this is an approximate test, the state name provided by typeinfo will be mangled, but should + // at least contain the string we're looking for + const char* fc_reflected_value_name = fc::reflector::to_string((event_state)i); + if (!strstr(filled_state_names[i], fc_reflected_value_name)) + { + fc_elog(fc::logger::get("default"), + "Error, state string mismatch between fc and boost::msm for int value ${int_value}: boost::msm -> ${boost_string}, fc::reflect -> ${fc_string}", + ("int_value", i)("boost_string", filled_state_names[i])("fc_string", fc_reflected_value_name)); + ++error_count; + } + } + catch (const fc::bad_cast_exception&) + { + fc_elog(fc::logger::get("default"), + "Error, no reflection for value ${int_value} in enum event_status", + ("int_value", i)); + ++error_count; + } + } + if (error_count == 0) + dlog("Event status constants are correct"); + else + wlog("There were ${count} errors in the event status constants", ("count", error_count)); + + return error_count == 0; + } + } // end anonymous namespace + + event_status event_object::get_status() const + { + static bool state_constants_are_correct = verify_event_status_constants(); + (void)&state_constants_are_correct; + event_state state = (event_state)my->state_machine.current_state()[0]; + + ddump((state)); + + switch (state) + { + case event_state::upcoming: + return event_status::upcoming; + case event_state::frozen_upcoming: + case event_state::frozen_in_progress: + return event_status::frozen; + case event_state::in_progress: + return event_status::in_progress; + case event_state::finished: + return event_status::finished; + case event_state::canceled: + return event_status::canceled; + case event_state::settled: + return event_status::settled; + default: + FC_THROW("Unexpected event state"); + }; + } + + void event_object::pack_impl(std::ostream& stream) const + { + boost::archive::binary_oarchive oa(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + oa << my->state_machine; + } + + void event_object::unpack_impl(std::istream& stream) + { + boost::archive::binary_iarchive ia(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + ia >> my->state_machine; + } + + void event_object::on_upcoming_event(database& db) + { + my->state_machine.process_event(upcoming_event(db)); + } + + void event_object::on_in_progress_event(database& db) + { + my->state_machine.process_event(in_progress_event(db)); + } + + void event_object::on_frozen_event(database& db) + { + my->state_machine.process_event(frozen_event(db)); + } + + void event_object::on_finished_event(database& db) + { + my->state_machine.process_event(finished_event(db)); + } + + void event_object::on_canceled_event(database& db) + { + my->state_machine.process_event(canceled_event(db)); + } + + void event_object::on_betting_market_group_resolved(database& db, betting_market_group_id_type resolved_group, bool was_canceled) + { + my->state_machine.process_event(betting_market_group_resolved_event(db, resolved_group, was_canceled)); + } + + void event_object::on_betting_market_group_closed(database& db, betting_market_group_id_type closed_group) + { + my->state_machine.process_event(betting_market_group_closed_event(db, closed_group)); + } + + // These are the only statuses that can be explicitly set by witness operations. The missing + // status, 'settled', is automatically set when all of the betting market groups have + // settled/canceled + void event_object::dispatch_new_status(database& db, event_status new_status) + { + switch (new_status) { + case event_status::upcoming: // by witnesses to unfreeze a frozen event + on_upcoming_event(db); + break; + case event_status::in_progress: // by witnesses when the event starts + on_in_progress_event(db); + break; + case event_status::frozen: // by witnesses when the event needs to be frozen + on_frozen_event(db); + break; + case event_status::finished: // by witnesses when the event is complete + on_finished_event(db); + break; + case event_status::canceled: // by witnesses to cancel the event + on_canceled_event(db); + break; + default: + FC_THROW("Status ${new_status} cannot be explicitly set", ("new_status", new_status)); + } + } + +} } // graphene::chain + +namespace fc { + // Manually reflect event_object to variant to properly reflect "state" + void to_variant(const graphene::chain::event_object& event_obj, fc::variant& v) + { + fc::mutable_variant_object o; + o("id", event_obj.id) + ("name", event_obj.name) + ("season", event_obj.season) + ("start_time", event_obj.start_time) + ("event_group_id", event_obj.event_group_id) + ("scores", event_obj.scores) + ("status", event_obj.get_status()); + + v = o; + } + + // Manually reflect event_object to variant to properly reflect "state" + void from_variant(const fc::variant& v, graphene::chain::event_object& event_obj) + { + event_obj.id = v["id"].as(); + event_obj.name = v["name"].as(); + event_obj.season = v["season"].as(); + event_obj.start_time = v["start_time"].as >(); + event_obj.event_group_id = v["event_group_id"].as(); + event_obj.scores = v["scores"].as>(); + graphene::chain::event_status status = v["status"].as(); + const_cast(event_obj.my->state_machine.current_state())[0] = (int)status; + } +} //end namespace fc + + diff --git a/libraries/chain/hardfork.d/1000.hf b/libraries/chain/hardfork.d/1000.hf new file mode 100644 index 00000000..a07928d0 --- /dev/null +++ b/libraries/chain/hardfork.d/1000.hf @@ -0,0 +1,4 @@ +// #615 Fix price feed expiration check, so websocket server will never spam too much data +#ifndef HARDFORK_1000_TIME +#define HARDFORK_1000_TIME (fc::time_point_sec( 1523534400 )) +#endif diff --git a/libraries/chain/hardfork.d/599.hf b/libraries/chain/hardfork.d/599.hf index 71f7e94e..6249101d 100644 --- a/libraries/chain/hardfork.d/599.hf +++ b/libraries/chain/hardfork.d/599.hf @@ -1,4 +1,4 @@ // #599 Unpacking of extension is incorrect #ifndef HARDFORK_599_TIME -#define HARDFORK_599_TIME (fc::time_point_sec( 1458061200 )) +#define HARDFORK_599_TIME (fc::time_point_sec( 1459789200 )) #endif diff --git a/libraries/chain/hardfork.d/607.hf b/libraries/chain/hardfork.d/607.hf index 135619c2..77c8c9e0 100644 --- a/libraries/chain/hardfork.d/607.hf +++ b/libraries/chain/hardfork.d/607.hf @@ -1,4 +1,4 @@ // #607 Disable negative voting on workers #ifndef HARDFORK_607_TIME -#define HARDFORK_607_TIME (fc::time_point_sec( 1458061200 )) +#define HARDFORK_607_TIME (fc::time_point_sec( 1458752400 )) #endif diff --git a/libraries/chain/hardfork.d/613.hf b/libraries/chain/hardfork.d/613.hf index 74c740fb..9978d33c 100644 --- a/libraries/chain/hardfork.d/613.hf +++ b/libraries/chain/hardfork.d/613.hf @@ -1,4 +1,4 @@ // #613 Deprecate annual membership #ifndef HARDFORK_613_TIME -#define HARDFORK_613_TIME (fc::time_point_sec( 1458061200 )) +#define HARDFORK_613_TIME (fc::time_point_sec( 1458752400 )) #endif diff --git a/libraries/chain/hardfork.d/615.hf b/libraries/chain/hardfork.d/615.hf index a5599bbc..ac0535dc 100644 --- a/libraries/chain/hardfork.d/615.hf +++ b/libraries/chain/hardfork.d/615.hf @@ -1,4 +1,4 @@ // #615 Fix price feed expiration check, so websocket server will never spam too much data #ifndef HARDFORK_615_TIME -#define HARDFORK_615_TIME (fc::time_point_sec( 1457550000 )) +#define HARDFORK_615_TIME (fc::time_point_sec( 1458752400 )) #endif diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index 9f51de7c..333168a8 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -50,7 +50,10 @@ namespace graphene { namespace chain { * Keep the most recent operation as a root pointer to a linked list of the transaction history. */ account_transaction_history_id_type most_recent_op; + /** Total operations related to this account. */ uint32_t total_ops = 0; + /** Total operations related to this account that has been removed from the database. */ + uint32_t removed_ops = 0; /** * When calculating votes it is necessary to know how much is stored in orders (and thus unavailable for @@ -454,7 +457,7 @@ FC_REFLECT_DERIVED( graphene::chain::account_statistics_object, (graphene::chain::object), (owner) (most_recent_op) - (total_ops) + (total_ops)(removed_ops) (total_core_in_orders) (lifetime_fees_paid) (pending_fees)(pending_vested_fees) diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index 3e2fd24d..d56a41a7 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -235,15 +235,18 @@ namespace graphene { namespace chain { > > > asset_bitasset_data_object_multi_index_type; - typedef flat_index asset_bitasset_data_index; + //typedef flat_index asset_bitasset_data_index; + typedef generic_index asset_bitasset_data_index; struct by_symbol; struct by_type; + struct by_issuer; typedef multi_index_container< asset_object, indexed_by< ordered_unique< tag, member< object, object_id_type, &object::id > >, ordered_unique< tag, member >, + ordered_non_unique< tag, member >, ordered_unique< tag, composite_key< asset_object, const_mem_fun, diff --git a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp new file mode 100644 index 00000000..9967979d --- /dev/null +++ b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace graphene { namespace chain { + + class betting_market_rules_create_evaluator : public evaluator + { + public: + typedef betting_market_rules_create_operation operation_type; + + void_result do_evaluate( const betting_market_rules_create_operation& o ); + object_id_type do_apply( const betting_market_rules_create_operation& o ); + }; + + class betting_market_rules_update_evaluator : public evaluator + { + public: + typedef betting_market_rules_update_operation operation_type; + + void_result do_evaluate( const betting_market_rules_update_operation& o ); + void_result do_apply( const betting_market_rules_update_operation& o ); + private: + const betting_market_rules_object* _rules; + }; + + class betting_market_group_create_evaluator : public evaluator + { + public: + typedef betting_market_group_create_operation operation_type; + + void_result do_evaluate(const betting_market_group_create_operation& o); + object_id_type do_apply(const betting_market_group_create_operation& o); + private: + event_id_type _event_id; + betting_market_rules_id_type _rules_id; + }; + + class betting_market_group_update_evaluator : public evaluator + { + public: + typedef betting_market_group_update_operation operation_type; + + void_result do_evaluate(const betting_market_group_update_operation& o); + void_result do_apply(const betting_market_group_update_operation& o); + private: + betting_market_rules_id_type _rules_id; + const betting_market_group_object* _betting_market_group; + }; + + class betting_market_create_evaluator : public evaluator + { + public: + typedef betting_market_create_operation operation_type; + + void_result do_evaluate( const betting_market_create_operation& o ); + object_id_type do_apply( const betting_market_create_operation& o ); + private: + betting_market_group_id_type _group_id; + }; + + class betting_market_update_evaluator : public evaluator + { + public: + typedef betting_market_update_operation operation_type; + + void_result do_evaluate( const betting_market_update_operation& o ); + void_result do_apply( const betting_market_update_operation& o ); + private: + const betting_market_object* _betting_market; + betting_market_group_id_type _group_id; + }; + + class bet_place_evaluator : public evaluator + { + public: + typedef bet_place_operation operation_type; + + void_result do_evaluate( const bet_place_operation& o ); + object_id_type do_apply( const bet_place_operation& o ); + private: + const betting_market_group_object* _betting_market_group; + const betting_market_object* _betting_market; + const chain_parameters* _current_params; + const asset_object* _asset; + share_type _stake_plus_fees; + }; + + class bet_cancel_evaluator : public evaluator + { + public: + typedef bet_cancel_operation operation_type; + + void_result do_evaluate( const bet_cancel_operation& o ); + void_result do_apply( const bet_cancel_operation& o ); + private: + const bet_object* _bet_to_cancel; + }; + + class betting_market_group_resolve_evaluator : public evaluator + { + public: + typedef betting_market_group_resolve_operation operation_type; + + void_result do_evaluate( const betting_market_group_resolve_operation& o ); + void_result do_apply( const betting_market_group_resolve_operation& o ); + private: + const betting_market_group_object* _betting_market_group; + }; + + class betting_market_group_cancel_unmatched_bets_evaluator : public evaluator + { + public: + typedef betting_market_group_cancel_unmatched_bets_operation operation_type; + + void_result do_evaluate( const betting_market_group_cancel_unmatched_bets_operation& o ); + void_result do_apply( const betting_market_group_cancel_unmatched_bets_operation& o ); + private: + const betting_market_group_object* _betting_market_group; + }; + + +} } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/betting_market_object.hpp b/libraries/chain/include/graphene/chain/betting_market_object.hpp new file mode 100644 index 00000000..4f6665c8 --- /dev/null +++ b/libraries/chain/include/graphene/chain/betting_market_object.hpp @@ -0,0 +1,713 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include +#include + +#include + +namespace graphene { namespace chain { + class betting_market_object; + class betting_market_group_object; +} } + +namespace fc { + void to_variant(const graphene::chain::betting_market_object& betting_market_obj, fc::variant& v); + void from_variant(const fc::variant& v, graphene::chain::betting_market_object& betting_market_obj); + void to_variant(const graphene::chain::betting_market_group_object& betting_market_group_obj, fc::variant& v); + void from_variant(const fc::variant& v, graphene::chain::betting_market_group_object& betting_market_group_obj); +} //end namespace fc + +namespace graphene { namespace chain { + +FC_DECLARE_EXCEPTION(no_transition, 100000, "Invalid state transition"); +class database; + +struct by_event_id; +struct by_settling_time; +struct by_betting_market_group_id; + +class betting_market_rules_object : public graphene::db::abstract_object< betting_market_rules_object > +{ + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = betting_market_rules_object_type; + + internationalized_string_type name; + + internationalized_string_type description; +}; + +class betting_market_group_object : public graphene::db::abstract_object< betting_market_group_object > +{ + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = betting_market_group_object_type; + + betting_market_group_object(); + betting_market_group_object(const betting_market_group_object& rhs); + ~betting_market_group_object(); + betting_market_group_object& operator=(const betting_market_group_object& rhs); + + internationalized_string_type description; + + event_id_type event_id; + + betting_market_rules_id_type rules_id; + + asset_id_type asset_id; + + share_type total_matched_bets_amount; + + bool never_in_play; + + uint32_t delay_before_settling; + + fc::optional settling_time; // the time the payout will occur (set after grading) + + bool bets_are_allowed() const { + return get_status() == betting_market_group_status::upcoming || + get_status() == betting_market_group_status::in_play; + } + + bool bets_are_delayed() const { + return get_status() == betting_market_group_status::in_play; + } + + betting_market_group_status get_status() const; + + // serialization functions: + // for serializing to raw, go through a temporary sstream object to avoid + // having to implement serialization in the header file + template + friend Stream& operator<<( Stream& s, const betting_market_group_object& betting_market_group_obj ); + + template + friend Stream& operator>>( Stream& s, betting_market_group_object& betting_market_group_obj ); + + friend void ::fc::to_variant(const graphene::chain::betting_market_group_object& betting_market_group_obj, fc::variant& v); + friend void ::fc::from_variant(const fc::variant& v, graphene::chain::betting_market_group_object& betting_market_group_obj); + + void pack_impl(std::ostream& stream) const; + void unpack_impl(std::istream& stream); + + void on_upcoming_event(database& db); + void on_in_play_event(database& db); + void on_frozen_event(database& db); + void on_closed_event(database& db, bool closed_by_event); + void on_graded_event(database& db); + void on_re_grading_event(database& db); + void on_settled_event(database& db); + void on_canceled_event(database& db, bool canceled_by_event); + void dispatch_new_status(database& db, betting_market_group_status new_status); + + private: + class impl; + std::unique_ptr my; +}; + +class betting_market_object : public graphene::db::abstract_object< betting_market_object > +{ + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = betting_market_object_type; + + betting_market_object(); + betting_market_object(const betting_market_object& rhs); + ~betting_market_object(); + betting_market_object& operator=(const betting_market_object& rhs); + + betting_market_group_id_type group_id; + + internationalized_string_type description; + + internationalized_string_type payout_condition; + + // once the market is graded, this holds the proposed grading + // after settling/canceling, this is the actual grading + fc::optional resolution; + + betting_market_status get_status() const; + + void cancel_all_unmatched_bets(database& db) const; + + // serialization functions: + // for serializing to raw, go through a temporary sstream object to avoid + // having to implement serialization in the header file + template + friend Stream& operator<<( Stream& s, const betting_market_object& betting_market_obj ); + + template + friend Stream& operator>>( Stream& s, betting_market_object& betting_market_obj ); + + friend void ::fc::to_variant(const graphene::chain::betting_market_object& betting_market_obj, fc::variant& v); + friend void ::fc::from_variant(const fc::variant& v, graphene::chain::betting_market_object& betting_market_obj); + + void pack_impl(std::ostream& stream) const; + void unpack_impl(std::istream& stream); + + void on_unresolved_event(database& db); + void on_frozen_event(database& db); + void on_closed_event(database& db); + void on_graded_event(database& db, betting_market_resolution_type new_grading); + void on_settled_event(database& db); + void on_canceled_event(database& db); + private: + class impl; + std::unique_ptr my; +}; + +class bet_object : public graphene::db::abstract_object< bet_object > +{ + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = bet_object_type; + + account_id_type bettor_id; + + betting_market_id_type betting_market_id; + + asset amount_to_bet; + + bet_multiplier_type backer_multiplier; + + bet_type back_or_lay; + + fc::optional end_of_delay; + + static share_type get_approximate_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay, bool round_up = false); + + // returns the amount of a bet that completely matches this bet + share_type get_approximate_matching_amount(bool round_up = false) const; + + static share_type get_exact_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay); + share_type get_exact_matching_amount() const; + + static std::pair get_ratio(bet_multiplier_type backer_multiplier); + std::pair get_ratio() const; + + // returns the minimum amount this bet could have that could be matched at these odds + share_type get_minimum_matchable_amount() const; + // returns the minimum amount another user could bet to match this bet at these odds + share_type get_minimum_matching_amount() const; +}; + +class betting_market_position_object : public graphene::db::abstract_object< betting_market_position_object > +{ + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_betting_market_position_object_type; + + account_id_type bettor_id; + + betting_market_id_type betting_market_id; + + share_type pay_if_payout_condition; + share_type pay_if_not_payout_condition; + share_type pay_if_canceled; + share_type pay_if_not_canceled; + share_type fees_collected; + + share_type reduce(); +}; + +typedef multi_index_container< + betting_market_rules_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > > + > > betting_market_rules_object_multi_index_type; +typedef generic_index betting_market_rules_object_index; + +typedef multi_index_container< + betting_market_group_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< tag, member >, + ordered_non_unique< tag, member, &betting_market_group_object::settling_time> > + > > betting_market_group_object_multi_index_type; +typedef generic_index betting_market_group_object_index; + +typedef multi_index_container< + betting_market_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< tag, member > + > > betting_market_object_multi_index_type; + +typedef generic_index betting_market_object_index; + +struct compare_bet_by_odds { + bool operator()(const bet_object& lhs, const bet_object& rhs) const + { + return compare(lhs.end_of_delay, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + rhs.end_of_delay, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(fc::optional(), std::get<0>(lhs), rhs.end_of_delay, rhs.betting_market_id); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.end_of_delay, lhs.betting_market_id, fc::optional(), std::get<0>(rhs)); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(fc::optional(), std::get<0>(lhs), std::get<1>(lhs), rhs.end_of_delay, rhs.betting_market_id, rhs.back_or_lay); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.end_of_delay, lhs.betting_market_id, lhs.back_or_lay, fc::optional(), std::get<0>(rhs), std::get<1>(rhs)); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(fc::optional(), std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), + rhs.end_of_delay, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.end_of_delay, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, + fc::optional(), std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs)); + } + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(fc::optional(), std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), std::get<3>(lhs), + rhs.end_of_delay, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.end_of_delay, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + fc::optional(), std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs), std::get<3>(rhs)); + } + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, + const fc::optional& rhs_end_of_delay, + const betting_market_id_type& rhs_betting_market_id) const + { + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + return *lhs_end_of_delay < *rhs_end_of_delay; + } + + return lhs_betting_market_id < rhs_betting_market_id; + } + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + const fc::optional& rhs_end_of_delay, + const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type) const + { + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + return *lhs_end_of_delay < *rhs_end_of_delay; + } + + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + return lhs_bet_type < rhs_bet_type; + } + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + bet_multiplier_type lhs_backer_multiplier, + const fc::optional& rhs_end_of_delay, + const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type, + bet_multiplier_type rhs_backer_multiplier) const + { + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + return *lhs_end_of_delay < *rhs_end_of_delay; + } + + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + if (lhs_bet_type < rhs_bet_type) + return true; + if (lhs_bet_type > rhs_bet_type) + return false; + if (lhs_bet_type == bet_type::back) + return lhs_backer_multiplier < rhs_backer_multiplier; + else + return lhs_backer_multiplier > rhs_backer_multiplier; + } + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + bet_multiplier_type lhs_backer_multiplier, const bet_id_type& lhs_bet_id, + const fc::optional& rhs_end_of_delay, + const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type, + bet_multiplier_type rhs_backer_multiplier, const bet_id_type& rhs_bet_id) const + { + + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + if (*lhs_end_of_delay < *rhs_end_of_delay) + return true; + if (*lhs_end_of_delay > *rhs_end_of_delay) + return false; + // if both bets have the same delay, prefer the one + // that was placed first (lowest id) + return lhs_bet_id < rhs_bet_id; + } + + // if neither bet was delayed + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + if (lhs_bet_type < rhs_bet_type) + return true; + if (lhs_bet_type > rhs_bet_type) + return false; + if (lhs_backer_multiplier < rhs_backer_multiplier) + return lhs_bet_type == bet_type::back; + if (lhs_backer_multiplier > rhs_backer_multiplier) + return lhs_bet_type == bet_type::lay; + return lhs_bet_id < rhs_bet_id; + } +}; + +struct compare_bet_by_bettor_then_odds { + bool operator()(const bet_object& lhs, const bet_object& rhs) const + { + return compare(lhs.bettor_id, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + rhs.bettor_id, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), rhs.bettor_id); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.bettor_id, std::get<0>(rhs)); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), std::get<1>(lhs), rhs.bettor_id, rhs.betting_market_id); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.bettor_id, lhs.betting_market_id, std::get<0>(rhs), std::get<1>(rhs)); + } + + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), + rhs.bettor_id, rhs.betting_market_id, rhs.back_or_lay); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.bettor_id, lhs.betting_market_id, lhs.back_or_lay, + std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs)); + } + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), std::get<3>(lhs), + rhs.bettor_id, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier); + } + + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.bettor_id, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs), std::get<3>(rhs)); + } + template + bool operator() (const std::tuple& lhs, const bet_object& rhs) const + { + return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), std::get<3>(lhs), std::get<4>(lhs), + rhs.bettor_id, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); + } + template + bool operator() (const bet_object& lhs, const std::tuple& rhs) const + { + return compare(lhs.bettor_id, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs), std::get<3>(rhs), std::get<4>(rhs)); + } + bool compare(const account_id_type& lhs_bettor_id, const account_id_type& rhs_bettor_id) const + { + return lhs_bettor_id < rhs_bettor_id; + } + bool compare(const account_id_type& lhs_bettor_id, const betting_market_id_type& lhs_betting_market_id, + const account_id_type& rhs_bettor_id, const betting_market_id_type& rhs_betting_market_id) const + { + if (lhs_bettor_id < rhs_bettor_id) + return true; + if (lhs_bettor_id > rhs_bettor_id) + return false; + return lhs_betting_market_id < rhs_betting_market_id; + } + + bool compare(const account_id_type& lhs_bettor_id, const betting_market_id_type& lhs_betting_market_id, + bet_type lhs_bet_type, + const account_id_type& rhs_bettor_id, const betting_market_id_type& rhs_betting_market_id, + bet_type rhs_bet_type) const + { + if (lhs_bettor_id < rhs_bettor_id) + return true; + if (lhs_bettor_id > rhs_bettor_id) + return false; + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + return lhs_bet_type < rhs_bet_type; + } + bool compare(const account_id_type& lhs_bettor_id, const betting_market_id_type& lhs_betting_market_id, + bet_type lhs_bet_type, + bet_multiplier_type lhs_backer_multiplier, + const account_id_type& rhs_bettor_id, const betting_market_id_type& rhs_betting_market_id, + bet_type rhs_bet_type, + bet_multiplier_type rhs_backer_multiplier) const + { + if (lhs_bettor_id < rhs_bettor_id) + return true; + if (lhs_bettor_id > rhs_bettor_id) + return false; + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + if (lhs_bet_type < rhs_bet_type) + return true; + if (lhs_bet_type > rhs_bet_type) + return false; + if (lhs_bet_type == bet_type::back) + return lhs_backer_multiplier < rhs_backer_multiplier; + else + return lhs_backer_multiplier > rhs_backer_multiplier; + } + bool compare(const account_id_type& lhs_bettor_id, const betting_market_id_type& lhs_betting_market_id, + bet_type lhs_bet_type, + bet_multiplier_type lhs_backer_multiplier, const bet_id_type& lhs_bet_id, + const account_id_type& rhs_bettor_id, const betting_market_id_type& rhs_betting_market_id, + bet_type rhs_bet_type, + bet_multiplier_type rhs_backer_multiplier, const bet_id_type& rhs_bet_id) const + { + if (lhs_bettor_id < rhs_bettor_id) + return true; + if (lhs_bettor_id > rhs_bettor_id) + return false; + if (lhs_betting_market_id < rhs_betting_market_id) + return true; + if (lhs_betting_market_id > rhs_betting_market_id) + return false; + if (lhs_bet_type < rhs_bet_type) + return true; + if (lhs_bet_type > rhs_bet_type) + return false; + if (lhs_backer_multiplier < rhs_backer_multiplier) + return lhs_bet_type == bet_type::back; + if (lhs_backer_multiplier > rhs_backer_multiplier) + return lhs_bet_type == bet_type::lay; + + return lhs_bet_id < rhs_bet_id; + } +}; + +struct by_odds {}; +struct by_bettor_and_odds {}; +typedef multi_index_container< + bet_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, identity, compare_bet_by_odds >, + ordered_unique< tag, identity, compare_bet_by_bettor_then_odds > > > bet_object_multi_index_type; +typedef generic_index bet_object_index; + +struct by_bettor_betting_market{}; +struct by_betting_market_bettor{}; +typedef multi_index_container< + betting_market_position_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, + composite_key< + betting_market_position_object, + member, + member > >, + ordered_unique< tag, + composite_key< + betting_market_position_object, + member, + member > > + > > betting_market_position_multi_index_type; + +typedef generic_index betting_market_position_index; + + +template +inline Stream& operator<<( Stream& s, const betting_market_object& betting_market_obj ) +{ + // pack all fields exposed in the header in the usual way + // instead of calling the derived pack, just serialize the one field in the base class + // fc::raw::pack >(s, betting_market_obj); + fc::raw::pack(s, betting_market_obj.id); + fc::raw::pack(s, betting_market_obj.group_id); + fc::raw::pack(s, betting_market_obj.description); + fc::raw::pack(s, betting_market_obj.payout_condition); + fc::raw::pack(s, betting_market_obj.resolution); + + // fc::raw::pack the contents hidden in the impl class + std::ostringstream stream; + betting_market_obj.pack_impl(stream); + std::string stringified_stream(stream.str()); + fc::raw::pack(s, stream.str()); + + return s; +} +template +inline Stream& operator>>( Stream& s, betting_market_object& betting_market_obj ) +{ + // unpack all fields exposed in the header in the usual way + //fc::raw::unpack >(s, betting_market_obj); + fc::raw::unpack(s, betting_market_obj.id); + fc::raw::unpack(s, betting_market_obj.group_id); + fc::raw::unpack(s, betting_market_obj.description); + fc::raw::unpack(s, betting_market_obj.payout_condition); + fc::raw::unpack(s, betting_market_obj.resolution); + + // fc::raw::unpack the contents hidden in the impl class + std::string stringified_stream; + fc::raw::unpack(s, stringified_stream); + std::istringstream stream(stringified_stream); + betting_market_obj.unpack_impl(stream); + + return s; +} + + +template +inline Stream& operator<<( Stream& s, const betting_market_group_object& betting_market_group_obj ) +{ + // pack all fields exposed in the header in the usual way + // instead of calling the derived pack, just serialize the one field in the base class + // fc::raw::pack >(s, betting_market_group_obj); + fc::raw::pack(s, betting_market_group_obj.id); + fc::raw::pack(s, betting_market_group_obj.description); + fc::raw::pack(s, betting_market_group_obj.event_id); + fc::raw::pack(s, betting_market_group_obj.rules_id); + fc::raw::pack(s, betting_market_group_obj.asset_id); + fc::raw::pack(s, betting_market_group_obj.total_matched_bets_amount); + fc::raw::pack(s, betting_market_group_obj.never_in_play); + fc::raw::pack(s, betting_market_group_obj.delay_before_settling); + fc::raw::pack(s, betting_market_group_obj.settling_time); + // fc::raw::pack the contents hidden in the impl class + std::ostringstream stream; + betting_market_group_obj.pack_impl(stream); + std::string stringified_stream(stream.str()); + fc::raw::pack(s, stream.str()); + + return s; +} +template +inline Stream& operator>>( Stream& s, betting_market_group_object& betting_market_group_obj ) +{ + // unpack all fields exposed in the header in the usual way + //fc::raw::unpack >(s, betting_market_group_obj); + fc::raw::unpack(s, betting_market_group_obj.id); + fc::raw::unpack(s, betting_market_group_obj.description); + fc::raw::unpack(s, betting_market_group_obj.event_id); + fc::raw::unpack(s, betting_market_group_obj.rules_id); + fc::raw::unpack(s, betting_market_group_obj.asset_id); + fc::raw::unpack(s, betting_market_group_obj.total_matched_bets_amount); + fc::raw::unpack(s, betting_market_group_obj.never_in_play); + fc::raw::unpack(s, betting_market_group_obj.delay_before_settling); + fc::raw::unpack(s, betting_market_group_obj.settling_time); + + // fc::raw::unpack the contents hidden in the impl class + std::string stringified_stream; + fc::raw::unpack(s, stringified_stream); + std::istringstream stream(stringified_stream); + betting_market_group_obj.unpack_impl(stream); + + return s; +} + +} } // graphene::chain + +FC_REFLECT_DERIVED( graphene::chain::betting_market_rules_object, (graphene::db::object), (name)(description) ) +FC_REFLECT_DERIVED( graphene::chain::betting_market_group_object, (graphene::db::object), (description) ) +FC_REFLECT_DERIVED( graphene::chain::betting_market_object, (graphene::db::object), (group_id) ) +FC_REFLECT_DERIVED( graphene::chain::bet_object, (graphene::db::object), (bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(back_or_lay)(end_of_delay) ) + +FC_REFLECT_DERIVED( graphene::chain::betting_market_position_object, (graphene::db::object), (bettor_id)(betting_market_id)(pay_if_payout_condition)(pay_if_not_payout_condition)(pay_if_canceled)(pay_if_not_canceled)(fees_collected) ) diff --git a/libraries/chain/include/graphene/chain/config.hpp b/libraries/chain/include/graphene/chain/config.hpp index a7bdea7e..5df38771 100644 --- a/libraries/chain/include/graphene/chain/config.hpp +++ b/libraries/chain/include/graphene/chain/config.hpp @@ -151,7 +151,7 @@ #define GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT 4 #define GRAPHENE_RECENTLY_MISSED_COUNT_DECREMENT 3 -#define GRAPHENE_CURRENT_DB_VERSION "BTS2.8" +#define GRAPHENE_CURRENT_DB_VERSION "PPY1.11" #define GRAPHENE_IRREVERSIBLE_THRESHOLD (70 * GRAPHENE_1_PERCENT) @@ -172,13 +172,45 @@ /// Represents the canonical account for specifying you will vote directly (as opposed to a proxy) #define GRAPHENE_PROXY_TO_SELF_ACCOUNT (graphene::chain::account_id_type(5)) /// -#define TOURNAMENT_RAKE_FEE_ACCOUNT_ID (graphene::chain::account_id_type(6)) +#define GRAPHENE_RAKE_FEE_ACCOUNT_ID (graphene::chain::account_id_type(6)) /// Sentinel value used in the scheduler. #define GRAPHENE_NULL_WITNESS (graphene::chain::witness_id_type(0)) ///@} #define GRAPHENE_FBA_STEALTH_DESIGNATED_ASSET (asset_id_type(743)) +#define GRAPHENE_DEFAULT_RAKE_FEE_PERCENTAGE (3*GRAPHENE_1_PERCENT) + +/** + * Betting-related constants. + * + * We store bet multipliers as fixed-precision uint32_t. These values are + * the maximum power-of-ten bet we can have on a "symmetric" market: + * (decimal) 1.0001 - 10001 + * (fractional) 1:10000 - 10000:1 + */ +///@{ +/// betting odds (multipliers) are stored as fixed-precision, divide by this to get the actual multiplier +#define GRAPHENE_BETTING_ODDS_PRECISION 10000 +/// the smallest bet multiplier we will accept +#define GRAPHENE_BETTING_MIN_MULTIPLIER 10001 +/// the largest bet multiplier we will accept +#define GRAPHENE_BETTING_MAX_MULTIPLIER 100010000 +///@} +#define GRAPHENE_DEFAULT_MIN_BET_MULTIPLIER 10100 +#define GRAPHENE_DEFAULT_MAX_BET_MULTIPLIER 10000000 +#define GRAPHENE_DEFAULT_PERMITTED_BETTING_ODDS_INCREMENTS { { 20000, 100}, /* <= 2: 0.01 */ \ + { 30000, 200}, /* <= 3: 0.02 */ \ + { 40000, 500}, /* <= 4: 0.05 */ \ + { 60000, 1000}, /* <= 6: 0.10 */ \ + { 100000, 2000}, /* <= 10: 0.20 */ \ + { 200000, 5000}, /* <= 20: 0.50 */ \ + { 300000, 10000}, /* <= 30: 1.00 */ \ + { 500000, 20000}, /* <= 50: 2.00 */ \ + { 1000000, 50000}, /* <= 100: 5.00 */ \ + { 10000000, 100000} } /* <= 1000: 10.00 */ +#define GRAPHENE_DEFAULT_BETTING_PERCENT_FEE (2 * GRAPHENE_1_PERCENT) +#define GRAPHENE_DEFAULT_LIVE_BETTING_DELAY_TIME 5 // seconds #define TOURNAMENT_MIN_ROUND_DELAY 0 #define TOURNAMENT_MAX_ROUND_DELAY 600 #define TOURNAMENT_MIN_TIME_PER_COMMIT_MOVE 0 diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index 893a095a..02fe64f6 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -171,8 +171,18 @@ namespace graphene { namespace chain { */ uint32_t push_applied_operation( const operation& op ); void set_applied_operation_result( uint32_t op_id, const operation_result& r ); + + // most plugins should use the const version of get_applied_operations const vector >& get_applied_operations()const; + // the account_history plugin uses the non-const version. When it decides to track an + // operation and assigns an operation_id to it, it will store that id into the operation + // history object so other plugins that evaluate later can reference it. + vector >& get_applied_operations(); + + // the bookie plugin depends on change notifications that are skipped during normal replays + void force_slow_replays(); + string to_pretty_string( const asset& a )const; /** @@ -195,12 +205,18 @@ namespace graphene { namespace chain { * Emitted After a block has been applied and committed. The callback * should not yield and should execute quickly. */ - fc::signal&)> changed_objects; + fc::signal&, const flat_set&)> new_objects; + + /** + * Emitted After a block has been applied and committed. The callback + * should not yield and should execute quickly. + */ + fc::signal&, const flat_set&)> changed_objects; /** this signal is emitted any time an object is removed and contains a * pointer to the last value of every object that was removed. */ - fc::signal&)> removed_objects; + fc::signal&, const vector&, const flat_set&)> removed_objects; //////////////////// db_witness_schedule.cpp //////////////////// @@ -369,6 +385,29 @@ namespace graphene { namespace chain { asset max_settlement); ///@} + //////////////////// db_bet.cpp //////////////////// + + /// @{ @group Betting Market Helpers + void cancel_bet(const bet_object& bet, bool create_virtual_op = true); + void cancel_all_unmatched_bets_on_betting_market(const betting_market_object& betting_market); + void cancel_all_unmatched_bets_on_betting_market_group(const betting_market_group_object& betting_market_group); + void validate_betting_market_group_resolutions(const betting_market_group_object& betting_market_group, + const std::map& resolutions); + void resolve_betting_market_group(const betting_market_group_object& betting_market_group, + const std::map& resolutions); + void settle_betting_market_group(const betting_market_group_object& betting_market_group); + void remove_completed_events(); + /** + * @brief Process a new bet + * @param new_bet_object The new bet to process + * @return true if order was completely filled; false otherwise + * + * This function takes a new bet and attempts to match it with existing + * bets already on the books. + */ + bool place_bet(const bet_object& new_bet_object); + ///@} + /** * @return true if the order was completely filled and thus freed. */ @@ -435,12 +474,14 @@ namespace graphene { namespace chain { void update_signing_witness(const witness_object& signing_witness, const signed_block& new_block); void update_last_irreversible_block(); void clear_expired_transactions(); + void place_delayed_bets(); void clear_expired_proposals(); void clear_expired_orders(); void update_expired_feeds(); void update_maintenance_flag( bool new_maintenance_flag ); void update_withdraw_permissions(); void update_tournaments(); + void update_betting_markets(fc::time_point_sec current_block_time); bool check_for_blackswan( const asset_object& mia, bool enable_black_swan = true ); ///Steps performed only at maintenance intervals @@ -497,6 +538,7 @@ namespace graphene { namespace chain { node_property_object _node_property_object; fc::hash_ctr_rng _random_number_generator; + bool _slow_replays = false; }; namespace detail diff --git a/libraries/chain/include/graphene/chain/event_evaluator.hpp b/libraries/chain/include/graphene/chain/event_evaluator.hpp new file mode 100644 index 00000000..121309ee --- /dev/null +++ b/libraries/chain/include/graphene/chain/event_evaluator.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace graphene { namespace chain { + + class event_create_evaluator : public evaluator + { + public: + typedef event_create_operation operation_type; + + void_result do_evaluate( const event_create_operation& o ); + object_id_type do_apply( const event_create_operation& o ); + private: + event_group_id_type event_group_id; + }; + + class event_update_evaluator : public evaluator + { + public: + typedef event_update_operation operation_type; + + void_result do_evaluate( const event_update_operation& o ); + void_result do_apply( const event_update_operation& o ); + private: + event_group_id_type event_group_id; + }; + + class event_update_status_evaluator : public evaluator + { + public: + typedef event_update_status_operation operation_type; + + void_result do_evaluate( const event_update_status_operation& o ); + void_result do_apply( const event_update_status_operation& o ); + private: + const event_object* _event_to_update = nullptr; + }; + +} } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/event_group_evaluator.hpp b/libraries/chain/include/graphene/chain/event_group_evaluator.hpp new file mode 100644 index 00000000..1feda1c0 --- /dev/null +++ b/libraries/chain/include/graphene/chain/event_group_evaluator.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace graphene { namespace chain { + + class event_group_create_evaluator : public evaluator + { + public: + typedef event_group_create_operation operation_type; + + void_result do_evaluate( const event_group_create_operation& o ); + object_id_type do_apply( const event_group_create_operation& o ); + + private: + sport_id_type sport_id; + }; + + class event_group_update_evaluator : public evaluator + { + public: + typedef event_group_update_operation operation_type; + + void_result do_evaluate( const event_group_update_operation& o ); + void_result do_apply( const event_group_update_operation& o ); + + private: + sport_id_type sport_id; + }; +} } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/event_group_object.hpp b/libraries/chain/include/graphene/chain/event_group_object.hpp new file mode 100644 index 00000000..f47eda31 --- /dev/null +++ b/libraries/chain/include/graphene/chain/event_group_object.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace graphene { namespace chain { + +class database; + +struct by_sport_id; + +class event_group_object : public graphene::db::abstract_object< event_group_object > +{ + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = event_group_object_type; + + internationalized_string_type name; + sport_id_type sport_id; +}; + +typedef multi_index_container< + event_group_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< tag, member< event_group_object, sport_id_type, &event_group_object::sport_id > > > + > event_group_object_multi_index_type; + +typedef generic_index event_group_object_index; +} } // graphene::chain + +FC_REFLECT_DERIVED( graphene::chain::event_group_object, (graphene::db::object), (name)(sport_id) ) diff --git a/libraries/chain/include/graphene/chain/event_object.hpp b/libraries/chain/include/graphene/chain/event_object.hpp new file mode 100644 index 00000000..2c38a33f --- /dev/null +++ b/libraries/chain/include/graphene/chain/event_object.hpp @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include +#include + +namespace graphene { namespace chain { + class event_object; +} } + +namespace fc { + void to_variant(const graphene::chain::event_object& event_obj, fc::variant& v); + void from_variant(const fc::variant& v, graphene::chain::event_object& event_obj); +} //end namespace fc + +namespace graphene { namespace chain { + +class database; + +class event_object : public graphene::db::abstract_object< event_object > +{ + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = event_object_type; + + event_object(); + event_object(const event_object& rhs); + ~event_object(); + event_object& operator=(const event_object& rhs); + + internationalized_string_type name; + + internationalized_string_type season; + + optional start_time; + + event_group_id_type event_group_id; + + bool at_least_one_betting_market_group_settled; + + event_status get_status() const; + vector scores; + + // serialization functions: + // for serializing to raw, go through a temporary sstream object to avoid + // having to implement serialization in the header file + template + friend Stream& operator<<( Stream& s, const event_object& event_obj ); + + template + friend Stream& operator>>( Stream& s, event_object& event_obj ); + + friend void ::fc::to_variant(const graphene::chain::event_object& event_obj, fc::variant& v); + friend void ::fc::from_variant(const fc::variant& v, graphene::chain::event_object& event_obj); + + void pack_impl(std::ostream& stream) const; + void unpack_impl(std::istream& stream); + + void on_upcoming_event(database& db); + void on_in_progress_event(database& db); + void on_frozen_event(database& db); + void on_finished_event(database& db); + void on_canceled_event(database& db); + void on_settled_event(database& db); + void on_betting_market_group_resolved(database& db, betting_market_group_id_type resolved_group, bool was_canceled); + void on_betting_market_group_closed(database& db, betting_market_group_id_type closed_group); + void dispatch_new_status(database& db, event_status new_status); + private: + class impl; + std::unique_ptr my; +}; + +struct by_event_group_id; +struct by_event_status; +typedef multi_index_container< + event_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< tag, member< event_object, event_group_id_type, &event_object::event_group_id > >, + ordered_non_unique< tag, const_mem_fun< event_object, event_status, &event_object::get_status > > > > event_object_multi_index_type; + +typedef generic_index event_object_index; + + template + inline Stream& operator<<( Stream& s, const event_object& event_obj ) + { + fc_elog(fc::logger::get("event"), "In event_obj to_raw"); + // pack all fields exposed in the header in the usual way + // instead of calling the derived pack, just serialize the one field in the base class + // fc::raw::pack >(s, event_obj); + fc::raw::pack(s, event_obj.id); + fc::raw::pack(s, event_obj.name); + fc::raw::pack(s, event_obj.season); + fc::raw::pack(s, event_obj.start_time); + fc::raw::pack(s, event_obj.event_group_id); + fc::raw::pack(s, event_obj.at_least_one_betting_market_group_settled); + fc::raw::pack(s, event_obj.scores); + + // fc::raw::pack the contents hidden in the impl class + std::ostringstream stream; + event_obj.pack_impl(stream); + std::string stringified_stream(stream.str()); + fc::raw::pack(s, stream.str()); + + return s; + } + template + inline Stream& operator>>( Stream& s, event_object& event_obj ) + { + fc_elog(fc::logger::get("event"), "In event_obj from_raw"); + // unpack all fields exposed in the header in the usual way + //fc::raw::unpack >(s, event_obj); + fc::raw::unpack(s, event_obj.id); + fc::raw::unpack(s, event_obj.name); + fc::raw::unpack(s, event_obj.season); + fc::raw::unpack(s, event_obj.start_time); + fc::raw::unpack(s, event_obj.event_group_id); + fc::raw::unpack(s, event_obj.at_least_one_betting_market_group_settled); + fc::raw::unpack(s, event_obj.scores); + + // fc::raw::unpack the contents hidden in the impl class + std::string stringified_stream; + fc::raw::unpack(s, stringified_stream); + std::istringstream stream(stringified_stream); + event_obj.unpack_impl(stream); + + return s; + } +} } // graphene::chain +FC_REFLECT(graphene::chain::event_object, (name)) + diff --git a/libraries/chain/include/graphene/chain/global_betting_statistics_object.hpp b/libraries/chain/include/graphene/chain/global_betting_statistics_object.hpp new file mode 100644 index 00000000..9d59cb66 --- /dev/null +++ b/libraries/chain/include/graphene/chain/global_betting_statistics_object.hpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace graphene { namespace chain { + +class database; + +class global_betting_statistics_object : public graphene::db::abstract_object< global_betting_statistics_object > +{ + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_global_betting_statistics_object_type; + + uint32_t number_of_active_events; + map total_amount_staked; +}; + +typedef multi_index_container< + global_betting_statistics_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > > > > global_betting_statistics_object_multi_index_type; +typedef generic_index global_betting_statistics_object_index; + +} } // graphene::chain + +FC_REFLECT_DERIVED( graphene::chain::global_betting_statistics_object, (graphene::db::object), (number_of_active_events)(total_amount_staked) ) diff --git a/libraries/chain/include/graphene/chain/market_object.hpp b/libraries/chain/include/graphene/chain/market_object.hpp index c41def13..b56f4e9c 100644 --- a/libraries/chain/include/graphene/chain/market_object.hpp +++ b/libraries/chain/include/graphene/chain/market_object.hpp @@ -120,6 +120,13 @@ class call_order_object : public abstract_object share_type collateral; ///< call_price.base.asset_id, access via get_collateral share_type debt; ///< call_price.quote.asset_id, access via get_collateral price call_price; ///< Debt / Collateral + + pair get_market()const + { + auto tmp = std::make_pair( call_price.base.asset_id, call_price.quote.asset_id ); + if( tmp.first > tmp.second ) std::swap( tmp.first, tmp.second ); + return tmp; + } }; /** diff --git a/libraries/chain/include/graphene/chain/operation_history_object.hpp b/libraries/chain/include/graphene/chain/operation_history_object.hpp index ecbbc58d..eae8a01e 100644 --- a/libraries/chain/include/graphene/chain/operation_history_object.hpp +++ b/libraries/chain/include/graphene/chain/operation_history_object.hpp @@ -102,6 +102,7 @@ namespace graphene { namespace chain { struct by_id; struct by_seq; struct by_op; +struct by_opid; typedef multi_index_container< account_transaction_history_object, indexed_by< @@ -117,6 +118,9 @@ typedef multi_index_container< member< account_transaction_history_object, account_id_type, &account_transaction_history_object::account>, member< account_transaction_history_object, operation_history_id_type, &account_transaction_history_object::operation_id> > + >, + ordered_non_unique< tag, + member< account_transaction_history_object, operation_history_id_type, &account_transaction_history_object::operation_id> > > > account_transaction_history_multi_index_type; diff --git a/libraries/chain/include/graphene/chain/proposal_object.hpp b/libraries/chain/include/graphene/chain/proposal_object.hpp index 97d98a5b..d41ea7ea 100644 --- a/libraries/chain/include/graphene/chain/proposal_object.hpp +++ b/libraries/chain/include/graphene/chain/proposal_object.hpp @@ -50,6 +50,7 @@ class proposal_object : public abstract_object flat_set required_owner_approvals; flat_set available_owner_approvals; flat_set available_key_approvals; + account_id_type proposer; bool is_authorized_to_execute(database& db)const; }; @@ -93,4 +94,4 @@ typedef generic_index proposal_ FC_REFLECT_DERIVED( graphene::chain::proposal_object, (graphene::chain::object), (expiration_time)(review_period_time)(proposed_transaction)(required_active_approvals) (available_active_approvals)(required_owner_approvals)(available_owner_approvals) - (available_key_approvals) ) + (available_key_approvals)(proposer) ) diff --git a/libraries/chain/include/graphene/chain/protocol/account.hpp b/libraries/chain/include/graphene/chain/protocol/account.hpp index 329d03e2..93dbb115 100644 --- a/libraries/chain/include/graphene/chain/protocol/account.hpp +++ b/libraries/chain/include/graphene/chain/protocol/account.hpp @@ -264,7 +264,7 @@ namespace graphene { namespace chain { } } // graphene::chain FC_REFLECT(graphene::chain::account_options, (memo_key)(voting_account)(num_witness)(num_committee)(votes)(extensions)) -FC_REFLECT_TYPENAME( graphene::chain::account_whitelist_operation::account_listing) +// FC_REFLECT_TYPENAME( graphene::chain::account_whitelist_operation::account_listing) FC_REFLECT_ENUM( graphene::chain::account_whitelist_operation::account_listing, (no_listing)(white_listed)(black_listed)(white_and_black_listed)) diff --git a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp index 62c9c9a2..5ff353a3 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp @@ -291,7 +291,7 @@ namespace graphene { namespace chain { asset_dividend_distribution_operation() {} asset_dividend_distribution_operation(const asset_id_type& dividend_asset_id, const account_id_type& account_id, - const flat_set& amounts) : + const vector& amounts) : dividend_asset_id(dividend_asset_id), account_id(account_id), amounts(amounts) @@ -323,7 +323,7 @@ namespace graphene { namespace chain { account_id_type account_id; /// The amounts received - flat_set amounts; + vector amounts; extensions_type extensions; diff --git a/libraries/chain/include/graphene/chain/protocol/authority.hpp b/libraries/chain/include/graphene/chain/protocol/authority.hpp index b6ef60d7..70b674b3 100644 --- a/libraries/chain/include/graphene/chain/protocol/authority.hpp +++ b/libraries/chain/include/graphene/chain/protocol/authority.hpp @@ -132,5 +132,5 @@ void add_authority_accounts( } } // namespace graphene::chain FC_REFLECT( graphene::chain::authority, (weight_threshold)(account_auths)(key_auths)(address_auths) ) -FC_REFLECT_TYPENAME( graphene::chain::authority::classification ) +// FC_REFLECT_TYPENAME( graphene::chain::authority::classification ) FC_REFLECT_ENUM( graphene::chain::authority::classification, (owner)(active)(key) ) diff --git a/libraries/chain/include/graphene/chain/protocol/betting_market.hpp b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp new file mode 100644 index 00000000..a27758c7 --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp @@ -0,0 +1,493 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include + +namespace graphene { namespace chain { + +struct betting_market_rules_create_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + /** + * A short name for the rules, like "Premier League Rules 1.0", probably not + * displayed to the user + */ + internationalized_string_type name; + + /** + * The full text of the rules to be displayed to the user. As yet, there is + * no standard format (html, markdown, etc) + */ + internationalized_string_type description; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + + +struct betting_market_rules_update_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + betting_market_rules_id_type betting_market_rules_id; + + fc::optional new_name; + + fc::optional new_description; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +enum class betting_market_status +{ + unresolved, /// no grading has been published for this betting market + frozen, /// bets are suspended, no bets allowed + graded, /// grading of win or not_win has been published + canceled, /// the betting market is canceled, no further bets are allowed + settled, /// the betting market has been paid out + BETTING_MARKET_STATUS_COUNT +}; + + +/** + * The status of a betting market group. This controls the behavior of the betting + * markets in the group. + */ +enum class betting_market_group_status +{ + upcoming, /// betting markets are accepting bets, will never go "in_play" + in_play, /// betting markets are delaying bets + closed, /// betting markets are no longer accepting bets + graded, /// witnesses have published win/not win for the betting markets + re_grading, /// initial win/not win grading has been challenged + settled, /// paid out + frozen, /// betting markets are not accepting bets + canceled, /// canceled + BETTING_MARKET_GROUP_STATUS_COUNT +}; + + +struct betting_market_group_create_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + /** + * A description of the betting market, like "Moneyline", "Over/Under 180", + * used for display + */ + internationalized_string_type description; + + /** + * This can be a event_id_type, or a + * relative object id that resolves to a event_id_type + */ + object_id_type event_id; + + /** + * This can be a betting_market_rules_id_type, or a + * relative object id that resolves to a betting_market_rules_id_type + */ + object_id_type rules_id; + + /** + * The asset used to place bets for all betting markets in this group + */ + asset_id_type asset_id; + + /** + * If true, this market will never go "in-play" + */ + bool never_in_play; + + /** + * After a grading has been published, the blockchain will wait this many + * seconds before settling (paying the winners). + * If the published grading is flagged (challenged) during this period, + * settling will be delayed indefinitely until the betting market + * group is re-graded (not implemented) + */ + uint32_t delay_before_settling; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +struct betting_market_group_update_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + betting_market_group_id_type betting_market_group_id; + + optional new_description; + + optional new_rules_id; + + optional status; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +struct betting_market_create_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + /** + * This can be a betting_market_group_id_type, or a + * relative object id that resolves to a betting_market_group_id_type + */ + object_id_type group_id; + + internationalized_string_type description; + + internationalized_string_type payout_condition; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +struct betting_market_update_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + betting_market_id_type betting_market_id; + + optional new_group_id; + + optional new_description; + + optional new_payout_condition; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +enum class betting_market_resolution_type { + win, + not_win, + cancel, + BETTING_MARKET_RESOLUTION_COUNT +}; + +struct betting_market_group_resolve_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + betting_market_group_id_type betting_market_group_id; + + std::map resolutions; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +struct betting_market_group_resolved_operation : public base_operation +{ + struct fee_parameters_type {}; + + account_id_type bettor_id; + betting_market_group_id_type betting_market_group_id; + std::map resolutions; + + share_type winnings; // always the asset type of the betting market group + share_type fees_paid; // always the asset type of the betting market group + + asset fee; // unused in a virtual operation + + betting_market_group_resolved_operation() {} + betting_market_group_resolved_operation(account_id_type bettor_id, + betting_market_group_id_type betting_market_group_id, + const std::map& resolutions, + share_type winnings, + share_type fees_paid) : + bettor_id(bettor_id), + betting_market_group_id(betting_market_group_id), + resolutions(resolutions), + winnings(winnings), + fees_paid(fees_paid) + { + } + + account_id_type fee_payer()const { return bettor_id; } + void validate()const { FC_ASSERT(false, "virtual operation"); } + /// This is a virtual operation; there is no fee + share_type calculate_fee(const fee_parameters_type& k)const { return 0; } +}; + +struct betting_market_group_cancel_unmatched_bets_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + betting_market_group_id_type betting_market_group_id; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +enum class bet_type { back, lay }; + +struct bet_place_operation : public base_operation +{ + struct fee_parameters_type + { + uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; // fixed fee charged upon placing the bet + }; + asset fee; + + account_id_type bettor_id; + + betting_market_id_type betting_market_id; + + /// the bettor's stake + asset amount_to_bet; + + // decimal odds as seen by the backer, even if this is a lay bet. + // this is a fixed-precision number scaled by GRAPHENE_BETTING_ODDS_PRECISION. + // + // For example, an even 1/1 bet would be decimal odds 2.0, so backer_multiplier + // would be 2 * GRAPHENE_BETTING_ODDS_PRECISION. + bet_multiplier_type backer_multiplier; + + bet_type back_or_lay; + + extensions_type extensions; + + account_id_type fee_payer()const { return bettor_id; } + void validate()const; +}; + +/** + * virtual op generated when a bet is matched + */ +struct bet_matched_operation : public base_operation +{ + struct fee_parameters_type {}; + + bet_matched_operation(){} + bet_matched_operation(account_id_type bettor_id, bet_id_type bet_id, + asset amount_bet, + bet_multiplier_type backer_multiplier, + share_type guaranteed_winnings_returned) : + bettor_id(bettor_id), + bet_id(bet_id), + amount_bet(amount_bet), + backer_multiplier(backer_multiplier), + guaranteed_winnings_returned(guaranteed_winnings_returned) + {} + + account_id_type bettor_id; + bet_id_type bet_id; + asset amount_bet; + bet_multiplier_type backer_multiplier; // the actual odds received + share_type guaranteed_winnings_returned; // same asset type as amount_bet + asset fee; // unimportant for a virtual op + + account_id_type fee_payer()const { return bettor_id; } + void validate()const { FC_ASSERT(false, "virtual operation"); } + + /// This is a virtual operation; there is no fee + share_type calculate_fee(const fee_parameters_type& k)const { return 0; } +}; + +struct bet_cancel_operation : public base_operation +{ + struct fee_parameters_type + { + uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; + //uint64_t fee = 0; + }; + asset fee; + + /// the bettor who is cancelling the bet + account_id_type bettor_id; + /// the bet being canceled + bet_id_type bet_to_cancel; + + extensions_type extensions; + + account_id_type fee_payer()const { return bettor_id; } + void validate()const; +}; + +/** + * virtual op generated when a bet is canceled + */ +struct bet_canceled_operation : public base_operation +{ + struct fee_parameters_type {}; + + bet_canceled_operation(){} + bet_canceled_operation(account_id_type bettor_id, bet_id_type bet_id, + asset stake_returned) : + bettor_id(bettor_id), + bet_id(bet_id), + stake_returned(stake_returned) + {} + + account_id_type bettor_id; + bet_id_type bet_id; + asset stake_returned; + asset fee; // unimportant for a virtual op + + account_id_type fee_payer()const { return bettor_id; } + void validate()const { FC_ASSERT(false, "virtual operation"); } + + /// This is a virtual operation; there is no fee + share_type calculate_fee(const fee_parameters_type& k)const { return 0; } +}; + +/** + * virtual op generated when a bet amount is rounded down to an amount that can + * match evenly at a given odds (the blockchain does this automatically at the time + * the bet is placed on the order books). (note: there is no way a user can adjust their bet + * after placing it, aside from canceling the bet and placing a new one) + */ +struct bet_adjusted_operation : public base_operation +{ + struct fee_parameters_type {}; + + bet_adjusted_operation(){} + bet_adjusted_operation(account_id_type bettor_id, bet_id_type bet_id, + asset stake_returned) : + bettor_id(bettor_id), + bet_id(bet_id), + stake_returned(stake_returned) + {} + + account_id_type bettor_id; + bet_id_type bet_id; + asset stake_returned; + asset fee; // unimportant for a virtual op + + account_id_type fee_payer()const { return bettor_id; } + void validate()const { FC_ASSERT(false, "virtual operation"); } + + /// This is a virtual operation; there is no fee + share_type calculate_fee(const fee_parameters_type& k)const { return 0; } +}; + +} } + +FC_REFLECT( graphene::chain::betting_market_rules_create_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_rules_create_operation, + (fee)(name)(description)(extensions) ) + +FC_REFLECT( graphene::chain::betting_market_rules_update_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_rules_update_operation, + (fee)(new_name)(new_description)(extensions)(betting_market_rules_id) ) + +FC_REFLECT_ENUM( graphene::chain::betting_market_status, + (unresolved) + (frozen) + (graded) + (canceled) + (settled) + (BETTING_MARKET_STATUS_COUNT) ) +FC_REFLECT_ENUM( graphene::chain::betting_market_group_status, + (upcoming) + (in_play) + (closed) + (graded) + (re_grading) + (settled) + (frozen) + (canceled) + (BETTING_MARKET_GROUP_STATUS_COUNT) ) + +FC_REFLECT( graphene::chain::betting_market_group_create_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_group_create_operation, + (fee)(description)(event_id)(rules_id)(asset_id) + (never_in_play)(delay_before_settling) + (extensions) ) + +FC_REFLECT( graphene::chain::betting_market_group_update_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_group_update_operation, + (fee)(betting_market_group_id)(new_description)(new_rules_id)(status)(extensions) ) + +FC_REFLECT( graphene::chain::betting_market_create_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_create_operation, + (fee)(group_id)(description)(payout_condition)(extensions) ) + +FC_REFLECT( graphene::chain::betting_market_update_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_update_operation, + (fee)(betting_market_id)(new_group_id)(new_description)(new_payout_condition)(extensions) ) + +FC_REFLECT_ENUM( graphene::chain::betting_market_resolution_type, (win)(not_win)(cancel)(BETTING_MARKET_RESOLUTION_COUNT) ) + +FC_REFLECT( graphene::chain::betting_market_group_resolve_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_group_resolve_operation, + (fee)(betting_market_group_id)(resolutions)(extensions) ) + +FC_REFLECT( graphene::chain::betting_market_group_resolved_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::betting_market_group_resolved_operation, + (bettor_id)(betting_market_group_id)(resolutions)(winnings)(fees_paid)(fee) ) + +FC_REFLECT( graphene::chain::betting_market_group_cancel_unmatched_bets_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::betting_market_group_cancel_unmatched_bets_operation, + (fee)(betting_market_group_id)(extensions) ) + +FC_REFLECT_ENUM( graphene::chain::bet_type, (back)(lay) ) +FC_REFLECT( graphene::chain::bet_place_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::bet_place_operation, + (fee)(bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(back_or_lay)(extensions) ) + +FC_REFLECT( graphene::chain::bet_matched_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::bet_matched_operation, (bettor_id)(bet_id)(amount_bet)(backer_multiplier)(guaranteed_winnings_returned) ) + +FC_REFLECT( graphene::chain::bet_cancel_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::bet_cancel_operation, (fee) (bettor_id) (bet_to_cancel) (extensions) ) + +FC_REFLECT( graphene::chain::bet_canceled_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::bet_canceled_operation, (bettor_id)(bet_id)(stake_returned) ) + +FC_REFLECT( graphene::chain::bet_adjusted_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::bet_adjusted_operation, (bettor_id)(bet_id)(stake_returned) ) diff --git a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp index aa847a69..a0445a3f 100644 --- a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp +++ b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp @@ -69,7 +69,12 @@ namespace graphene { namespace chain { uint16_t accounts_per_fee_scale = GRAPHENE_DEFAULT_ACCOUNTS_PER_FEE_SCALE; ///< number of accounts between fee scalings uint8_t account_fee_scale_bitshifts = GRAPHENE_DEFAULT_ACCOUNT_FEE_SCALE_BITSHIFTS; ///< number of times to left bitshift account registration fee at each scaling uint8_t max_authority_depth = GRAPHENE_MAX_SIG_CHECK_DEPTH; - //uint8_t witness_schedule_algorithm = GRAPHENE_WITNESS_SHUFFLED_ALGORITHM; ///< 0 shuffled, 1 scheduled + uint16_t betting_rake_fee_percentage = GRAPHENE_DEFAULT_RAKE_FEE_PERCENTAGE; ///< part of prize paid into the dividend account for the core token holders + bet_multiplier_type min_bet_multiplier = GRAPHENE_DEFAULT_MIN_BET_MULTIPLIER; + bet_multiplier_type max_bet_multiplier = GRAPHENE_DEFAULT_MAX_BET_MULTIPLIER; + flat_map permitted_betting_odds_increments = GRAPHENE_DEFAULT_PERMITTED_BETTING_ODDS_INCREMENTS; + uint16_t live_betting_delay_time = GRAPHENE_DEFAULT_LIVE_BETTING_DELAY_TIME; ///< delayed bets + //uint8_t witness_schedule_algorithm = GRAPHENE_WITNESS_SHUFFLED_ALGORITHM; ///< 0 shuffled, 1 scheduled uint8_t witness_schedule_algorithm = GRAPHENE_WITNESS_SCHEDULED_ALGORITHM; ///< 0 shuffled, 1 scheduled /* rps tournament parameters constraints */ uint32_t min_round_delay = TOURNAMENT_MIN_ROUND_DELAY; ///< miniaml delay between games @@ -122,7 +127,12 @@ FC_REFLECT( graphene::chain::chain_parameters, (accounts_per_fee_scale) (account_fee_scale_bitshifts) (max_authority_depth) + (min_bet_multiplier) + (max_bet_multiplier) + (betting_rake_fee_percentage) + (permitted_betting_odds_increments) (witness_schedule_algorithm) + (live_betting_delay_time) (min_round_delay) (max_round_delay) (min_time_per_commit_move) diff --git a/libraries/chain/include/graphene/chain/protocol/event.hpp b/libraries/chain/include/graphene/chain/protocol/event.hpp new file mode 100644 index 00000000..f0da95f7 --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/event.hpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include + +namespace graphene { namespace chain { + +struct event_create_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + /** + * The name of the event + */ + internationalized_string_type name; + + internationalized_string_type season; + + optional start_time; + + /** + * This can be a event_group_id_type, or a + * relative object id that resolves to a event_group_id_type + */ + object_id_type event_group_id; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +/** + * The status of an event. This is used to display in the UI, and setting + * the event's status to certain values will propagate down to the + * betting market groups in the event: + * - when set to `in_progress`, all betting market groups are set to `in_play` + * - when set to `completed`, all betting market groups are set to `closed` + * - when set to `frozen`, all betting market groups are set to `frozen` + * - when set to `canceled`, all betting market groups are set to `canceled` + */ +enum class event_status +{ + upcoming, /// Event has not started yet, betting is allowed + in_progress, /// Event is in progress, if "in-play" betting is enabled, bets will be delayed + frozen, /// Betting is temporarily disabled + finished, /// Event has finished, no more betting allowed + canceled, /// Event has been canceled, all betting markets have been canceled + settled, /// All betting markets have been paid out + STATUS_COUNT +}; + +struct event_update_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + event_id_type event_id; + + optional new_event_group_id; + + optional new_name; + + optional new_season; + + optional new_start_time; + + optional new_status; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +/** + * The current (or final) score of an event. + * This is only used for display to the user, witnesses must resolve each + * betting market explicitly. + * These are free-form strings that we assume will make sense to the user. + * For a game like football, this may be a score like "3". For races, + * it could be a time like "1:53.4". + */ +struct event_update_status_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + /// the id of the event to update + event_id_type event_id; + + /** + * the new status of the event (if the status hasn't changed, the creator + * of this operation must still set `status` to the event's current status) + */ + event_status status; + + /* + * scores for each competitor stored in same order as competitors in event_object + */ + vector scores; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +} } + +FC_REFLECT( graphene::chain::event_create_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::event_create_operation, + (fee)(name)(season)(start_time)(event_group_id)(extensions) ) + +FC_REFLECT( graphene::chain::event_update_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::event_update_operation, + (fee)(event_id)(new_event_group_id)(new_name)(new_season)(new_start_time)(new_status)(extensions) ) + +FC_REFLECT_ENUM( graphene::chain::event_status, (upcoming)(in_progress)(frozen)(finished)(canceled)(settled)(STATUS_COUNT) ) +FC_REFLECT( graphene::chain::event_update_status_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::event_update_status_operation, + (fee)(event_id)(status)(scores)(extensions) ) + diff --git a/libraries/chain/include/graphene/chain/protocol/event_group.hpp b/libraries/chain/include/graphene/chain/protocol/event_group.hpp new file mode 100644 index 00000000..450273bc --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/event_group.hpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include + +namespace graphene { namespace chain { + +struct event_group_create_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + /** + * The name of the event_group + */ + internationalized_string_type name; + + /** + * This can be a sport_id_type, or a + * relative object id that resolves to a sport_id_type + */ + object_id_type sport_id; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +struct event_group_update_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + event_group_id_type event_group_id; + + /** + * This can be a sport_id_type, or a + * relative object id that resolves to a sport_id_type + */ + optional new_sport_id; + + optional new_name; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + + +} } + +FC_REFLECT( graphene::chain::event_group_create_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::event_group_create_operation, + (fee)(name)(sport_id)(extensions) ) + +FC_REFLECT( graphene::chain::event_group_update_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::event_group_update_operation, + (fee)(new_sport_id)(new_name)(event_group_id)(extensions) ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index 1bc1904d..58cc2cba 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -38,6 +38,10 @@ #include #include #include +#include +#include +#include +#include #include namespace graphene { namespace chain { @@ -92,14 +96,35 @@ namespace graphene { namespace chain { transfer_from_blind_operation, asset_settle_cancel_operation, // VIRTUAL asset_claim_fees_operation, - fba_distribute_operation, // VIRTUAL + fba_distribute_operation, // VIRTUAL tournament_create_operation, tournament_join_operation, game_move_operation, asset_update_dividend_operation, asset_dividend_distribution_operation, // VIRTUAL tournament_payout_operation, // VIRTUAL - tournament_leave_operation + tournament_leave_operation, + sport_create_operation, + sport_update_operation, + event_group_create_operation, + event_group_update_operation, + event_create_operation, + event_update_operation, + betting_market_rules_create_operation, + betting_market_rules_update_operation, + betting_market_group_create_operation, + betting_market_create_operation, + bet_place_operation, + betting_market_group_resolve_operation, + betting_market_group_resolved_operation, // VIRTUAL + bet_adjusted_operation, // VIRTUAL + betting_market_group_cancel_unmatched_bets_operation, + bet_matched_operation, // VIRTUAL + bet_cancel_operation, + bet_canceled_operation, // VIRTUAL + betting_market_group_update_operation, + betting_market_update_operation, + event_update_status_operation > operation; /// @} // operations group diff --git a/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp b/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp index 1d377e0f..1dff56cc 100644 --- a/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp +++ b/libraries/chain/include/graphene/chain/protocol/rock_paper_scissors.hpp @@ -94,7 +94,7 @@ namespace graphene { namespace chain { FC_REFLECT( graphene::chain::rock_paper_scissors_game_options, (insurance_enabled)(time_per_commit_move)(time_per_reveal_move)(number_of_gestures) ) -FC_REFLECT_TYPENAME( graphene::chain::rock_paper_scissors_gesture) +// FC_REFLECT_TYPENAME( graphene::chain::rock_paper_scissors_gesture) FC_REFLECT_ENUM( graphene::chain::rock_paper_scissors_gesture, (rock) (paper) diff --git a/libraries/chain/include/graphene/chain/protocol/sport.hpp b/libraries/chain/include/graphene/chain/protocol/sport.hpp new file mode 100644 index 00000000..2824044a --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/sport.hpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include + +namespace graphene { namespace chain { + +struct sport_create_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + /** + * The name of the sport + */ + internationalized_string_type name; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +struct sport_update_operation : public base_operation +{ + struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; + asset fee; + + sport_id_type sport_id; + + optional new_name; + + extensions_type extensions; + + account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } + void validate()const; +}; + +} } + +FC_REFLECT( graphene::chain::sport_create_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::sport_create_operation, + (fee)(name)(extensions) ) + +FC_REFLECT( graphene::chain::sport_update_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::sport_update_operation, + (fee)(sport_id)(new_name)(extensions) ) diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index e8ccfcf5..c17a10b1 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -134,6 +134,13 @@ namespace graphene { namespace chain { vesting_balance_object_type, worker_object_type, balance_object_type, + sport_object_type, + event_group_object_type, + event_object_type, + betting_market_rules_object_type, + betting_market_group_object_type, + betting_market_object_type, + bet_object_type, tournament_object_type, tournament_details_object_type, match_object_type, @@ -160,6 +167,8 @@ namespace graphene { namespace chain { impl_special_authority_object_type, impl_buyback_object_type, impl_fba_accumulator_object_type, + impl_betting_market_position_object_type, + impl_global_betting_statistics_object_type, impl_asset_dividend_data_type, impl_pending_dividend_payout_balance_for_holder_object_type, impl_distributed_dividend_balance_data_type @@ -182,6 +191,13 @@ namespace graphene { namespace chain { class worker_object; class balance_object; class blinded_balance_object; + class sport_object; + class event_group_object; + class event_object; + class betting_market_rules_object; + class betting_market_group_object; + class betting_market_object; + class bet_object; class tournament_object; class tournament_details_object; class match_object; @@ -201,6 +217,13 @@ namespace graphene { namespace chain { typedef object_id< protocol_ids, vesting_balance_object_type, vesting_balance_object> vesting_balance_id_type; typedef object_id< protocol_ids, worker_object_type, worker_object> worker_id_type; typedef object_id< protocol_ids, balance_object_type, balance_object> balance_id_type; + typedef object_id< protocol_ids, sport_object_type, sport_object> sport_id_type; + typedef object_id< protocol_ids, event_group_object_type, event_group_object> event_group_id_type; + typedef object_id< protocol_ids, event_object_type, event_object> event_id_type; + typedef object_id< protocol_ids, betting_market_rules_object_type, betting_market_rules_object> betting_market_rules_id_type; + typedef object_id< protocol_ids, betting_market_group_object_type, betting_market_group_object> betting_market_group_id_type; + typedef object_id< protocol_ids, betting_market_object_type, betting_market_object> betting_market_id_type; + typedef object_id< protocol_ids, bet_object_type, bet_object> bet_id_type; typedef object_id< protocol_ids, tournament_object_type, tournament_object> tournament_id_type; typedef object_id< protocol_ids, tournament_details_object_type, tournament_details_object> tournament_details_id_type; typedef object_id< protocol_ids, match_object_type, match_object> match_id_type; @@ -222,6 +245,8 @@ namespace graphene { namespace chain { class special_authority_object; class buyback_object; class fba_accumulator_object; + class betting_market_position_object; + class global_betting_statistics_object; class tournament_details_object; class asset_dividend_data_object; class pending_dividend_payout_balance_for_holder_object; @@ -247,6 +272,8 @@ namespace graphene { namespace chain { typedef object_id< implementation_ids, impl_special_authority_object_type, special_authority_object > special_authority_id_type; typedef object_id< implementation_ids, impl_buyback_object_type, buyback_object > buyback_id_type; typedef object_id< implementation_ids, impl_fba_accumulator_object_type, fba_accumulator_object > fba_accumulator_id_type; + typedef object_id< implementation_ids, impl_betting_market_position_object_type, betting_market_position_object > betting_market_position_id_type; + typedef object_id< implementation_ids, impl_global_betting_statistics_object_type, global_betting_statistics_object > global_betting_statistics_id_type; typedef fc::array symbol_type; typedef fc::ripemd160 block_id_type; @@ -325,6 +352,10 @@ namespace graphene { namespace chain { friend bool operator == ( const extended_private_key_type& p1, const extended_private_key_type& p2); friend bool operator != ( const extended_private_key_type& p1, const extended_private_key_type& p2); }; + + typedef flat_map internationalized_string_type; + + typedef uint32_t bet_multiplier_type; } } // graphene::chain namespace fc @@ -361,6 +392,13 @@ FC_REFLECT_ENUM( graphene::chain::object_type, (vesting_balance_object_type) (worker_object_type) (balance_object_type) + (sport_object_type) + (event_group_object_type) + (event_object_type) + (betting_market_rules_object_type) + (betting_market_group_object_type) + (betting_market_object_type) + (bet_object_type) (tournament_object_type) (tournament_details_object_type) (match_object_type) @@ -385,6 +423,8 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_special_authority_object_type) (impl_buyback_object_type) (impl_fba_accumulator_object_type) + (impl_betting_market_position_object_type) + (impl_global_betting_statistics_object_type) (impl_asset_dividend_data_type) (impl_pending_dividend_payout_balance_for_holder_object_type) (impl_distributed_dividend_balance_data_type) @@ -406,6 +446,13 @@ FC_REFLECT_TYPENAME( graphene::chain::withdraw_permission_id_type ) FC_REFLECT_TYPENAME( graphene::chain::vesting_balance_id_type ) FC_REFLECT_TYPENAME( graphene::chain::worker_id_type ) FC_REFLECT_TYPENAME( graphene::chain::balance_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::sport_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::event_group_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::event_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::betting_market_rules_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::betting_market_group_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::betting_market_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::bet_id_type ) FC_REFLECT_TYPENAME( graphene::chain::tournament_id_type ) FC_REFLECT_TYPENAME( graphene::chain::global_property_id_type ) FC_REFLECT_TYPENAME( graphene::chain::dynamic_global_property_id_type ) @@ -420,6 +467,8 @@ FC_REFLECT_TYPENAME( graphene::chain::budget_record_id_type ) FC_REFLECT_TYPENAME( graphene::chain::special_authority_id_type ) FC_REFLECT_TYPENAME( graphene::chain::buyback_id_type ) FC_REFLECT_TYPENAME( graphene::chain::fba_accumulator_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::betting_market_position_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::global_betting_statistics_id_type ) FC_REFLECT_TYPENAME( graphene::chain::tournament_details_id_type ) FC_REFLECT( graphene::chain::void_t, ) diff --git a/libraries/chain/include/graphene/chain/protocol/vote.hpp b/libraries/chain/include/graphene/chain/protocol/vote.hpp index b93b9d03..215d4902 100644 --- a/libraries/chain/include/graphene/chain/protocol/vote.hpp +++ b/libraries/chain/include/graphene/chain/protocol/vote.hpp @@ -146,7 +146,6 @@ void from_variant( const fc::variant& var, graphene::chain::vote_id_type& vo ); } // fc -FC_REFLECT_TYPENAME( graphene::chain::vote_id_type::vote_type ) FC_REFLECT_TYPENAME( fc::flat_set ) FC_REFLECT_ENUM( graphene::chain::vote_id_type::vote_type, (witness)(committee)(worker)(VOTE_TYPE_COUNT) ) diff --git a/libraries/chain/include/graphene/chain/sport_evaluator.hpp b/libraries/chain/include/graphene/chain/sport_evaluator.hpp new file mode 100644 index 00000000..a35dc4e7 --- /dev/null +++ b/libraries/chain/include/graphene/chain/sport_evaluator.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace graphene { namespace chain { + + class sport_create_evaluator : public evaluator + { + public: + typedef sport_create_operation operation_type; + + void_result do_evaluate( const sport_create_operation& o ); + object_id_type do_apply( const sport_create_operation& o ); + }; + + class sport_update_evaluator : public evaluator + { + public: + typedef sport_update_operation operation_type; + + void_result do_evaluate( const sport_update_operation& o ); + void_result do_apply( const sport_update_operation& o ); + }; + +} } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/sport_object.hpp b/libraries/chain/include/graphene/chain/sport_object.hpp new file mode 100644 index 00000000..314ebf17 --- /dev/null +++ b/libraries/chain/include/graphene/chain/sport_object.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +namespace graphene { namespace chain { + +class database; + +class sport_object : public graphene::db::abstract_object< sport_object > +{ + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = sport_object_type; + + internationalized_string_type name; +}; + +typedef multi_index_container< + sport_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > > > > sport_object_multi_index_type; + +typedef generic_index sport_object_index; +} } // graphene::chain + +FC_REFLECT_DERIVED( graphene::chain::sport_object, (graphene::db::object), (name) ) diff --git a/libraries/chain/market_evaluator.cpp b/libraries/chain/market_evaluator.cpp index 4f219565..60e07669 100644 --- a/libraries/chain/market_evaluator.cpp +++ b/libraries/chain/market_evaluator.cpp @@ -35,6 +35,7 @@ #include #include +#include namespace graphene { namespace chain { void_result limit_order_create_evaluator::do_evaluate(const limit_order_create_operation& op) diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index a64a38dd..2b543954 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -85,6 +85,7 @@ object_id_type proposal_create_evaluator::do_apply(const proposal_create_operati const proposal_object& proposal = d.create([&](proposal_object& proposal) { _proposed_trx.expiration = o.expiration_time; proposal.proposed_transaction = _proposed_trx; + proposal.proposer = o.fee_paying_account; proposal.expiration_time = o.expiration_time; if( o.review_period_seconds ) proposal.review_period_time = o.expiration_time - *o.review_period_seconds; diff --git a/libraries/chain/protocol/betting_market.cpp b/libraries/chain/protocol/betting_market.cpp new file mode 100644 index 00000000..7517c098 --- /dev/null +++ b/libraries/chain/protocol/betting_market.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +namespace graphene { namespace chain { + +void betting_market_rules_create_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void betting_market_rules_update_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void betting_market_group_create_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void betting_market_group_update_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void betting_market_create_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void betting_market_update_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void betting_market_group_resolve_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void betting_market_group_cancel_unmatched_bets_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void bet_place_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void bet_cancel_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + + + +} } // graphene::chain + diff --git a/libraries/p2p/peer_connection.cpp b/libraries/chain/protocol/competitor.cpp similarity index 85% rename from libraries/p2p/peer_connection.cpp rename to libraries/chain/protocol/competitor.cpp index 0f5b6e0c..0767720a 100644 --- a/libraries/p2p/peer_connection.cpp +++ b/libraries/chain/protocol/competitor.cpp @@ -21,10 +21,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -#include +#include -namespace graphene { namespace p2p { +namespace graphene { namespace chain { -} } //graphene::p2p +void competitor_create_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} +} } // graphene::chain + diff --git a/libraries/chain/protocol/event.cpp b/libraries/chain/protocol/event.cpp new file mode 100644 index 00000000..143404bc --- /dev/null +++ b/libraries/chain/protocol/event.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +namespace graphene { namespace chain { + +void event_create_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void event_update_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void event_update_status_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +} } // graphene::chain + diff --git a/libraries/chain/protocol/event_group.cpp b/libraries/chain/protocol/event_group.cpp new file mode 100644 index 00000000..ed003ef6 --- /dev/null +++ b/libraries/chain/protocol/event_group.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +namespace graphene { namespace chain { + +void event_group_create_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void event_group_update_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +} } // graphene::chain + diff --git a/libraries/chain/protocol/fee_schedule.cpp b/libraries/chain/protocol/fee_schedule.cpp index abded517..136371ba 100644 --- a/libraries/chain/protocol/fee_schedule.cpp +++ b/libraries/chain/protocol/fee_schedule.cpp @@ -189,6 +189,11 @@ namespace graphene { namespace chain { FC_ASSERT( maximum_proposal_lifetime - committee_proposal_review_period > block_interval, "Committee proposal review period must be less than the maximum proposal lifetime" ); + FC_ASSERT( min_bet_multiplier >= GRAPHENE_BETTING_MIN_MULTIPLIER && + min_bet_multiplier <= GRAPHENE_BETTING_MAX_MULTIPLIER ); + FC_ASSERT( max_bet_multiplier >= GRAPHENE_BETTING_MIN_MULTIPLIER && + max_bet_multiplier <= GRAPHENE_BETTING_MAX_MULTIPLIER ); + FC_ASSERT( min_bet_multiplier < max_bet_multiplier ); FC_ASSERT( rake_fee_percentage >= TOURNAMENT_MINIMAL_RAKE_FEE_PERCENTAGE, "Rake fee percentage must not be less than ${min}", ("min",TOURNAMENT_MINIMAL_RAKE_FEE_PERCENTAGE)); FC_ASSERT( rake_fee_percentage <= TOURNAMENT_MAXIMAL_RAKE_FEE_PERCENTAGE, diff --git a/libraries/chain/protocol/sport.cpp b/libraries/chain/protocol/sport.cpp new file mode 100644 index 00000000..1ee07fa3 --- /dev/null +++ b/libraries/chain/protocol/sport.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include + +namespace graphene { namespace chain { + +void sport_create_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + +void sport_update_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); +} + + +} } // graphene::chain + diff --git a/libraries/chain/protocol/transaction.cpp b/libraries/chain/protocol/transaction.cpp index 5de878ea..5faf1c0a 100644 --- a/libraries/chain/protocol/transaction.cpp +++ b/libraries/chain/protocol/transaction.cpp @@ -193,7 +193,7 @@ struct sign_state if( approved_by.find(a.first) == approved_by.end() ) { if( depth == max_recursion ) - return false; + continue; if( check_authority( get_active( a.first ), depth+1 ) ) { approved_by.insert( a.first ); diff --git a/libraries/chain/sport_evaluator.cpp b/libraries/chain/sport_evaluator.cpp new file mode 100644 index 00000000..afc19da2 --- /dev/null +++ b/libraries/chain/sport_evaluator.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + +void_result sport_create_evaluator::do_evaluate(const sport_create_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +object_id_type sport_create_evaluator::do_apply(const sport_create_operation& op) +{ try { + const sport_object& new_sport = + db().create( [&]( sport_object& sport_obj ) { + sport_obj.name = op.name; + }); + return new_sport.id; +} FC_CAPTURE_AND_RETHROW( (op) ) } + + +void_result sport_update_evaluator::do_evaluate(const sport_update_operation& op) +{ try { + FC_ASSERT(db().head_block_time() >= HARDFORK_1000_TIME); + FC_ASSERT(trx_state->_is_proposed_trx); + FC_ASSERT(op.new_name.valid()); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + + void_result sport_update_evaluator::do_apply(const sport_update_operation& op) +{ try { + database& _db = db(); + _db.modify( + _db.get(op.sport_id), + [&]( sport_object& spo ) + { + if( op.new_name.valid() ) + spo.name = *op.new_name; + }); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + + +} } // graphene::chain diff --git a/libraries/chain/tournament_object.cpp b/libraries/chain/tournament_object.cpp index b71909e2..86049b81 100644 --- a/libraries/chain/tournament_object.cpp +++ b/libraries/chain/tournament_object.cpp @@ -304,53 +304,51 @@ namespace graphene { namespace chain { const tournament_details_object& details = tournament_obj.tournament_details_id(event.db); share_type total_prize = 0; for (const auto& payer_pair : details.payers) - { - total_prize += payer_pair.second; - } + total_prize += payer_pair.second; assert(total_prize == tournament_obj.prize_pool); #endif assert(event.match.match_winners.size() == 1); const account_id_type& winner = *event.match.match_winners.begin(); uint16_t rake_fee_percentage = event.db.get_global_properties().parameters.rake_fee_percentage; + + // check whether the core asset pays dividends. If so, we transfer the rake fee + // to the core asset's dividend account + const asset_object& core_asset_obj = asset_id_type()(event.db); + optional dividend_id = core_asset_obj.dividend_data_id; + share_type rake_amount = 0; - - const asset_object & asset_obj = tournament_obj.options.buy_in.asset_id(event.db); - optional dividend_id = asset_obj.dividend_data_id; - - if (dividend_id.valid()) - { - rake_amount = (fc::uint128_t(tournament_obj.prize_pool.value) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100).to_uint64(); - } + if (dividend_id) + rake_amount = (fc::uint128_t(tournament_obj.prize_pool.value) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100).to_uint64(); asset won_prize(tournament_obj.prize_pool - rake_amount, tournament_obj.options.buy_in.asset_id); tournament_payout_operation op; if (won_prize.amount.value) { - // Adjusting balance of winner - event.db.adjust_balance(winner, won_prize); + // Adjusting balance of winner + event.db.adjust_balance(winner, won_prize); - // Generating a virtual operation that shows the payment - op.tournament_id = tournament_obj.id; - op.payout_amount = won_prize; - op.payout_account_id = winner; - op.type = payout_type::prize_award; - event.db.push_applied_operation(op); + // Generating a virtual operation that shows the payment + op.tournament_id = tournament_obj.id; + op.payout_amount = won_prize; + op.payout_account_id = winner; + op.type = payout_type::prize_award; + event.db.push_applied_operation(op); } - if (dividend_id.valid() && rake_amount.value) + if (dividend_id && rake_amount.value) { - // Adjusting balance of dividend_distribution_account - const asset_dividend_data_id_type& asset_dividend_data_id_= *dividend_id; - const asset_dividend_data_object& dividend_obj = asset_dividend_data_id_(event.db); - const account_id_type& rake_account_id = dividend_obj.dividend_distribution_account; - asset rake(rake_amount, tournament_obj.options.buy_in.asset_id); - event.db.adjust_balance(rake_account_id, rake); + // Adjusting balance of dividend_distribution_account + const asset_dividend_data_id_type& asset_dividend_data_id_= *dividend_id; + const asset_dividend_data_object& dividend_obj = asset_dividend_data_id_(event.db); + const account_id_type& rake_account_id = dividend_obj.dividend_distribution_account; + asset rake(rake_amount, tournament_obj.options.buy_in.asset_id); + event.db.adjust_balance(rake_account_id, rake); - // Generating a virtual operation that shows the payment - op.payout_amount = rake; - op.payout_account_id = rake_account_id; - op.type = payout_type::rake_fee; - event.db.push_applied_operation(op); + // Generating a virtual operation that shows the payment + op.payout_amount = rake; + op.payout_account_id = rake_account_id; + op.type = payout_type::rake_fee; + event.db.push_applied_operation(op); } } }; diff --git a/libraries/chain/vesting_balance_object.cpp b/libraries/chain/vesting_balance_object.cpp index 8414840c..73448e04 100644 --- a/libraries/chain/vesting_balance_object.cpp +++ b/libraries/chain/vesting_balance_object.cpp @@ -64,7 +64,7 @@ asset linear_vesting_policy::get_allowed_withdraw( const vesting_policy_context& } } - return asset( allowed_withdraw, ctx.amount.asset_id ); + return asset( allowed_withdraw, ctx.balance.asset_id ); } void linear_vesting_policy::on_deposit(const vesting_policy_context& ctx) diff --git a/libraries/db/CMakeLists.txt b/libraries/db/CMakeLists.txt index 986fe9cb..6feb985c 100644 --- a/libraries/db/CMakeLists.txt +++ b/libraries/db/CMakeLists.txt @@ -10,3 +10,4 @@ install( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +install( FILES ${HEADERS} DESTINATION "include/graphene/db" ) diff --git a/libraries/db/include/graphene/db/generic_index.hpp b/libraries/db/include/graphene/db/generic_index.hpp index 96e982ef..8a433264 100644 --- a/libraries/db/include/graphene/db/generic_index.hpp +++ b/libraries/db/include/graphene/db/generic_index.hpp @@ -80,6 +80,8 @@ namespace graphene { namespace chain { virtual const object* find( object_id_type id )const override { + static_assert(std::is_same::value, + "First index of MultiIndexType MUST be object_id_type!"); auto itr = _indices.find( id ); if( itr == _indices.end() ) return nullptr; return &*itr; diff --git a/libraries/db/include/graphene/db/index.hpp b/libraries/db/include/graphene/db/index.hpp index 7ecf5a66..aebdb8b9 100644 --- a/libraries/db/include/graphene/db/index.hpp +++ b/libraries/db/include/graphene/db/index.hpp @@ -165,9 +165,10 @@ namespace graphene { namespace db { void on_modify( const object& obj ); template - void add_secondary_index() + T* add_secondary_index() { _sindex.emplace_back( new T() ); + return static_cast(_sindex.back().get()); } template diff --git a/libraries/deterministic_openssl_rand/CMakeLists.txt b/libraries/deterministic_openssl_rand/CMakeLists.txt index 13ef69a0..1d9c5870 100644 --- a/libraries/deterministic_openssl_rand/CMakeLists.txt +++ b/libraries/deterministic_openssl_rand/CMakeLists.txt @@ -25,3 +25,4 @@ install( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +install( FILES ${headers} DESTINATION "include/graphene/deterministic_openssl_rand" ) diff --git a/libraries/net/CMakeLists.txt b/libraries/net/CMakeLists.txt index 5fde5373..39f9cd05 100644 --- a/libraries/net/CMakeLists.txt +++ b/libraries/net/CMakeLists.txt @@ -32,3 +32,4 @@ install( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +install( FILES ${HEADERS} DESTINATION "include/graphene/net" ) diff --git a/libraries/net/include/graphene/net/config.hpp b/libraries/net/include/graphene/net/config.hpp index 88f51638..1d400bcf 100644 --- a/libraries/net/include/graphene/net/config.hpp +++ b/libraries/net/include/graphene/net/config.hpp @@ -49,6 +49,9 @@ #define GRAPHENE_NET_PEER_DISCONNECT_TIMEOUT 20 +/* uncomment next line to use testnet seed ip and port */ +//#define GRAPHENE_TEST_NETWORK 1 + #define GRAPHENE_NET_TEST_SEED_IP "104.236.44.210" // autogenerated #define GRAPHENE_NET_TEST_P2P_PORT 1700 #define GRAPHENE_NET_DEFAULT_P2P_PORT 2776 diff --git a/libraries/net/include/graphene/net/message.hpp b/libraries/net/include/graphene/net/message.hpp index 9cbc0af9..cfef1519 100644 --- a/libraries/net/include/graphene/net/message.hpp +++ b/libraries/net/include/graphene/net/message.hpp @@ -39,8 +39,8 @@ namespace graphene { namespace net { */ struct message_header { - uint32_t size; // number of bytes in message, capped at MAX_MESSAGE_SIZE - uint32_t msg_type; // every channel gets a 16 bit message type specifier + uint32_t size = 0; // number of bytes in message, capped at MAX_MESSAGE_SIZE + uint32_t msg_type = 0; // every channel gets a 16 bit message type specifier }; typedef fc::uint160_t message_hash_type; diff --git a/libraries/net/include/graphene/net/node.hpp b/libraries/net/include/graphene/net/node.hpp index 1e01904f..e17af148 100644 --- a/libraries/net/include/graphene/net/node.hpp +++ b/libraries/net/include/graphene/net/node.hpp @@ -158,9 +158,6 @@ namespace graphene { namespace net { */ virtual fc::time_point_sec get_block_time(const item_hash_t& block_id) = 0; - /** returns graphene::blockchain::now() */ - virtual fc::time_point_sec get_blockchain_now() = 0; - virtual item_hash_t get_head_block_id() const = 0; virtual uint32_t estimate_last_known_fork_from_git_revision_timestamp(uint32_t unix_timestamp) const = 0; diff --git a/libraries/net/include/graphene/net/peer_connection.hpp b/libraries/net/include/graphene/net/peer_connection.hpp index 7cfa316a..6f9a4b20 100644 --- a/libraries/net/include/graphene/net/peer_connection.hpp +++ b/libraries/net/include/graphene/net/peer_connection.hpp @@ -166,25 +166,25 @@ namespace graphene { namespace net }; - size_t _total_queued_messages_size; + size_t _total_queued_messages_size = 0; std::queue, std::list > > _queued_messages; fc::future _send_queued_messages_done; public: fc::time_point connection_initiation_time; fc::time_point connection_closed_time; fc::time_point connection_terminated_time; - peer_connection_direction direction; + peer_connection_direction direction = peer_connection_direction::unknown; //connection_state state; - firewalled_state is_firewalled; + firewalled_state is_firewalled = firewalled_state::unknown; fc::microseconds clock_offset; fc::microseconds round_trip_delay; - our_connection_state our_state; - bool they_have_requested_close; - their_connection_state their_state; - bool we_have_requested_close; + our_connection_state our_state = our_connection_state::disconnected; + bool they_have_requested_close = false; + their_connection_state their_state = their_connection_state::disconnected; + bool we_have_requested_close = false; - connection_negotiation_status negotiation_status; + connection_negotiation_status negotiation_status = connection_negotiation_status::disconnected; fc::oexception connection_closed_error; fc::time_point get_connection_time()const { return _message_connection.get_connection_time(); } @@ -199,7 +199,7 @@ namespace graphene { namespace net * from the user_data field of the hello, or if none is present it will be filled with a * copy of node_public_key */ node_id_t node_id; - uint32_t core_protocol_version; + uint32_t core_protocol_version = 0; std::string user_agent; fc::optional graphene_git_revision_sha; fc::optional graphene_git_revision_unix_timestamp; @@ -212,8 +212,8 @@ namespace graphene { namespace net // its hello message. For outbound, they record what we sent the peer // in our hello message fc::ip::address inbound_address; - uint16_t inbound_port; - uint16_t outbound_port; + uint16_t inbound_port = 0; + uint16_t outbound_port = 0; /// @} typedef std::unordered_map item_to_time_map_type; @@ -222,14 +222,14 @@ namespace graphene { namespace net /// @{ boost::container::deque ids_of_items_to_get; /// id of items in the blockchain that this peer has told us about std::set ids_of_items_being_processed; /// list of all items this peer has offered use that we've already handed to the client but the client hasn't finished processing - uint32_t number_of_unfetched_item_ids; /// number of items in the blockchain that follow ids_of_items_to_get but the peer hasn't yet told us their ids - bool peer_needs_sync_items_from_us; - bool we_need_sync_items_from_peer; + uint32_t number_of_unfetched_item_ids = 0; /// number of items in the blockchain that follow ids_of_items_to_get but the peer hasn't yet told us their ids + bool peer_needs_sync_items_from_us = false; + bool we_need_sync_items_from_peer = false; fc::optional, fc::time_point> > item_ids_requested_from_peer; /// we check this to detect a timed-out request and in busy() item_to_time_map_type sync_items_requested_from_peer; /// ids of blocks we've requested from this peer during sync. fetch from another peer if this peer disconnects item_hash_t last_block_delegate_has_seen; /// the hash of the last block this peer has told us about that the peer knows fc::time_point_sec last_block_time_delegate_has_seen; - bool inhibit_fetching_sync_blocks; + bool inhibit_fetching_sync_blocks = false; /// @} /// non-synchronization state data @@ -259,16 +259,17 @@ namespace graphene { namespace net // blockchain catch up fc::time_point transaction_fetching_inhibited_until; - uint32_t last_known_fork_block_number; + uint32_t last_known_fork_block_number = 0; fc::future accept_or_connect_task_done; - firewall_check_state_data *firewall_check_state; + firewall_check_state_data *firewall_check_state = nullptr; #ifndef NDEBUG private: - fc::thread* _thread; - unsigned _send_message_queue_tasks_running; // temporary debugging + fc::thread* _thread = nullptr; + unsigned _send_message_queue_tasks_running = 0; // temporary debugging #endif + bool _currently_handling_message = false; // true while we're in the middle of handling a message from the remote system private: peer_connection(peer_connection_delegate* delegate); void destroy(); @@ -299,8 +300,9 @@ namespace graphene { namespace net fc::ip::endpoint get_local_endpoint(); void set_remote_endpoint(fc::optional new_remote_endpoint); - bool busy(); - bool idle(); + bool busy() const; + bool idle() const; + bool is_currently_handling_message() const; bool is_transaction_fetching_inhibited() const; fc::sha512 get_shared_secret() const; diff --git a/libraries/net/message_oriented_connection.cpp b/libraries/net/message_oriented_connection.cpp index 5808a038..5dea08d4 100644 --- a/libraries/net/message_oriented_connection.cpp +++ b/libraries/net/message_oriented_connection.cpp @@ -32,6 +32,8 @@ #include #include +#include + #ifdef DEFAULT_LOGGER # undef DEFAULT_LOGGER #endif @@ -61,7 +63,6 @@ namespace graphene { namespace net { fc::time_point _last_message_sent_time; bool _send_message_in_progress; - #ifndef NDEBUG fc::thread* _thread; #endif @@ -137,7 +138,6 @@ namespace graphene { namespace net { _sock.bind(local_endpoint); } - void message_oriented_connection_impl::read_loop() { VERIFY_CORRECT_THREAD(); @@ -261,8 +261,13 @@ namespace graphene { namespace net { //pad the message we send to a multiple of 16 bytes size_t size_with_padding = 16 * ((size_of_message_and_header + 15) / 16); std::unique_ptr padded_message(new char[size_with_padding]); + memcpy(padded_message.get(), (char*)&message_to_send, sizeof(message_header)); memcpy(padded_message.get() + sizeof(message_header), message_to_send.data.data(), message_to_send.size ); + char* paddingSpace = padded_message.get() + sizeof(message_header) + message_to_send.size; + size_t toClean = size_with_padding - size_of_message_and_header; + memset(paddingSpace, 0, toClean); + _sock.write(padded_message.get(), size_with_padding); _sock.flush(); _bytes_sent += size_with_padding; diff --git a/libraries/net/node.cpp b/libraries/net/node.cpp index 6fb212c7..9d8b9529 100644 --- a/libraries/net/node.cpp +++ b/libraries/net/node.cpp @@ -396,7 +396,6 @@ namespace graphene { namespace net { namespace detail { void connection_count_changed( uint32_t c ) override; uint32_t get_block_number(const item_hash_t& block_id) override; fc::time_point_sec get_block_time(const item_hash_t& block_id) override; - fc::time_point_sec get_blockchain_now() override; item_hash_t get_head_block_id() const override; uint32_t estimate_last_known_fork_from_git_revision_timestamp(uint32_t unix_timestamp) const override; void error_encountered(const std::string& message, const fc::oexception& error) override; @@ -975,10 +974,7 @@ namespace graphene { namespace net { namespace detail { { throw; } - catch (const fc::exception& e) - { - elog("${e}", ("e", e)); - } + FC_CAPTURE_AND_LOG( (0) ) }// while(!canceled) } @@ -1425,6 +1421,19 @@ namespace graphene { namespace net { namespace detail { wlog( "Sending a keepalive message to peer ${peer} who hasn't sent us any messages in the last ${timeout} seconds", ( "peer", active_peer->get_remote_endpoint() )("timeout", active_send_keepalive_timeout ) ); peers_to_send_keep_alive.push_back(active_peer); + } + else if (active_peer->we_need_sync_items_from_peer && + !active_peer->is_currently_handling_message() && + !active_peer->item_ids_requested_from_peer && + active_peer->ids_of_items_to_get.empty()) + { + // This is a state we should never get into in the first place, but if we do, we should disconnect the peer + // to re-establish the connection. + fc_wlog(fc::logger::get("sync"), "Disconnecting peer ${peer} because we think we need blocks from them but sync has stalled.", + ("peer", active_peer->get_remote_endpoint())); + wlog("Disconnecting peer ${peer} because we think we need blocks from them but sync has stalled.", + ("peer", active_peer->get_remote_endpoint())); + peers_to_disconnect_forcibly.push_back(active_peer); } } } @@ -2456,24 +2465,24 @@ namespace graphene { namespace net { namespace detail { uint32_t expected_num = first_block_number_in_reponse + i; if (actual_num != expected_num) { - wlog("Invalid response from peer ${peer_endpoint}. The list of blocks they provided is not sequential, " - "the ${position}th block in their reply was block number ${actual_num}, " - "but it should have been number ${expected_num}", - ("peer_endpoint", originating_peer->get_remote_endpoint()) - ("position", i) - ("actual_num", actual_num) - ("expected_num", expected_num)); - fc::exception error_for_peer(FC_LOG_MESSAGE(error, - "You gave an invalid response to my request for sync blocks. The list of blocks you provided is not sequential, " - "the ${position}th block in their reply was block number ${actual_num}, " - "but it should have been number ${expected_num}", - ("position", i) - ("actual_num", actual_num) - ("expected_num", expected_num))); - disconnect_from_peer(originating_peer, - "You gave an invalid response to my request for sync blocks", - true, error_for_peer); - return; + wlog("Invalid response from peer ${peer_endpoint}. The list of blocks they provided is not sequential, " + "the ${position}th block in their reply was block number ${actual_num}, " + "but it should have been number ${expected_num}", + ("peer_endpoint", originating_peer->get_remote_endpoint()) + ("position", i) + ("actual_num", actual_num) + ("expected_num", expected_num)); + fc::exception error_for_peer(FC_LOG_MESSAGE(error, + "You gave an invalid response to my request for sync blocks. The list of blocks you provided is not sequential, " + "the ${position}th block in their reply was block number ${actual_num}, " + "but it should have been number ${expected_num}", + ("position", i) + ("actual_num", actual_num) + ("expected_num", expected_num))); + disconnect_from_peer(originating_peer, + "You gave an invalid response to my request for sync blocks", + true, error_for_peer); + return; } } @@ -2520,180 +2529,205 @@ namespace graphene { namespace net { namespace detail { } originating_peer->item_ids_requested_from_peer.reset(); - dlog( "sync: received a list of ${count} available items from ${peer_endpoint}", - ( "count", blockchain_item_ids_inventory_message_received.item_hashes_available.size() ) - ( "peer_endpoint", originating_peer->get_remote_endpoint() ) ); - //for( const item_hash_t& item_hash : blockchain_item_ids_inventory_message_received.item_hashes_available ) - //{ - // dlog( "sync: ${hash}", ("hash", item_hash ) ); - //} - - // if the peer doesn't have any items after the one we asked for - if( blockchain_item_ids_inventory_message_received.total_remaining_item_count == 0 && - ( blockchain_item_ids_inventory_message_received.item_hashes_available.empty() || // there are no items in the peer's blockchain. this should only happen if our blockchain was empty when we requested, might want to verify that. - ( blockchain_item_ids_inventory_message_received.item_hashes_available.size() == 1 && - _delegate->has_item( item_id(blockchain_item_ids_inventory_message_received.item_type, - blockchain_item_ids_inventory_message_received.item_hashes_available.front() ) ) ) ) && // we've already seen the last item in the peer's blockchain - originating_peer->ids_of_items_to_get.empty() && - originating_peer->number_of_unfetched_item_ids == 0 ) // <-- is the last check necessary? + // if exceptions are throw after clearing the item_ids_requested_from_peer (above), + // it could leave our sync in a stalled state. Wrap a try/catch around the rest + // of the function so we can log if this ever happens. + try { - dlog( "sync: peer said we're up-to-date, entering normal operation with this peer" ); - originating_peer->we_need_sync_items_from_peer = false; + dlog( "sync: received a list of ${count} available items from ${peer_endpoint}", + ( "count", blockchain_item_ids_inventory_message_received.item_hashes_available.size() ) + ( "peer_endpoint", originating_peer->get_remote_endpoint() ) ); + //for( const item_hash_t& item_hash : blockchain_item_ids_inventory_message_received.item_hashes_available ) + //{ + // dlog( "sync: ${hash}", ("hash", item_hash ) ); + //} - uint32_t new_number_of_unfetched_items = calculate_unsynced_block_count_from_all_peers(); - _total_number_of_unfetched_items = new_number_of_unfetched_items; - if( new_number_of_unfetched_items == 0 ) - _delegate->sync_status( blockchain_item_ids_inventory_message_received.item_type, 0 ); - - return; - } - - std::deque item_hashes_received( blockchain_item_ids_inventory_message_received.item_hashes_available.begin(), - blockchain_item_ids_inventory_message_received.item_hashes_available.end() ); - originating_peer->number_of_unfetched_item_ids = blockchain_item_ids_inventory_message_received.total_remaining_item_count; - // flush any items this peer sent us that we've already received and processed from another peer - if (!item_hashes_received.empty() && - originating_peer->ids_of_items_to_get.empty()) - { - bool is_first_item_for_other_peer = false; - for (const peer_connection_ptr& peer : _active_connections) - if (peer != originating_peer->shared_from_this() && - !peer->ids_of_items_to_get.empty() && - peer->ids_of_items_to_get.front() == blockchain_item_ids_inventory_message_received.item_hashes_available.front()) - { - dlog("The item ${newitem} is the first item for peer ${peer}", - ("newitem", blockchain_item_ids_inventory_message_received.item_hashes_available.front()) - ("peer", peer->get_remote_endpoint())); - is_first_item_for_other_peer = true; - break; - } - dlog("is_first_item_for_other_peer: ${is_first}. item_hashes_received.size() = ${size}", - ("is_first", is_first_item_for_other_peer)("size", item_hashes_received.size())); - if (!is_first_item_for_other_peer) + // if the peer doesn't have any items after the one we asked for + if( blockchain_item_ids_inventory_message_received.total_remaining_item_count == 0 && + ( blockchain_item_ids_inventory_message_received.item_hashes_available.empty() || // there are no items in the peer's blockchain. this should only happen if our blockchain was empty when we requested, might want to verify that. + ( blockchain_item_ids_inventory_message_received.item_hashes_available.size() == 1 && + _delegate->has_item( item_id(blockchain_item_ids_inventory_message_received.item_type, + blockchain_item_ids_inventory_message_received.item_hashes_available.front() ) ) ) ) && // we've already seen the last item in the peer's blockchain + originating_peer->ids_of_items_to_get.empty() && + originating_peer->number_of_unfetched_item_ids == 0 ) // <-- is the last check necessary? { - while (!item_hashes_received.empty() && - _delegate->has_item(item_id(blockchain_item_ids_inventory_message_received.item_type, - item_hashes_received.front()))) + dlog( "sync: peer said we're up-to-date, entering normal operation with this peer" ); + originating_peer->we_need_sync_items_from_peer = false; + + uint32_t new_number_of_unfetched_items = calculate_unsynced_block_count_from_all_peers(); + _total_number_of_unfetched_items = new_number_of_unfetched_items; + if( new_number_of_unfetched_items == 0 ) + _delegate->sync_status( blockchain_item_ids_inventory_message_received.item_type, 0 ); + + return; + } + + std::deque item_hashes_received( blockchain_item_ids_inventory_message_received.item_hashes_available.begin(), + blockchain_item_ids_inventory_message_received.item_hashes_available.end() ); + originating_peer->number_of_unfetched_item_ids = blockchain_item_ids_inventory_message_received.total_remaining_item_count; + // flush any items this peer sent us that we've already received and processed from another peer + if (!item_hashes_received.empty() && + originating_peer->ids_of_items_to_get.empty()) + { + bool is_first_item_for_other_peer = false; + for (const peer_connection_ptr& peer : _active_connections) + if (peer != originating_peer->shared_from_this() && + !peer->ids_of_items_to_get.empty() && + peer->ids_of_items_to_get.front() == blockchain_item_ids_inventory_message_received.item_hashes_available.front()) + { + dlog("The item ${newitem} is the first item for peer ${peer}", + ("newitem", blockchain_item_ids_inventory_message_received.item_hashes_available.front()) + ("peer", peer->get_remote_endpoint())); + is_first_item_for_other_peer = true; + break; + } + dlog("is_first_item_for_other_peer: ${is_first}. item_hashes_received.size() = ${size}", + ("is_first", is_first_item_for_other_peer)("size", item_hashes_received.size())); + if (!is_first_item_for_other_peer) { - assert(item_hashes_received.front() != item_hash_t()); + while (!item_hashes_received.empty() && + _delegate->has_item(item_id(blockchain_item_ids_inventory_message_received.item_type, + item_hashes_received.front()))) + { + assert(item_hashes_received.front() != item_hash_t()); + originating_peer->last_block_delegate_has_seen = item_hashes_received.front(); + originating_peer->last_block_time_delegate_has_seen = _delegate->get_block_time(item_hashes_received.front()); + dlog("popping item because delegate has already seen it. peer ${peer}'s last block the delegate has seen is now ${block_id} (actual block #${actual_block_num})", + ("peer", originating_peer->get_remote_endpoint()) + ("block_id", originating_peer->last_block_delegate_has_seen) + ("actual_block_num", _delegate->get_block_number(item_hashes_received.front()))); + + item_hashes_received.pop_front(); + } + dlog("after removing all items we have already seen, item_hashes_received.size() = ${size}", ("size", item_hashes_received.size())); + } + } + else if (!item_hashes_received.empty()) + { + // we received a list of items and we already have a list of items to fetch from this peer. + // In the normal case, this list will immediately follow the existing list, meaning the + // last hash of our existing list will match the first hash of the new list. + + // In the much less likely case, we've received a partial list of items from the peer, then + // the peer switched forks before sending us the remaining list. In this case, the first + // hash in the new list may not be the last hash in the existing list (it may be earlier, or + // it may not exist at all. + + // In either case, pop items off the back of our existing list until we find our first + // item, then append our list. + while (!originating_peer->ids_of_items_to_get.empty()) + { + if (item_hashes_received.front() != originating_peer->ids_of_items_to_get.back()) + originating_peer->ids_of_items_to_get.pop_back(); + else + break; + } + if (originating_peer->ids_of_items_to_get.empty()) + { + // this happens when the peer has switched forks between the last inventory message and + // this one, and there weren't any unfetched items in common + // We don't know where in the blockchain the new front() actually falls, all we can + // expect is that it is a block that we knew about because it should be one of the + // blocks we sent in the initial synopsis. + assert(_delegate->has_item(item_id(_sync_item_type, item_hashes_received.front()))); originating_peer->last_block_delegate_has_seen = item_hashes_received.front(); originating_peer->last_block_time_delegate_has_seen = _delegate->get_block_time(item_hashes_received.front()); - dlog("popping item because delegate has already seen it. peer ${peer}'s last block the delegate has seen is now ${block_id} (actual block #${actual_block_num})", - ("peer", originating_peer->get_remote_endpoint()) - ("block_id", originating_peer->last_block_delegate_has_seen) - ("actual_block_num", _delegate->get_block_number(item_hashes_received.front()))); - item_hashes_received.pop_front(); } - dlog("after removing all items we have already seen, item_hashes_received.size() = ${size}", ("size", item_hashes_received.size())); - } - } - else if (!item_hashes_received.empty()) - { - // we received a list of items and we already have a list of items to fetch from this peer. - // In the normal case, this list will immediately follow the existing list, meaning the - // last hash of our existing list will match the first hash of the new list. - - // In the much less likely case, we've received a partial list of items from the peer, then - // the peer switched forks before sending us the remaining list. In this case, the first - // hash in the new list may not be the last hash in the existing list (it may be earlier, or - // it may not exist at all. - - // In either case, pop items off the back of our existing list until we find our first - // item, then append our list. - while (!originating_peer->ids_of_items_to_get.empty()) - { - if (item_hashes_received.front() != originating_peer->ids_of_items_to_get.back()) - originating_peer->ids_of_items_to_get.pop_back(); else - break; + { + // the common simple case: the new list extends the old. pop off the duplicate element + originating_peer->ids_of_items_to_get.pop_back(); + } } - if (originating_peer->ids_of_items_to_get.empty()) + + if (!item_hashes_received.empty() && !originating_peer->ids_of_items_to_get.empty()) + assert(item_hashes_received.front() != originating_peer->ids_of_items_to_get.back()); + + // append the remaining items to the peer's list + boost::push_back(originating_peer->ids_of_items_to_get, item_hashes_received); + + originating_peer->number_of_unfetched_item_ids = blockchain_item_ids_inventory_message_received.total_remaining_item_count; + + // at any given time, there's a maximum number of blocks that can possibly be out there + // [(now - genesis time) / block interval]. If they offer us more blocks than that, + // they must be an attacker or have a buggy client. + fc::time_point_sec minimum_time_of_last_offered_block = + originating_peer->last_block_time_delegate_has_seen + // timestamp of the block immediately before the first unfetched block + originating_peer->number_of_unfetched_item_ids * GRAPHENE_MIN_BLOCK_INTERVAL; + fc::time_point_sec now = fc::time_point::now(); + if (minimum_time_of_last_offered_block > now + GRAPHENE_NET_FUTURE_SYNC_BLOCKS_GRACE_PERIOD_SEC) { - // this happens when the peer has switched forks between the last inventory message and - // this one, and there weren't any unfetched items in common - // We don't know where in the blockchain the new front() actually falls, all we can - // expect is that it is a block that we knew about because it should be one of the - // blocks we sent in the initial synopsis. - assert(_delegate->has_item(item_id(_sync_item_type, item_hashes_received.front()))); - originating_peer->last_block_delegate_has_seen = item_hashes_received.front(); - originating_peer->last_block_time_delegate_has_seen = _delegate->get_block_time(item_hashes_received.front()); - item_hashes_received.pop_front(); + wlog("Disconnecting from peer ${peer} who offered us an implausible number of blocks, their last block would be in the future (${timestamp})", + ("peer", originating_peer->get_remote_endpoint()) + ("timestamp", minimum_time_of_last_offered_block)); + fc::exception error_for_peer(FC_LOG_MESSAGE(error, "You offered me a list of more sync blocks than could possibly exist. Total blocks offered: ${blocks}, Minimum time of the last block you offered: ${minimum_time_of_last_offered_block}, Now: ${now}", + ("blocks", originating_peer->number_of_unfetched_item_ids) + ("minimum_time_of_last_offered_block", minimum_time_of_last_offered_block) + ("now", now))); + disconnect_from_peer(originating_peer, + "You offered me a list of more sync blocks than could possibly exist", + true, error_for_peer); + return; + } + + uint32_t new_number_of_unfetched_items = calculate_unsynced_block_count_from_all_peers(); + if (new_number_of_unfetched_items != _total_number_of_unfetched_items) + _delegate->sync_status(blockchain_item_ids_inventory_message_received.item_type, + new_number_of_unfetched_items); + _total_number_of_unfetched_items = new_number_of_unfetched_items; + + if (blockchain_item_ids_inventory_message_received.total_remaining_item_count != 0) + { + // the peer hasn't sent us all the items it knows about. + if (originating_peer->ids_of_items_to_get.size() > GRAPHENE_NET_MIN_BLOCK_IDS_TO_PREFETCH) + { + // we have a good number of item ids from this peer, start fetching blocks from it; + // we'll switch back later to finish the job. + trigger_fetch_sync_items_loop(); + } + else + { + // keep fetching the peer's list of sync items until we get enough to switch into block- + // fetchimg mode + fetch_next_batch_of_item_ids_from_peer(originating_peer); + } } else { - // the common simple case: the new list extends the old. pop off the duplicate element - originating_peer->ids_of_items_to_get.pop_back(); + // the peer has told us about all of the items it knows + if (!originating_peer->ids_of_items_to_get.empty()) + { + // we now know about all of the items the peer knows about, and there are some items on the list + // that we should try to fetch. Kick off the fetch loop. + trigger_fetch_sync_items_loop(); + } + else + { + // If we get here, the peer has sent us a non-empty list of items, but we have already + // received all of the items from other peers. Send a new request to the peer to + // see if we're really in sync + fetch_next_batch_of_item_ids_from_peer(originating_peer); + } } } - - if (!item_hashes_received.empty() && !originating_peer->ids_of_items_to_get.empty()) - assert(item_hashes_received.front() != originating_peer->ids_of_items_to_get.back()); - - // append the remaining items to the peer's list - boost::push_back(originating_peer->ids_of_items_to_get, item_hashes_received); - - originating_peer->number_of_unfetched_item_ids = blockchain_item_ids_inventory_message_received.total_remaining_item_count; - - // at any given time, there's a maximum number of blocks that can possibly be out there - // [(now - genesis time) / block interval]. If they offer us more blocks than that, - // they must be an attacker or have a buggy client. - fc::time_point_sec minimum_time_of_last_offered_block = - originating_peer->last_block_time_delegate_has_seen + // timestamp of the block immediately before the first unfetched block - originating_peer->number_of_unfetched_item_ids * GRAPHENE_MIN_BLOCK_INTERVAL; - if (minimum_time_of_last_offered_block > _delegate->get_blockchain_now() + GRAPHENE_NET_FUTURE_SYNC_BLOCKS_GRACE_PERIOD_SEC) + catch (const fc::canceled_exception&) { - wlog("Disconnecting from peer ${peer} who offered us an implausible number of blocks, their last block would be in the future (${timestamp})", - ("peer", originating_peer->get_remote_endpoint()) - ("timestamp", minimum_time_of_last_offered_block)); - fc::exception error_for_peer(FC_LOG_MESSAGE(error, "You offered me a list of more sync blocks than could possibly exist. Total blocks offered: ${blocks}, Minimum time of the last block you offered: ${minimum_time_of_last_offered_block}, Now: ${now}", - ("blocks", originating_peer->number_of_unfetched_item_ids) - ("minimum_time_of_last_offered_block", minimum_time_of_last_offered_block) - ("now", _delegate->get_blockchain_now()))); - disconnect_from_peer(originating_peer, - "You offered me a list of more sync blocks than could possibly exist", - true, error_for_peer); - return; + throw; } - - uint32_t new_number_of_unfetched_items = calculate_unsynced_block_count_from_all_peers(); - if (new_number_of_unfetched_items != _total_number_of_unfetched_items) - _delegate->sync_status(blockchain_item_ids_inventory_message_received.item_type, - new_number_of_unfetched_items); - _total_number_of_unfetched_items = new_number_of_unfetched_items; - - if (blockchain_item_ids_inventory_message_received.total_remaining_item_count != 0) + catch (const fc::exception& e) { - // the peer hasn't sent us all the items it knows about. - if (originating_peer->ids_of_items_to_get.size() > GRAPHENE_NET_MIN_BLOCK_IDS_TO_PREFETCH) - { - // we have a good number of item ids from this peer, start fetching blocks from it; - // we'll switch back later to finish the job. - trigger_fetch_sync_items_loop(); - } - else - { - // keep fetching the peer's list of sync items until we get enough to switch into block- - // fetchimg mode - fetch_next_batch_of_item_ids_from_peer(originating_peer); - } + elog("Caught unexpected exception: ${e}", ("e", e)); + assert(false && "exceptions not expected here"); } - else + catch (const std::exception& e) { - // the peer has told us about all of the items it knows - if (!originating_peer->ids_of_items_to_get.empty()) - { - // we now know about all of the items the peer knows about, and there are some items on the list - // that we should try to fetch. Kick off the fetch loop. - trigger_fetch_sync_items_loop(); - } - else - { - // If we get here, the peer has sent us a non-empty list of items, but we have already - // received all of the items from other peers. Send a new request to the peer to - // see if we're really in sync - fetch_next_batch_of_item_ids_from_peer(originating_peer); - } + elog("Caught unexpected exception: ${e}", ("e", e.what())); + assert(false && "exceptions not expected here"); + } + catch (...) + { + elog("Caught unexpected exception, could break sync operation"); } } else @@ -3270,7 +3304,33 @@ namespace graphene { namespace net { namespace detail { block_processed_this_iteration = true; } else + { dlog("Already received and accepted this block (presumably through normal inventory mechanism), treating it as accepted"); + std::vector< peer_connection_ptr > peers_needing_next_batch; + for (const peer_connection_ptr& peer : _active_connections) + { + auto items_being_processed_iter = peer->ids_of_items_being_processed.find(received_block_iter->block_id); + if (items_being_processed_iter != peer->ids_of_items_being_processed.end()) + { + peer->ids_of_items_being_processed.erase(items_being_processed_iter); + dlog("Removed item from ${endpoint}'s list of items being processed, still processing ${len} blocks", + ("endpoint", peer->get_remote_endpoint())("len", peer->ids_of_items_being_processed.size())); + + // if we just processed the last item in our list from this peer, we will want to + // send another request to find out if we are now in sync (this is normally handled in + // send_sync_block_to_node_delegate) + if (peer->ids_of_items_to_get.empty() && + peer->number_of_unfetched_item_ids == 0 && + peer->ids_of_items_being_processed.empty()) + { + dlog("We received last item in our list for peer ${endpoint}, setup to do a sync check", ("endpoint", peer->get_remote_endpoint())); + peers_needing_next_batch.push_back( peer ); + } + } + } + for( const peer_connection_ptr& peer : peers_needing_next_batch ) + fetch_next_batch_of_item_ids_from_peer(peer.get()); + } break; // start iterating _received_sync_items from the beginning } // end if potential_first_block @@ -3486,19 +3546,43 @@ namespace graphene { namespace net { namespace detail { if (sync_item_iter != originating_peer->sync_items_requested_from_peer.end()) { originating_peer->sync_items_requested_from_peer.erase(sync_item_iter); - _active_sync_requests.erase(block_message_to_process.block_id); - process_block_during_sync(originating_peer, block_message_to_process, message_hash); - if (originating_peer->idle()) + // if exceptions are throw here after removing the sync item from the list (above), + // it could leave our sync in a stalled state. Wrap a try/catch around the rest + // of the function so we can log if this ever happens. + try { - // we have finished fetching a batch of items, so we either need to grab another batch of items - // or we need to get another list of item ids. - if (originating_peer->number_of_unfetched_item_ids > 0 && - originating_peer->ids_of_items_to_get.size() < GRAPHENE_NET_MIN_BLOCK_IDS_TO_PREFETCH) - fetch_next_batch_of_item_ids_from_peer(originating_peer); - else - trigger_fetch_sync_items_loop(); + _active_sync_requests.erase(block_message_to_process.block_id); + process_block_during_sync(originating_peer, block_message_to_process, message_hash); + if (originating_peer->idle()) + { + // we have finished fetching a batch of items, so we either need to grab another batch of items + // or we need to get another list of item ids. + if (originating_peer->number_of_unfetched_item_ids > 0 && + originating_peer->ids_of_items_to_get.size() < GRAPHENE_NET_MIN_BLOCK_IDS_TO_PREFETCH) + fetch_next_batch_of_item_ids_from_peer(originating_peer); + else + trigger_fetch_sync_items_loop(); + } + return; + } + catch (const fc::canceled_exception& e) + { + throw; + } + catch (const fc::exception& e) + { + elog("Caught unexpected exception: ${e}", ("e", e)); + assert(false && "exceptions not expected here"); + } + catch (const std::exception& e) + { + elog("Caught unexpected exception: ${e}", ("e", e.what())); + assert(false && "exceptions not expected here"); + } + catch (...) + { + elog("Caught unexpected exception, could break sync operation"); } - return; } } @@ -4192,7 +4276,7 @@ namespace graphene { namespace net { namespace detail { // limit the rate at which we accept connections to mitigate DOS attacks fc::usleep( fc::milliseconds(10) ); - } FC_CAPTURE_AND_RETHROW() + } FC_CAPTURE_AND_LOG( (0) ) } } // accept_loop() @@ -4393,7 +4477,7 @@ namespace graphene { namespace net { namespace detail { _node_configuration = detail::node_configuration(); #ifdef GRAPHENE_TEST_NETWORK - uint32_t port = GRAPHENE_NET_TEST_P2P_PORT + GRAPHENE_TEST_NETWORK_VERSION; + uint32_t port = GRAPHENE_NET_TEST_P2P_PORT; #else uint32_t port = GRAPHENE_NET_DEFAULT_P2P_PORT; #endif @@ -4489,7 +4573,8 @@ namespace graphene { namespace net { namespace detail { error_message_stream << "\nStill waiting for port " << listen_endpoint.port() << " to become available\n"; } std::string error_message = error_message_stream.str(); - ulog(error_message); + wlog(error_message); + std::cout << "\033[31m" << error_message; _delegate->error_encountered( error_message, fc::oexception() ); fc::usleep( fc::seconds(5 ) ); } @@ -5335,17 +5420,21 @@ namespace graphene { namespace net { namespace detail { # define INVOKE_AND_COLLECT_STATISTICS(method_name, ...) \ try \ { \ - call_statistics_collector statistics_collector(#method_name, \ - &_ ## method_name ## _execution_accumulator, \ - &_ ## method_name ## _delay_before_accumulator, \ - &_ ## method_name ## _delay_after_accumulator); \ if (_thread->is_current()) \ { \ + call_statistics_collector statistics_collector(#method_name, \ + &_ ## method_name ## _execution_accumulator, \ + &_ ## method_name ## _delay_before_accumulator, \ + &_ ## method_name ## _delay_after_accumulator); \ call_statistics_collector::actual_execution_measurement_helper helper(statistics_collector); \ return _node_delegate->method_name(__VA_ARGS__); \ } \ else \ return _thread->async([&](){ \ + call_statistics_collector statistics_collector(#method_name, \ + &_ ## method_name ## _execution_accumulator, \ + &_ ## method_name ## _delay_before_accumulator, \ + &_ ## method_name ## _delay_after_accumulator); \ call_statistics_collector::actual_execution_measurement_helper helper(statistics_collector); \ return _node_delegate->method_name(__VA_ARGS__); \ }, "invoke " BOOST_STRINGIZE(method_name)).wait(); \ @@ -5367,17 +5456,21 @@ namespace graphene { namespace net { namespace detail { } #else # define INVOKE_AND_COLLECT_STATISTICS(method_name, ...) \ - call_statistics_collector statistics_collector(#method_name, \ - &_ ## method_name ## _execution_accumulator, \ - &_ ## method_name ## _delay_before_accumulator, \ - &_ ## method_name ## _delay_after_accumulator); \ if (_thread->is_current()) \ { \ + call_statistics_collector statistics_collector(#method_name, \ + &_ ## method_name ## _execution_accumulator, \ + &_ ## method_name ## _delay_before_accumulator, \ + &_ ## method_name ## _delay_after_accumulator); \ call_statistics_collector::actual_execution_measurement_helper helper(statistics_collector); \ return _node_delegate->method_name(__VA_ARGS__); \ } \ else \ return _thread->async([&](){ \ + call_statistics_collector statistics_collector(#method_name, \ + &_ ## method_name ## _execution_accumulator, \ + &_ ## method_name ## _delay_before_accumulator, \ + &_ ## method_name ## _delay_after_accumulator); \ call_statistics_collector::actual_execution_measurement_helper helper(statistics_collector); \ return _node_delegate->method_name(__VA_ARGS__); \ }, "invoke " BOOST_STRINGIZE(method_name)).wait() @@ -5447,14 +5540,6 @@ namespace graphene { namespace net { namespace detail { INVOKE_AND_COLLECT_STATISTICS(get_block_time, block_id); } - /** returns graphene::blockchain::now() */ - fc::time_point_sec statistics_gathering_node_delegate_wrapper::get_blockchain_now() - { - // this function doesn't need to block, - ASSERT_TASK_NOT_PREEMPTED(); - return _node_delegate->get_blockchain_now(); - } - item_hash_t statistics_gathering_node_delegate_wrapper::get_head_block_id() const { INVOKE_AND_COLLECT_STATISTICS(get_head_block_id); diff --git a/libraries/net/peer_connection.cpp b/libraries/net/peer_connection.cpp index 4dfcec3b..f1f20d3f 100644 --- a/libraries/net/peer_connection.cpp +++ b/libraries/net/peer_connection.cpp @@ -29,6 +29,8 @@ #include +#include + #ifdef DEFAULT_LOGGER # undef DEFAULT_LOGGER #endif @@ -86,11 +88,12 @@ namespace graphene { namespace net inhibit_fetching_sync_blocks(false), transaction_fetching_inhibited_until(fc::time_point::min()), last_known_fork_block_number(0), - firewall_check_state(nullptr) + firewall_check_state(nullptr), #ifndef NDEBUG - ,_thread(&fc::thread::current()), - _send_message_queue_tasks_running(0) + _thread(&fc::thread::current()), + _send_message_queue_tasks_running(0), #endif + _currently_handling_message(false) { } @@ -265,6 +268,10 @@ namespace graphene { namespace net void peer_connection::on_message( message_oriented_connection* originating_connection, const message& received_message ) { VERIFY_CORRECT_THREAD(); + _currently_handling_message = true; + BOOST_SCOPE_EXIT(this_) { + this_->_currently_handling_message = false; + } BOOST_SCOPE_EXIT_END _node->on_message( this, received_message ); } @@ -438,18 +445,24 @@ namespace graphene { namespace net _remote_endpoint = new_remote_endpoint; } - bool peer_connection::busy() + bool peer_connection::busy() const { VERIFY_CORRECT_THREAD(); return !items_requested_from_peer.empty() || !sync_items_requested_from_peer.empty() || item_ids_requested_from_peer; } - bool peer_connection::idle() + bool peer_connection::idle() const { VERIFY_CORRECT_THREAD(); return !busy(); } + bool peer_connection::is_currently_handling_message() const + { + VERIFY_CORRECT_THREAD(); + return _currently_handling_message; + } + bool peer_connection::is_transaction_fetching_inhibited() const { VERIFY_CORRECT_THREAD(); diff --git a/libraries/p2p/CMakeLists.txt b/libraries/p2p/CMakeLists.txt deleted file mode 100644 index 6b5918d5..00000000 --- a/libraries/p2p/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -file(GLOB HEADERS "include/graphene/p2p/*.hpp") - -set(SOURCES node.cpp - stcp_socket.cpp - peer_connection.cpp - message_oriented_connection.cpp) - -add_library( graphene_p2p ${SOURCES} ${HEADERS} ) - -target_link_libraries( graphene_p2p - PUBLIC fc graphene_db ) -target_include_directories( graphene_p2p - PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" - PRIVATE "${CMAKE_SOURCE_DIR}/libraries/chain/include" -) - -#if(MSVC) -# set_source_files_properties( node.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) -#endif(MSVC) - -#if (USE_PCH) -# set_target_properties(graphene_p2p PROPERTIES COTIRE_ADD_UNITY_BUILD FALSE) -# cotire(graphene_p2p ) -#endif(USE_PCH) - -install( TARGETS - graphene_p2p - - RUNTIME DESTINATION bin - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib -) diff --git a/libraries/p2p/design.md b/libraries/p2p/design.md deleted file mode 100644 index 96653d7e..00000000 --- a/libraries/p2p/design.md +++ /dev/null @@ -1,96 +0,0 @@ -# Network Protocol 2 - -Building a low-latency network requires P2P nodes that have low-latency -connections and a protocol designed to minimize latency. for the purpose -of this document we will assume that two nodes are located on opposite -sides of the globe with a ping time of 250ms. - - -## Announce, Request, Send Protocol -Under the prior network archtiecture, transactions and blocks were broadcast -in a manner similar to the Bitcoin protocol: inventory messages notify peers of -transactions and blocks, then peers fetch the transaction or block from one -peer. After validating the item a node will broadcast an inventory message to -its peers. - -Under this model it will take 0.75 seconds for a peer to communicate a transaction -or block to another peer even if their size was 0 and there was no processing overhead. -This level of performance is unacceptable for a network attempting to produce one block -every second. - -This prior protocol also sent every transaction twice: initial broadcast, and again as -part of a block. - - -## Push Protocol -To minimize latency each node needs to immediately broadcast the data it receives -to its peers after validating it. Given the average transaction size is less than -100 bytes, it is almost as effecient to send the transaction as it is to send -the notice (assuming a 20 byte transaction id) - -Each node implements the following protocol: - - - onReceiveTransaction( from_peer, transaction ) - if( isKnown( transaction.id() ) ) - return - - markKnown( transaction.id() ) - - if( !validate( transaction ) ) - return - - for( peer : peers ) - if( peer != from_peer ) - send( peer, transaction ) - - - onReceiveBlock( from_peer, block_summary ) - if( isKnown( block_summary ) - return - - full_block = reconstructFullBlcok( from_peer, block_summary ) - if( !full_block ) disconnect from_peer - - markKnown( block_summary ) - - if( !pushBlock( full_block ) ) disconnect from_peer - - for( peer : peers ) - if( peer != from_peer ) - send( peer, block_summary ) - - - onHello( new_peer, new_peer_head_block_num ) - - replyHello( new_peer ) // ack the hello message with our timestamp to measure latency - - if( peers.size() >= max_peers ) - send( new_peer, peers ) - disconnect( new_peer ) - return - - while( new_peer_head_block_num < our_head_block_num ) - sendFullBlock( new_peer, ++new_peer_head_block_num ) - - new_peer.synced = true - for( peer : peers ) - send( peer, new_peer ) - - onHelloReply( from_peer, hello_reply ) - update_latency_measure, disconnect if too slow - - onReceivePeers( from_peer, peers ) - addToPotentialPeers( peers ) - - onUpdateConnectionsTimer - if( peers.size() < desired_peers ) - connect( random_potential_peer ) - - onFullBlock( from_peer, full_block ) - if( !pushBlock( full_block ) ) disconnect from_peer - - onStartup - init_potential_peers from config - start onUpdateConnectionsTimer - diff --git a/libraries/p2p/include/graphene/p2p/message.hpp b/libraries/p2p/include/graphene/p2p/message.hpp deleted file mode 100644 index 3a913507..00000000 --- a/libraries/p2p/include/graphene/p2p/message.hpp +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#pragma once -#include -#include -#include -#include -#include -#include - -namespace graphene { namespace p2p { - using namespace graphene::chain; - - struct message_header - { - uint32_t size; // number of bytes in message, capped at MAX_MESSAGE_SIZE - uint32_t msg_type; - }; - - typedef fc::uint160_t message_hash_type; - - /** - * Abstracts the process of packing/unpacking a message for a - * particular channel. - */ - struct message : public message_header - { - std::vector data; - - message(){} - - message( message&& m ) - :message_header(m),data( std::move(m.data) ){} - - message( const message& m ) - :message_header(m),data( m.data ){} - - /** - * Assumes that T::type specifies the message type - */ - template - message( const T& m ) - { - msg_type = T::type; - data = fc::raw::pack(m); - size = (uint32_t)data.size(); - } - - fc::uint160_t id()const - { - return fc::ripemd160::hash( data.data(), (uint32_t)data.size() ); - } - - /** - * Automatically checks the type and deserializes T in the - * opposite process from the constructor. - */ - template - T as()const - { - try { - FC_ASSERT( msg_type == T::type ); - T tmp; - if( data.size() ) - { - fc::datastream ds( data.data(), data.size() ); - fc::raw::unpack( ds, tmp ); - } - else - { - // just to make sure that tmp shouldn't have any data - fc::datastream ds( nullptr, 0 ); - fc::raw::unpack( ds, tmp ); - } - return tmp; - } FC_RETHROW_EXCEPTIONS( warn, - "error unpacking network message as a '${type}' ${x} !=? ${msg_type}", - ("type", fc::get_typename::name() ) - ("x", T::type) - ("msg_type", msg_type) - ); - } - }; - - enum core_message_type_enum { - hello_message_type = 1000, - transaction_message_type = 1001, - block_message_type = 1002, - peer_message_type = 1003, - error_message_type = 1004 - }; - - struct hello_message - { - static const core_message_type_enum type; - - std::string user_agent; - uint16_t version; - fc::time_point timestamp; - - fc::ip::address inbound_address; - uint16_t inbound_port; - uint16_t outbound_port; - public_key_type node_public_key; - fc::sha256 chain_id; - fc::variant_object user_data; - block_id_type head_block; - }; - - struct hello_reply_message - { - static const core_message_type_enum type; - - fc::time_point hello_timestamp; - fc::time_point reply_timestamp; - }; - - struct transaction_message - { - static const core_message_type_enum type; - signed_transaction trx; - }; - - struct block_summary_message - { - static const core_message_type_enum type; - - signed_block_header header; - vector transaction_ids; - }; - - struct full_block_message - { - static const core_message_type_enum type; - signed_block block; - }; - - struct peers_message - { - static const core_message_type_enum type; - - vector peers; - }; - - struct error_message - { - static const core_message_type_enum type; - string message; - }; - - -} } // graphene::p2p - -FC_REFLECT( graphene::p2p::message_header, (size)(msg_type) ) -FC_REFLECT_DERIVED( graphene::p2p::message, (graphene::p2p::message_header), (data) ) -FC_REFLECT_ENUM( graphene::p2p::core_message_type_enum, - (hello_message_type) - (transaction_message_type) - (block_message_type) - (peer_message_type) - (error_message_type) -) diff --git a/libraries/p2p/include/graphene/p2p/message_oriented_connection.hpp b/libraries/p2p/include/graphene/p2p/message_oriented_connection.hpp deleted file mode 100644 index b2a586d1..00000000 --- a/libraries/p2p/include/graphene/p2p/message_oriented_connection.hpp +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#pragma once -#include -#include - -namespace graphene { namespace p2p { - - namespace detail { class message_oriented_connection_impl; } - - class message_oriented_connection; - - /** receives incoming messages from a message_oriented_connection object */ - class message_oriented_connection_delegate - { - public: - virtual void on_message( message_oriented_connection* originating_connection, - const message& received_message) = 0; - - virtual void on_connection_closed(message_oriented_connection* originating_connection) = 0; - }; - - /** uses a secure socket to create a connection that reads and writes a stream of `fc::p2p::message` objects */ - class message_oriented_connection - { - public: - message_oriented_connection(message_oriented_connection_delegate* delegate = nullptr); - ~message_oriented_connection(); - fc::tcp_socket& get_socket(); - - void accept(); - void bind(const fc::ip::endpoint& local_endpoint); - void connect_to(const fc::ip::endpoint& remote_endpoint); - - void send_message(const message& message_to_send); - void close_connection(); - void destroy_connection(); - - uint64_t get_total_bytes_sent() const; - uint64_t get_total_bytes_received() const; - fc::time_point get_last_message_sent_time() const; - fc::time_point get_last_message_received_time() const; - fc::time_point get_connection_time() const; - fc::sha512 get_shared_secret() const; - private: - std::unique_ptr my; - }; - typedef std::shared_ptr message_oriented_connection_ptr; - -} } // graphene::net diff --git a/libraries/p2p/include/graphene/p2p/node.hpp b/libraries/p2p/include/graphene/p2p/node.hpp deleted file mode 100644 index b89c1d54..00000000 --- a/libraries/p2p/include/graphene/p2p/node.hpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#pragma once -#include -#include - - - -namespace graphene { namespace p2p { - using namespace graphene::chain; - - struct node_config - { - fc::ip::endpoint server_endpoint; - bool wait_if_not_available = true; - uint32_t desired_peers; - uint32_t max_peers; - /** receive, but don't rebroadcast data */ - bool subscribe_only = false; - public_key_type node_id; - vector seed_nodes; - }; - - struct by_remote_endpoint; - struct by_peer_id; - - /** - * @ingroup object_index - */ - typedef multi_index_container< - peer_connection_ptr, - indexed_by< - ordered_unique< tag, - const_mem_fun< peer_connection, fc::ip::endpoint, &peer_connection::get_remote_endpoint > >, - ordered_unique< tag, member< peer_connection, public_key_type, &peer_connection::node_id > > - > - > peer_connection_index; - - - class node : public std::enable_shared_from_this - { - public: - server( chain_database& db ); - - void add_peer( const fc::ip::endpoint& ep ); - void configure( const node_config& cfg ); - - void on_incomming_connection( peer_connection_ptr new_peer ); - void on_hello( peer_connection_ptr new_peer, hello_message m ); - void on_transaction( peer_connection_ptr from_peer, transaction_message m ); - void on_block( peer_connection_ptr from_peer, block_message m ); - void on_peers( peer_connection_ptr from_peer, peers_message m ); - void on_error( peer_connection_ptr from_peer, error_message m ); - void on_full_block( peer_connection_ptr from_peer, full_block_message m ); - void on_update_connections(); - - private: - /** - * Specifies the network interface and port upon which incoming - * connections should be accepted. - */ - void listen_on_endpoint( fc::ip::endpoint ep, bool wait_if_not_available ); - void accept_loop(); - - graphene::chain::database& _db; - - fc::tcp_server _tcp_server; - fc::ip::endpoint _actual_listening_endpoint; - fc::future _accept_loop_complete; - peer_connection_index _peers; - - }; - -} } /// graphene::p2p diff --git a/libraries/p2p/include/graphene/p2p/peer_connection.hpp b/libraries/p2p/include/graphene/p2p/peer_connection.hpp deleted file mode 100644 index e120c1c0..00000000 --- a/libraries/p2p/include/graphene/p2p/peer_connection.hpp +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#pragma once - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace graphene { namespace p2p { - - class peer_connection; - class peer_connection_delegate - { - public: - virtual void on_message(peer_connection* originating_peer, const message& received_message) = 0; - virtual void on_connection_closed(peer_connection* originating_peer) = 0; - }; - - class peer_connection; - typedef std::shared_ptr peer_connection_ptr; - - - /** - * Each connection maintains its own queue of messages to be sent, when an item - * is first pushed to the queue it starts an async fiber that will sequentially write - * all items until there is nothing left to be sent. - * - * If a particular connection is unable to keep up with the real-time stream of - * messages to be sent then it will be disconnected. The backlog will be measured in - * seconds. - * - * A multi-index container that tracks the - */ - class peer_connection : public message_oriented_connection_delegate, - public std::enable_shared_from_this - { - public: - enum direction_type { inbound, outbound }; - enum connection_state { - connecting = 0, - syncing = 1, - synced = 2 - }; - - fc::time_point connection_initiation_time; - fc::time_point connection_closed_time; - fc::time_point connection_terminated_time; - direction_type direction = outbound; - connection_state state = connecting; - bool is_firewalled = true - - //connection_state state; - fc::microseconds clock_offset; - fc::microseconds round_trip_delay; - - /// data about the peer node - /// @{ - - /** the unique identifier we'll use to refer to the node with. zero-initialized before - * we receive the hello message, at which time it will be filled with either the "node_id" - * from the user_data field of the hello, or if none is present it will be filled with a - * copy of node_public_key */ - public_key_type node_id; - uint32_t core_protocol_version; - std::string user_agent; - - fc::optional graphene_git_revision_sha; - fc::optional graphene_git_revision_unix_timestamp; - fc::optional fc_git_revision_sha; - fc::optional fc_git_revision_unix_timestamp; - fc::optional platform; - fc::optional bitness; - - // for inbound connections, these fields record what the peer sent us in - // its hello message. For outbound, they record what we sent the peer - // in our hello message - fc::ip::address inbound_address; - uint16_t inbound_port; - uint16_t outbound_port; - /// @} - - void send( transaction_message_ptr msg ) - { - // if not in sent_or_received then insert into _pending_send - // if process_send_queue is invalid or complete then - // async process_send_queue - } - - void received_transaction( const transaction_id_type& id ) - { - _sent_or_received.insert(id); - } - - void process_send_queue() - { - // while _pending_send.size() || _pending_blocks.size() - // while there are pending blocks, then take the oldest - // for each transaction id, verify that it exists in _sent_or_received - // else find it in the _pending_send queue and send it - // send one from _pending_send - } - - - std::unordered_map _pending_send; - /// todo: make multi-index that tracks how long items have been cached and removes them - /// after a resasonable period of time (say 10 seconds) - std::unordered_set _sent_or_received; - std::map _pending_blocks; - - - fc::ip::endpoint get_remote_endpoint()const - { return get_socket().get_remote_endpoint(); } - - void on_message(message_oriented_connection* originating_connection, - const message& received_message) override - { - switch( core_message_type_enum( received_message.type ) ) - { - case hello_message_type: - _node->on_hello( shared_from_this(), - received_message.as() ); - break; - case transaction_message_type: - _node->on_transaction( shared_from_this(), - received_message.as() ); - break; - case block_message_type: - _node->on_block( shared_from_this(), - received_message.as() ); - break; - case peer_message_type: - _node->on_peers( shared_from_this(), - received_message.as() ); - break; - } - } - - void on_connection_closed(message_oriented_connection* originating_connection) override - { - _node->on_close( shared_from_this() ); - } - - fc::tcp_socket& get_socket() { return _message_connection.get_socket(); } - - private: - peer_connection_delegate* _node; - fc::optional _remote_endpoint; - message_oriented_connection _message_connection; - - }; - typedef std::shared_ptr peer_connection_ptr; - - - } } // end namespace graphene::p2p - -// not sent over the wire, just reflected for logging -FC_REFLECT_ENUM(graphene::p2p::peer_connection::connection_state, (connecting)(syncing)(synced) ) -FC_REFLECT_ENUM(graphene::p2p::peer_connection::direction_type, (inbound)(outbound) ) diff --git a/libraries/p2p/include/graphene/p2p/stcp_socket.hpp b/libraries/p2p/include/graphene/p2p/stcp_socket.hpp deleted file mode 100644 index 333d96bb..00000000 --- a/libraries/p2p/include/graphene/p2p/stcp_socket.hpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#pragma once -#include -#include -#include - -namespace graphene { namespace p2p { - -/** - * Uses ECDH to negotiate a aes key for communicating - * with other nodes on the network. - */ -class stcp_socket : public virtual fc::iostream -{ - public: - stcp_socket(); - ~stcp_socket(); - fc::tcp_socket& get_socket() { return _sock; } - void accept(); - - void connect_to( const fc::ip::endpoint& remote_endpoint ); - void bind( const fc::ip::endpoint& local_endpoint ); - - virtual size_t readsome( char* buffer, size_t max ); - virtual size_t readsome( const std::shared_ptr& buf, size_t len, size_t offset ); - virtual bool eof()const; - - virtual size_t writesome( const char* buffer, size_t len ); - virtual size_t writesome( const std::shared_ptr& buf, size_t len, size_t offset ); - - virtual void flush(); - virtual void close(); - - using istream::get; - void get( char& c ) { read( &c, 1 ); } - fc::sha512 get_shared_secret() const { return _shared_secret; } - private: - void do_key_exchange(); - - fc::sha512 _shared_secret; - fc::ecc::private_key _priv_key; - fc::array _buf; - //uint32_t _buf_len; - fc::tcp_socket _sock; - fc::aes_encoder _send_aes; - fc::aes_decoder _recv_aes; - std::shared_ptr _read_buffer; - std::shared_ptr _write_buffer; -#ifndef NDEBUG - bool _read_buffer_in_use; - bool _write_buffer_in_use; -#endif -}; - -typedef std::shared_ptr stcp_socket_ptr; - -} } // graphene::p2p diff --git a/libraries/p2p/message_oriented_connection.cpp b/libraries/p2p/message_oriented_connection.cpp deleted file mode 100644 index a9e23c34..00000000 --- a/libraries/p2p/message_oriented_connection.cpp +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#ifdef DEFAULT_LOGGER -# undef DEFAULT_LOGGER -#endif -#define DEFAULT_LOGGER "p2p" - -#ifndef NDEBUG -# define VERIFY_CORRECT_THREAD() assert(_thread->is_current()) -#else -# define VERIFY_CORRECT_THREAD() do {} while (0) -#endif - -namespace graphene { namespace p2p { - namespace detail - { - class message_oriented_connection_impl - { - private: - message_oriented_connection* _self; - message_oriented_connection_delegate *_delegate; - stcp_socket _sock; - fc::future _read_loop_done; - uint64_t _bytes_received; - uint64_t _bytes_sent; - - fc::time_point _connected_time; - fc::time_point _last_message_received_time; - fc::time_point _last_message_sent_time; - - bool _send_message_in_progress; - -#ifndef NDEBUG - fc::thread* _thread; -#endif - - void read_loop(); - void start_read_loop(); - public: - fc::tcp_socket& get_socket(); - void accept(); - void connect_to(const fc::ip::endpoint& remote_endpoint); - void bind(const fc::ip::endpoint& local_endpoint); - - message_oriented_connection_impl(message_oriented_connection* self, - message_oriented_connection_delegate* delegate = nullptr); - ~message_oriented_connection_impl(); - - void send_message(const message& message_to_send); - void close_connection(); - void destroy_connection(); - - uint64_t get_total_bytes_sent() const; - uint64_t get_total_bytes_received() const; - - fc::time_point get_last_message_sent_time() const; - fc::time_point get_last_message_received_time() const; - fc::time_point get_connection_time() const { return _connected_time; } - fc::sha512 get_shared_secret() const; - }; - - message_oriented_connection_impl::message_oriented_connection_impl(message_oriented_connection* self, - message_oriented_connection_delegate* delegate) - : _self(self), - _delegate(delegate), - _bytes_received(0), - _bytes_sent(0), - _send_message_in_progress(false) -#ifndef NDEBUG - ,_thread(&fc::thread::current()) -#endif - { - } - message_oriented_connection_impl::~message_oriented_connection_impl() - { - VERIFY_CORRECT_THREAD(); - destroy_connection(); - } - - fc::tcp_socket& message_oriented_connection_impl::get_socket() - { - VERIFY_CORRECT_THREAD(); - return _sock.get_socket(); - } - - void message_oriented_connection_impl::accept() - { - VERIFY_CORRECT_THREAD(); - _sock.accept(); - assert(!_read_loop_done.valid()); // check to be sure we never launch two read loops - _read_loop_done = fc::async([=](){ read_loop(); }, "message read_loop"); - } - - void message_oriented_connection_impl::connect_to(const fc::ip::endpoint& remote_endpoint) - { - VERIFY_CORRECT_THREAD(); - _sock.connect_to(remote_endpoint); - assert(!_read_loop_done.valid()); // check to be sure we never launch two read loops - _read_loop_done = fc::async([=](){ read_loop(); }, "message read_loop"); - } - - void message_oriented_connection_impl::bind(const fc::ip::endpoint& local_endpoint) - { - VERIFY_CORRECT_THREAD(); - _sock.bind(local_endpoint); - } - - - void message_oriented_connection_impl::read_loop() - { - VERIFY_CORRECT_THREAD(); - const int BUFFER_SIZE = 16; - const int LEFTOVER = BUFFER_SIZE - sizeof(message_header); - static_assert(BUFFER_SIZE >= sizeof(message_header), "insufficient buffer"); - - _connected_time = fc::time_point::now(); - - fc::oexception exception_to_rethrow; - bool call_on_connection_closed = false; - - try - { - message m; - while( true ) - { - char buffer[BUFFER_SIZE]; - _sock.read(buffer, BUFFER_SIZE); - _bytes_received += BUFFER_SIZE; - memcpy((char*)&m, buffer, sizeof(message_header)); - - FC_ASSERT( m.size <= MAX_MESSAGE_SIZE, "", ("m.size",m.size)("MAX_MESSAGE_SIZE",MAX_MESSAGE_SIZE) ); - - size_t remaining_bytes_with_padding = 16 * ((m.size - LEFTOVER + 15) / 16); - m.data.resize(LEFTOVER + remaining_bytes_with_padding); //give extra 16 bytes to allow for padding added in send call - std::copy(buffer + sizeof(message_header), buffer + sizeof(buffer), m.data.begin()); - if (remaining_bytes_with_padding) - { - _sock.read(&m.data[LEFTOVER], remaining_bytes_with_padding); - _bytes_received += remaining_bytes_with_padding; - } - m.data.resize(m.size); // truncate off the padding bytes - - _last_message_received_time = fc::time_point::now(); - - try - { - // message handling errors are warnings... - _delegate->on_message(_self, m); - } - /// Dedicated catches needed to distinguish from general fc::exception - catch ( const fc::canceled_exception& e ) { throw e; } - catch ( const fc::eof_exception& e ) { throw e; } - catch ( const fc::exception& e) - { - /// Here loop should be continued so exception should be just caught locally. - wlog( "message transmission failed ${er}", ("er", e.to_detail_string() ) ); - throw; - } - } - } - catch ( const fc::canceled_exception& e ) - { - wlog( "caught a canceled_exception in read_loop. this should mean we're in the process of deleting this object already, so there's no need to notify the delegate: ${e}", ("e", e.to_detail_string() ) ); - throw; - } - catch ( const fc::eof_exception& e ) - { - wlog( "disconnected ${e}", ("e", e.to_detail_string() ) ); - call_on_connection_closed = true; - } - catch ( const fc::exception& e ) - { - elog( "disconnected ${er}", ("er", e.to_detail_string() ) ); - call_on_connection_closed = true; - exception_to_rethrow = fc::unhandled_exception(FC_LOG_MESSAGE(warn, "disconnected: ${e}", ("e", e.to_detail_string()))); - } - catch ( const std::exception& e ) - { - elog( "disconnected ${er}", ("er", e.what() ) ); - call_on_connection_closed = true; - exception_to_rethrow = fc::unhandled_exception(FC_LOG_MESSAGE(warn, "disconnected: ${e}", ("e", e.what()))); - } - catch ( ... ) - { - elog( "unexpected exception" ); - call_on_connection_closed = true; - exception_to_rethrow = fc::unhandled_exception(FC_LOG_MESSAGE(warn, "disconnected: ${e}", ("e", fc::except_str()))); - } - - if (call_on_connection_closed) - _delegate->on_connection_closed(_self); - - if (exception_to_rethrow) - throw *exception_to_rethrow; - } - - void message_oriented_connection_impl::send_message(const message& message_to_send) - { - VERIFY_CORRECT_THREAD(); -#if 0 // this gets too verbose -#ifndef NDEBUG - fc::optional remote_endpoint; - if (_sock.get_socket().is_open()) - remote_endpoint = _sock.get_socket().remote_endpoint(); - struct scope_logger { - const fc::optional& endpoint; - scope_logger(const fc::optional& endpoint) : endpoint(endpoint) { dlog("entering message_oriented_connection::send_message() for peer ${endpoint}", ("endpoint", endpoint)); } - ~scope_logger() { dlog("leaving message_oriented_connection::send_message() for peer ${endpoint}", ("endpoint", endpoint)); } - } send_message_scope_logger(remote_endpoint); -#endif -#endif - struct verify_no_send_in_progress { - bool& var; - verify_no_send_in_progress(bool& var) : var(var) - { - if (var) - elog("Error: two tasks are calling message_oriented_connection::send_message() at the same time"); - assert(!var); - var = true; - } - ~verify_no_send_in_progress() { var = false; } - } _verify_no_send_in_progress(_send_message_in_progress); - - try - { - size_t size_of_message_and_header = sizeof(message_header) + message_to_send.size; - if( message_to_send.size > MAX_MESSAGE_SIZE ) - elog("Trying to send a message larger than MAX_MESSAGE_SIZE. This probably won't work..."); - //pad the message we send to a multiple of 16 bytes - size_t size_with_padding = 16 * ((size_of_message_and_header + 15) / 16); - std::unique_ptr padded_message(new char[size_with_padding]); - memcpy(padded_message.get(), (char*)&message_to_send, sizeof(message_header)); - memcpy(padded_message.get() + sizeof(message_header), message_to_send.data.data(), message_to_send.size ); - _sock.write(padded_message.get(), size_with_padding); - _sock.flush(); - _bytes_sent += size_with_padding; - _last_message_sent_time = fc::time_point::now(); - } FC_RETHROW_EXCEPTIONS( warn, "unable to send message" ); - } - - void message_oriented_connection_impl::close_connection() - { - VERIFY_CORRECT_THREAD(); - _sock.close(); - } - - void message_oriented_connection_impl::destroy_connection() - { - VERIFY_CORRECT_THREAD(); - - fc::optional remote_endpoint; - if (_sock.get_socket().is_open()) - remote_endpoint = _sock.get_socket().remote_endpoint(); - ilog( "in destroy_connection() for ${endpoint}", ("endpoint", remote_endpoint) ); - - if (_send_message_in_progress) - elog("Error: message_oriented_connection is being destroyed while a send_message is in progress. " - "The task calling send_message() should have been canceled already"); - assert(!_send_message_in_progress); - - try - { - _read_loop_done.cancel_and_wait(__FUNCTION__); - } - catch ( const fc::exception& e ) - { - wlog( "Exception thrown while canceling message_oriented_connection's read_loop, ignoring: ${e}", ("e",e) ); - } - catch (...) - { - wlog( "Exception thrown while canceling message_oriented_connection's read_loop, ignoring" ); - } - } - - uint64_t message_oriented_connection_impl::get_total_bytes_sent() const - { - VERIFY_CORRECT_THREAD(); - return _bytes_sent; - } - - uint64_t message_oriented_connection_impl::get_total_bytes_received() const - { - VERIFY_CORRECT_THREAD(); - return _bytes_received; - } - - fc::time_point message_oriented_connection_impl::get_last_message_sent_time() const - { - VERIFY_CORRECT_THREAD(); - return _last_message_sent_time; - } - - fc::time_point message_oriented_connection_impl::get_last_message_received_time() const - { - VERIFY_CORRECT_THREAD(); - return _last_message_received_time; - } - - fc::sha512 message_oriented_connection_impl::get_shared_secret() const - { - VERIFY_CORRECT_THREAD(); - return _sock.get_shared_secret(); - } - - } // end namespace graphene::p2p::detail - - - message_oriented_connection::message_oriented_connection(message_oriented_connection_delegate* delegate) : - my(new detail::message_oriented_connection_impl(this, delegate)) - { - } - - message_oriented_connection::~message_oriented_connection() - { - } - - fc::tcp_socket& message_oriented_connection::get_socket() - { - return my->get_socket(); - } - - void message_oriented_connection::accept() - { - my->accept(); - } - - void message_oriented_connection::connect_to(const fc::ip::endpoint& remote_endpoint) - { - my->connect_to(remote_endpoint); - } - - void message_oriented_connection::bind(const fc::ip::endpoint& local_endpoint) - { - my->bind(local_endpoint); - } - - void message_oriented_connection::send_message(const message& message_to_send) - { - my->send_message(message_to_send); - } - - void message_oriented_connection::close_connection() - { - my->close_connection(); - } - - void message_oriented_connection::destroy_connection() - { - my->destroy_connection(); - } - - uint64_t message_oriented_connection::get_total_bytes_sent() const - { - return my->get_total_bytes_sent(); - } - - uint64_t message_oriented_connection::get_total_bytes_received() const - { - return my->get_total_bytes_received(); - } - - fc::time_point message_oriented_connection::get_last_message_sent_time() const - { - return my->get_last_message_sent_time(); - } - - fc::time_point message_oriented_connection::get_last_message_received_time() const - { - return my->get_last_message_received_time(); - } - fc::time_point message_oriented_connection::get_connection_time() const - { - return my->get_connection_time(); - } - fc::sha512 message_oriented_connection::get_shared_secret() const - { - return my->get_shared_secret(); - } - -} } // end namespace graphene::p2p diff --git a/libraries/p2p/node.cpp b/libraries/p2p/node.cpp deleted file mode 100644 index 3de41047..00000000 --- a/libraries/p2p/node.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#include - -namespace graphene { namespace p2p { - - node::node( chain_database& db ) - :_db(db) - { - - } - - node::~node() - { - - } - - void node::add_peer( const fc::ip::endpoint& ep ) - { - - } - - void node::configure( const node_config& cfg ) - { - listen_on_endpoint( cfg.server_endpoint, wait_if_not_available ); - - /** don't allow node to go out of scope until accept loop exits */ - auto self = shared_from_this(); - _accept_loop_complete = fc::async( [self](){ self->accept_loop(); } ) - } - - void node::accept_loop() - { - auto self = shared_from_this(); - while( !_accept_loop_complete.canceled() ) - { - try { - auto new_peer = std::make_shared(self); - _tcp_server.accept( new_peer.get_socket() ); - - if( _accept_loop_complete.canceled() ) - return; - - _peers.insert( new_peer ); - - - - // limit the rate at which we accept connections to mitigate DOS attacks - fc::usleep( fc::milliseconds(10) ); - } FC_CAPTURE_AND_RETHROW() - } - } // accept_loop() - - - - void node::listen_on_endpoint( fc::ip::endpoint ep, bool wait_if_not_available ) - { - if( ep.port() != 0 ) - { - // if the user specified a port, we only want to bind to it if it's not already - // being used by another application. During normal operation, we set the - // SO_REUSEADDR/SO_REUSEPORT flags so that we can bind outbound sockets to the - // same local endpoint as we're listening on here. On some platforms, setting - // those flags will prevent us from detecting that other applications are - // listening on that port. We'd like to detect that, so we'll set up a temporary - // tcp server without that flag to see if we can listen on that port. - bool first = true; - for( ;; ) - { - bool listen_failed = false; - - try - { - fc::tcp_server temporary_server; - if( listen_endpoint.get_address() != fc::ip::address() ) - temporary_server.listen( ep ); - else - temporary_server.listen( ep.port() ); - break; - } - catch ( const fc::exception&) - { - listen_failed = true; - } - - if (listen_failed) - { - if( wait_if_endpoint_is_busy ) - { - std::ostringstream error_message_stream; - if( first ) - { - error_message_stream << "Unable to listen for connections on port " - << ep.port() - << ", retrying in a few seconds\n"; - error_message_stream << "You can wait for it to become available, or restart " - "this program using\n"; - error_message_stream << "the --p2p-port option to specify another port\n"; - first = false; - } - else - { - error_message_stream << "\nStill waiting for port " << listen_endpoint.port() << " to become available\n"; - } - - std::string error_message = error_message_stream.str(); - ulog(error_message); - fc::usleep( fc::seconds(5 ) ); - } - else // don't wait, just find a random port - { - wlog( "unable to bind on the requested endpoint ${endpoint}, " - "which probably means that endpoint is already in use", - ( "endpoint", ep ) ); - ep.set_port( 0 ); - } - } // if (listen_failed) - } // for(;;) - } // if (listen_endpoint.port() != 0) - - - _tcp_server.set_reuse_address(); - try - { - if( ep.get_address() != fc::ip::address() ) - _tcp_server.listen( ep ); - else - _tcp_server.listen( ep.port() ); - - _actual_listening_endpoint = _tcp_server.get_local_endpoint(); - ilog( "listening for connections on endpoint ${endpoint} (our first choice)", - ( "endpoint", _actual_listening_endpoint ) ); - } - catch ( fc::exception& e ) - { - FC_RETHROW_EXCEPTION( e, error, - "unable to listen on ${endpoint}", ("endpoint",listen_endpoint ) ); - } - } - - - -} } diff --git a/libraries/p2p/stcp_socket.cpp b/libraries/p2p/stcp_socket.cpp deleted file mode 100644 index 0a54bc2e..00000000 --- a/libraries/p2p/stcp_socket.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2015 Cryptonomex, Inc., and contributors. - * - * The MIT License - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#include - -#include - -#include -#include -#include -#include -#include -#include - -#include - -namespace graphene { namespace p2p { - -stcp_socket::stcp_socket() -//:_buf_len(0) -#ifndef NDEBUG - : _read_buffer_in_use(false), - _write_buffer_in_use(false) -#endif -{ -} -stcp_socket::~stcp_socket() -{ -} - -void stcp_socket::do_key_exchange() -{ - _priv_key = fc::ecc::private_key::generate(); - fc::ecc::public_key pub = _priv_key.get_public_key(); - fc::ecc::public_key_data s = pub.serialize(); - std::shared_ptr serialized_key_buffer(new char[sizeof(fc::ecc::public_key_data)], [](char* p){ delete[] p; }); - memcpy(serialized_key_buffer.get(), (char*)&s, sizeof(fc::ecc::public_key_data)); - _sock.write( serialized_key_buffer, sizeof(fc::ecc::public_key_data) ); - _sock.read( serialized_key_buffer, sizeof(fc::ecc::public_key_data) ); - fc::ecc::public_key_data rpub; - memcpy((char*)&rpub, serialized_key_buffer.get(), sizeof(fc::ecc::public_key_data)); - - _shared_secret = _priv_key.get_shared_secret( rpub ); -// ilog("shared secret ${s}", ("s", shared_secret) ); - _send_aes.init( fc::sha256::hash( (char*)&_shared_secret, sizeof(_shared_secret) ), - fc::city_hash_crc_128((char*)&_shared_secret,sizeof(_shared_secret) ) ); - _recv_aes.init( fc::sha256::hash( (char*)&_shared_secret, sizeof(_shared_secret) ), - fc::city_hash_crc_128((char*)&_shared_secret,sizeof(_shared_secret) ) ); -} - - -void stcp_socket::connect_to( const fc::ip::endpoint& remote_endpoint ) -{ - _sock.connect_to( remote_endpoint ); - do_key_exchange(); -} - -void stcp_socket::bind( const fc::ip::endpoint& local_endpoint ) -{ - _sock.bind(local_endpoint); -} - -/** - * This method must read at least 16 bytes at a time from - * the underlying TCP socket so that it can decrypt them. It - * will buffer any left-over. - */ -size_t stcp_socket::readsome( char* buffer, size_t len ) -{ try { - assert( len > 0 && (len % 16) == 0 ); - -#ifndef NDEBUG - // This code was written with the assumption that you'd only be making one call to readsome - // at a time so it reuses _read_buffer. If you really need to make concurrent calls to - // readsome(), you'll need to prevent reusing _read_buffer here - struct check_buffer_in_use { - bool& _buffer_in_use; - check_buffer_in_use(bool& buffer_in_use) : _buffer_in_use(buffer_in_use) { assert(!_buffer_in_use); _buffer_in_use = true; } - ~check_buffer_in_use() { assert(_buffer_in_use); _buffer_in_use = false; } - } buffer_in_use_checker(_read_buffer_in_use); -#endif - - const size_t read_buffer_length = 4096; - if (!_read_buffer) - _read_buffer.reset(new char[read_buffer_length], [](char* p){ delete[] p; }); - - len = std::min(read_buffer_length, len); - - size_t s = _sock.readsome( _read_buffer, len, 0 ); - if( s % 16 ) - { - _sock.read(_read_buffer, 16 - (s%16), s); - s += 16-(s%16); - } - _recv_aes.decode( _read_buffer.get(), s, buffer ); - return s; -} FC_RETHROW_EXCEPTIONS( warn, "", ("len",len) ) } - -size_t stcp_socket::readsome( const std::shared_ptr& buf, size_t len, size_t offset ) -{ - return readsome(buf.get() + offset, len); -} - -bool stcp_socket::eof()const -{ - return _sock.eof(); -} - -size_t stcp_socket::writesome( const char* buffer, size_t len ) -{ try { - assert( len > 0 && (len % 16) == 0 ); - -#ifndef NDEBUG - // This code was written with the assumption that you'd only be making one call to writesome - // at a time so it reuses _write_buffer. If you really need to make concurrent calls to - // writesome(), you'll need to prevent reusing _write_buffer here - struct check_buffer_in_use { - bool& _buffer_in_use; - check_buffer_in_use(bool& buffer_in_use) : _buffer_in_use(buffer_in_use) { assert(!_buffer_in_use); _buffer_in_use = true; } - ~check_buffer_in_use() { assert(_buffer_in_use); _buffer_in_use = false; } - } buffer_in_use_checker(_write_buffer_in_use); -#endif - - const std::size_t write_buffer_length = 4096; - if (!_write_buffer) - _write_buffer.reset(new char[write_buffer_length], [](char* p){ delete[] p; }); - len = std::min(write_buffer_length, len); - memset(_write_buffer.get(), 0, len); // just in case aes.encode screws up - /** - * every sizeof(crypt_buf) bytes the aes channel - * has an error and doesn't decrypt properly... disable - * for now because we are going to upgrade to something - * better. - */ - uint32_t ciphertext_len = _send_aes.encode( buffer, len, _write_buffer.get() ); - assert(ciphertext_len == len); - _sock.write( _write_buffer, ciphertext_len ); - return ciphertext_len; -} FC_RETHROW_EXCEPTIONS( warn, "", ("len",len) ) } - -size_t stcp_socket::writesome( const std::shared_ptr& buf, size_t len, size_t offset ) -{ - return writesome(buf.get() + offset, len); -} - -void stcp_socket::flush() -{ - _sock.flush(); -} - - -void stcp_socket::close() -{ - try - { - _sock.close(); - }FC_RETHROW_EXCEPTIONS( warn, "error closing stcp socket" ); -} - -void stcp_socket::accept() -{ - do_key_exchange(); -} - - -}} // namespace graphene::p2p - diff --git a/libraries/plugins/CMakeLists.txt b/libraries/plugins/CMakeLists.txt index b18415f8..3cf7fd97 100644 --- a/libraries/plugins/CMakeLists.txt +++ b/libraries/plugins/CMakeLists.txt @@ -1,7 +1,9 @@ add_subdirectory( witness ) add_subdirectory( account_history ) +add_subdirectory( accounts_list ) add_subdirectory( market_history ) add_subdirectory( delayed_node ) +add_subdirectory( bookie ) add_subdirectory( generate_genesis ) add_subdirectory( generate_uia_sharedrop_genesis ) add_subdirectory( debug_witness ) diff --git a/libraries/plugins/account_history/CMakeLists.txt b/libraries/plugins/account_history/CMakeLists.txt index 18fd6135..4af81abb 100644 --- a/libraries/plugins/account_history/CMakeLists.txt +++ b/libraries/plugins/account_history/CMakeLists.txt @@ -19,3 +19,5 @@ install( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +INSTALL( FILES ${HEADERS} DESTINATION "include/graphene/account_history" ) + diff --git a/libraries/plugins/account_history/account_history_plugin.cpp b/libraries/plugins/account_history/account_history_plugin.cpp index bdd1fb23..5cd00235 100644 --- a/libraries/plugins/account_history/account_history_plugin.cpp +++ b/libraries/plugins/account_history/account_history_plugin.cpp @@ -64,42 +64,57 @@ class account_history_plugin_impl account_history_plugin& _self; flat_set _tracked_accounts; + bool _partial_operations = false; + primary_index< simple_index< operation_history_object > >* _oho_index; + uint32_t _max_ops_per_account = -1; + private: + /** add one history record, then check and remove the earliest history record */ + void add_account_history( const account_id_type account_id, const operation_history_id_type op_id ); + }; account_history_plugin_impl::~account_history_plugin_impl() { - return; } void account_history_plugin_impl::update_account_histories( const signed_block& b ) { graphene::chain::database& db = database(); - const vector >& hist = db.get_applied_operations(); - for( const optional< operation_history_object >& o_op : hist ) + vector >& hist = db.get_applied_operations(); + for( optional< operation_history_object >& o_op : hist ) { - // add to the operation history index - const auto& oho = db.create( [&]( operation_history_object& h ) - { - if( o_op.valid() ) - h = *o_op; - } ); + optional oho; - if( !o_op.valid() ) + auto create_oho = [&]() { + operation_history_object result = db.create( [&]( operation_history_object& h ) + { + if( o_op.valid() ) + h = *o_op; + } ); + o_op->id = result.id; + return optional(result); + }; + + if( !o_op.valid() || ( _max_ops_per_account == 0 && _partial_operations ) ) { - ilog( "removing failed operation with ID: ${id}", ("id", oho.id) ); - db.remove( oho ); + // Note: the 2nd and 3rd checks above are for better performance, when the db is not clean, + // they will break consistency of account_stats.total_ops and removed_ops and most_recent_op + _oho_index->use_next_id(); continue; } + else if( !_partial_operations ) + // add to the operation history index + oho = create_oho(); const operation_history_object& op = *o_op; // get the set of accounts this operation applies to flat_set impacted; vector other; - operation_get_required_authorities( op.op, impacted, impacted, other ); + operation_get_required_authorities( op.op, impacted, impacted, other ); // fee_payer is added here if( op.op.which() == operation::tag< account_create_operation >::value ) - impacted.insert( oho.result.get() ); + impacted.insert( op.result.get() ); else graphene::app::operation_get_impacted_accounts( op.op, impacted ); @@ -107,48 +122,119 @@ void account_history_plugin_impl::update_account_histories( const signed_block& for( auto& item : a.account_auths ) impacted.insert( item.first ); - // for each operation this account applies to that is in the config link it into the history - if( _tracked_accounts.size() == 0 ) - { - for( auto& account_id : impacted ) - { - // we don't do index_account_keys here anymore, because - // that indexing now happens in observers' post_evaluate() + // be here, either _max_ops_per_account > 0, or _partial_operations == false, or both + // if _partial_operations == false, oho should have been created above + // so the only case should be checked here is: + // whether need to create oho if _max_ops_per_account > 0 and _partial_operations == true - // add history - const auto& stats_obj = account_id(db).statistics(db); - const auto& ath = db.create( [&]( account_transaction_history_object& obj ){ - obj.operation_id = oho.id; - obj.account = account_id; - obj.sequence = stats_obj.total_ops+1; - obj.next = stats_obj.most_recent_op; - }); - db.modify( stats_obj, [&]( account_statistics_object& obj ){ - obj.most_recent_op = ath.id; - obj.total_ops = ath.sequence; - }); + // for each operation this account applies to that is in the config link it into the history + if( _tracked_accounts.size() == 0 ) // tracking all accounts + { + // if tracking all accounts, when impacted is not empty (although it will always be), + // still need to create oho if _max_ops_per_account > 0 and _partial_operations == true + // so always need to create oho if not done + if (!impacted.empty() && !oho.valid()) { oho = create_oho(); } + + if( _max_ops_per_account > 0 ) + { + // Note: the check above is for better performance, when the db is not clean, + // it breaks consistency of account_stats.total_ops and removed_ops and most_recent_op, + // but it ensures it's safe to remove old entries in add_account_history(...) + for( auto& account_id : impacted ) + { + // we don't do index_account_keys here anymore, because + // that indexing now happens in observers' post_evaluate() + + // add history + add_account_history( account_id, oho->id ); + } } } - else + else // tracking a subset of accounts { - for( auto account_id : _tracked_accounts ) + // whether need to create oho if _max_ops_per_account > 0 and _partial_operations == true ? + // the answer: only need to create oho if a tracked account is impacted and need to save history + + if( _max_ops_per_account > 0 ) { - if( impacted.find( account_id ) != impacted.end() ) + // Note: the check above is for better performance, when the db is not clean, + // it breaks consistency of account_stats.total_ops and removed_ops and most_recent_op, + // but it ensures it's safe to remove old entries in add_account_history(...) + for( auto account_id : _tracked_accounts ) { - // add history - const auto& stats_obj = account_id(db).statistics(db); - const auto& ath = db.create( [&]( account_transaction_history_object& obj ){ - obj.operation_id = oho.id; - obj.next = stats_obj.most_recent_op; - }); - db.modify( stats_obj, [&]( account_statistics_object& obj ){ - obj.most_recent_op = ath.id; - }); + if( impacted.find( account_id ) != impacted.end() ) + { + if (!oho.valid()) { oho = create_oho(); } + // add history + add_account_history( account_id, oho->id ); + } + } + } + } + if (_partial_operations && ! oho.valid()) + _oho_index->use_next_id(); + } +} + +void account_history_plugin_impl::add_account_history( const account_id_type account_id, const operation_history_id_type op_id ) +{ + graphene::chain::database& db = database(); + const auto& stats_obj = account_id(db).statistics(db); + // add new entry + const auto& ath = db.create( [&]( account_transaction_history_object& obj ){ + obj.operation_id = op_id; + obj.account = account_id; + obj.sequence = stats_obj.total_ops + 1; + obj.next = stats_obj.most_recent_op; + }); + db.modify( stats_obj, [&]( account_statistics_object& obj ){ + obj.most_recent_op = ath.id; + obj.total_ops = ath.sequence; + }); + // remove the earliest account history entry if too many + // _max_ops_per_account is guaranteed to be non-zero outside + if( stats_obj.total_ops - stats_obj.removed_ops > _max_ops_per_account ) + { + // look for the earliest entry + const auto& his_idx = db.get_index_type(); + const auto& by_seq_idx = his_idx.indices().get(); + auto itr = by_seq_idx.lower_bound( boost::make_tuple( account_id, 0 ) ); + // make sure don't remove the one just added + if( itr != by_seq_idx.end() && itr->account == account_id && itr->id != ath.id ) + { + // if found, remove the entry, and adjust account stats object + const auto remove_op_id = itr->operation_id; + const auto itr_remove = itr; + ++itr; + db.remove( *itr_remove ); + db.modify( stats_obj, [&]( account_statistics_object& obj ){ + obj.removed_ops = obj.removed_ops + 1; + }); + // modify previous node's next pointer + // this should be always true, but just have a check here + if( itr != by_seq_idx.end() && itr->account == account_id ) + { + db.modify( *itr, [&]( account_transaction_history_object& obj ){ + obj.next = account_transaction_history_id_type(); + }); + } + // else need to modify the head pointer, but it shouldn't be true + + // remove the operation history entry (1.11.x) if configured and no reference left + if( _partial_operations ) + { + // check for references + const auto& by_opid_idx = his_idx.indices().get(); + if( by_opid_idx.find( remove_op_id ) == by_opid_idx.end() ) + { + // if no reference, remove + db.remove( remove_op_id(db) ); } } } } } + } // end namespace detail @@ -177,6 +263,8 @@ void account_history_plugin::plugin_set_program_options( { cli.add_options() ("track-account", boost::program_options::value>()->composing()->multitoken(), "Account ID to track history for (may specify multiple times)") + ("partial-operations", boost::program_options::value(), "Keep only those operations in memory that are related to account history tracking") + ("max-ops-per-account", boost::program_options::value(), "Maximum number of operations per account will be kept in memory") ; cfg.add(cli); } @@ -184,10 +272,16 @@ void account_history_plugin::plugin_set_program_options( void account_history_plugin::plugin_initialize(const boost::program_options::variables_map& options) { database().applied_block.connect( [&]( const signed_block& b){ my->update_account_histories(b); } ); - database().add_index< primary_index< simple_index< operation_history_object > > >(); + my->_oho_index = database().add_index< primary_index< simple_index< operation_history_object > > >(); database().add_index< primary_index< account_transaction_history_index > >(); - LOAD_VALUE_SET(options, "tracked-accounts", my->_tracked_accounts, graphene::chain::account_id_type); + LOAD_VALUE_SET(options, "track-account", my->_tracked_accounts, graphene::chain::account_id_type); + if (options.count("partial-operations")) { + my->_partial_operations = options["partial-operations"].as(); + } + if (options.count("max-ops-per-account")) { + my->_max_ops_per_account = options["max-ops-per-account"].as(); + } } void account_history_plugin::plugin_startup() diff --git a/libraries/plugins/accounts_list/CMakeLists.txt b/libraries/plugins/accounts_list/CMakeLists.txt new file mode 100644 index 00000000..3c2747c2 --- /dev/null +++ b/libraries/plugins/accounts_list/CMakeLists.txt @@ -0,0 +1,21 @@ +file(GLOB HEADERS "include/graphene/accouns_list/*.hpp") + +add_library( graphene_accounts_list + accounts_list_plugin.cpp + ) + +target_link_libraries( graphene_accounts_list graphene_chain graphene_app ) +target_include_directories( graphene_accounts_list + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) + +if(MSVC) + set_source_files_properties( accounts_list_plugin.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) +endif(MSVC) + +install( TARGETS + graphene_accounts_list + + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) diff --git a/libraries/plugins/accounts_list/accounts_list_plugin.cpp b/libraries/plugins/accounts_list/accounts_list_plugin.cpp new file mode 100644 index 00000000..aabf711d --- /dev/null +++ b/libraries/plugins/accounts_list/accounts_list_plugin.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace graphene { namespace accounts_list { + +namespace detail +{ + + +class accounts_list_plugin_impl +{ + public: + accounts_list_plugin_impl(accounts_list_plugin& _plugin) + : _self( _plugin ) + { } + virtual ~accounts_list_plugin_impl(); + + + /** + */ + void list_accounts(); + + graphene::chain::database& database() + { + return _self.database(); + } + + accounts_list_plugin& _self; + vector _listed_balances; +}; + +accounts_list_plugin_impl::~accounts_list_plugin_impl() +{ + return; +} + +void accounts_list_plugin_impl::list_accounts() +{ + graphene::chain::database& db = database(); + _listed_balances.clear(); + + auto& balance_index = db.get_index_type().indices().get(); + for (auto balance_iter = balance_index.begin(); + balance_iter != balance_index.end() && + balance_iter->asset_type == graphene::chain::asset_id_type() && + balance_iter->balance > 0; ++balance_iter) + { + //idump((balance_iter->owner(db) .name)(*balance_iter)); + _listed_balances.emplace_back(*balance_iter); + } + +} +} // end namespace detail + +accounts_list_plugin::accounts_list_plugin() : + my( new detail::accounts_list_plugin_impl(*this) ) +{ +} + +accounts_list_plugin::~accounts_list_plugin() +{ +} + +std::string accounts_list_plugin::plugin_name()const +{ + return "accounts_list"; +} + +void accounts_list_plugin::plugin_set_program_options( + boost::program_options::options_description& /*cli*/, + boost::program_options::options_description& /*cfg*/ + ) +{ +// cli.add_options() +// ("list-account", boost::program_options::value>()->composing()->multitoken(), "Account ID to list (may specify multiple times)") +// ; +// cfg.add(cli); +} + +void accounts_list_plugin::plugin_initialize(const boost::program_options::variables_map& /*options*/) +{ + //ilog("accounts list plugin: plugin_initialize()"); + list_accounts(); +} + +void accounts_list_plugin::plugin_startup() +{ + //ilog("accounts list plugin: plugin_startup()"); +} + + vector accounts_list_plugin::list_accounts() const +{ + ilog("accounts list plugin: list_accounts()"); + my->list_accounts(); + //idump((my->_listed_balances)); + return my->_listed_balances; +} + +} } diff --git a/libraries/plugins/accounts_list/include/graphene/accounts_list/accounts_list_plugin.hpp b/libraries/plugins/accounts_list/include/graphene/accounts_list/accounts_list_plugin.hpp new file mode 100644 index 00000000..d57e19eb --- /dev/null +++ b/libraries/plugins/accounts_list/include/graphene/accounts_list/accounts_list_plugin.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include + +#include + +namespace graphene { namespace accounts_list { +using namespace chain; + +namespace detail +{ + class accounts_list_plugin_impl; +} + +class accounts_list_plugin : public graphene::app::plugin +{ + public: + accounts_list_plugin(); + virtual ~accounts_list_plugin(); + + std::string plugin_name()const override; + virtual void plugin_set_program_options( + boost::program_options::options_description& cli, + boost::program_options::options_description& cfg) override; + virtual void plugin_initialize(const boost::program_options::variables_map& options) override; + virtual void plugin_startup() override; + + vectorlist_accounts()const; + + friend class detail::accounts_list_plugin_impl; + std::unique_ptr my; +}; + +} } //graphene::accounts_list + diff --git a/libraries/plugins/bookie/CMakeLists.txt b/libraries/plugins/bookie/CMakeLists.txt new file mode 100644 index 00000000..12d84e3c --- /dev/null +++ b/libraries/plugins/bookie/CMakeLists.txt @@ -0,0 +1,22 @@ +file(GLOB HEADERS "include/graphene/bookie/*.hpp") + +add_library( graphene_bookie + bookie_plugin.cpp + bookie_api.cpp + ) + +target_link_libraries( graphene_bookie graphene_chain graphene_app ) +target_include_directories( graphene_bookie + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) + +if(MSVC) + set_source_files_properties( bookie_plugin.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) +endif(MSVC) + +install( TARGETS + graphene_bookie + + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) diff --git a/libraries/plugins/bookie/bookie_api.cpp b/libraries/plugins/bookie/bookie_api.cpp new file mode 100644 index 00000000..798fe474 --- /dev/null +++ b/libraries/plugins/bookie/bookie_api.cpp @@ -0,0 +1,294 @@ +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +namespace graphene { namespace bookie { + +namespace detail { + +class bookie_api_impl +{ + public: + bookie_api_impl(graphene::app::application& _app); + + binned_order_book get_binned_order_book(graphene::chain::betting_market_id_type betting_market_id, int32_t precision); + std::shared_ptr get_plugin(); + asset get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id); + std::vector get_events_containing_sub_string(const std::string& sub_string, const std::string& language); + fc::variants get_objects(const vector& ids) const; + std::vector get_matched_bets_for_bettor(account_id_type bettor_id) const; + std::vector get_all_matched_bets_for_bettor(account_id_type bettor_id, bet_id_type start, unsigned limit) const; + graphene::app::application& app; +}; + +bookie_api_impl::bookie_api_impl(graphene::app::application& _app) : app(_app) +{} + + +binned_order_book bookie_api_impl::get_binned_order_book(graphene::chain::betting_market_id_type betting_market_id, int32_t precision) +{ + std::shared_ptr db = app.chain_database(); + const auto& bet_odds_idx = db->get_index_type().indices().get(); + const chain_parameters& current_params = db->get_global_properties().parameters; + + graphene::chain::bet_multiplier_type bin_size = GRAPHENE_BETTING_ODDS_PRECISION; + if (precision > 0) + for (int32_t i = 0; i < precision; ++i) { + FC_ASSERT(bin_size > (GRAPHENE_BETTING_MIN_MULTIPLIER - GRAPHENE_BETTING_ODDS_PRECISION), "invalid precision"); + bin_size /= 10; + } + else if (precision < 0) + for (int32_t i = 0; i > precision; --i) { + FC_ASSERT(bin_size < (GRAPHENE_BETTING_MAX_MULTIPLIER - GRAPHENE_BETTING_ODDS_PRECISION), "invalid precision"); + bin_size *= 10; + } + + binned_order_book result; + + // use a bet_object here for convenience. we really only use it to track the amount, odds, and back_or_lay + fc::optional current_bin; + + auto flush_current_bin = [¤t_bin, &result]() + { + if (current_bin) // do nothing if the current bin is empty + { + order_bin current_order_bin; + + current_order_bin.backer_multiplier = current_bin->backer_multiplier; + current_order_bin.amount_to_bet = current_bin->amount_to_bet.amount; + //idump((*current_bin)(current_order_bin)); + if (current_bin->back_or_lay == bet_type::back) + result.aggregated_back_bets.emplace_back(std::move(current_order_bin)); + else // current_bin is aggregating back positions + result.aggregated_lay_bets.emplace_back(std::move(current_order_bin)); + + current_bin.reset(); + } + }; + + // iterate through both sides of the order book (backs at increasing odds then lays at decreasing odds) + for (auto bet_odds_iter = bet_odds_idx.lower_bound(std::make_tuple(betting_market_id)); + bet_odds_iter != bet_odds_idx.end() && betting_market_id == bet_odds_iter->betting_market_id; + ++bet_odds_iter) + { + if (current_bin && + (bet_odds_iter->back_or_lay != current_bin->back_or_lay /* we have switched from back to lay bets */ || + (bet_odds_iter->back_or_lay == bet_type::back ? bet_odds_iter->backer_multiplier > current_bin->backer_multiplier : + bet_odds_iter->backer_multiplier < current_bin->backer_multiplier))) + flush_current_bin(); + + if (!current_bin) + { + // if there is no current bin, create one appropriate for the bet we're processing + current_bin = graphene::chain::bet_object(); + + // for back bets, we want to group all bets with odds from 3.0001 to 4 into the "4" bin + // for lay bets, we want to group all bets with odds from 3 to 3.9999 into the "3" bin + if (bet_odds_iter->back_or_lay == bet_type::back) + { + current_bin->backer_multiplier = (bet_odds_iter->backer_multiplier + bin_size - 1) / bin_size * bin_size; + current_bin->backer_multiplier = std::min(current_bin->backer_multiplier, current_params.max_bet_multiplier); + current_bin->back_or_lay = bet_type::back; + } + else + { + current_bin->backer_multiplier = bet_odds_iter->backer_multiplier / bin_size * bin_size; + current_bin->backer_multiplier = std::max(current_bin->backer_multiplier, current_params.min_bet_multiplier); + current_bin->back_or_lay = bet_type::lay; + } + + current_bin->amount_to_bet.amount = 0; + } + + current_bin->amount_to_bet.amount += bet_odds_iter->amount_to_bet.amount; + } + if (current_bin) + flush_current_bin(); + + return result; +} + +fc::variants bookie_api_impl::get_objects(const vector& ids) const +{ + std::shared_ptr db = app.chain_database(); + fc::variants result; + result.reserve(ids.size()); + + std::transform(ids.begin(), ids.end(), std::back_inserter(result), + [this, &db](object_id_type id) -> fc::variant { + switch (id.type()) + { + case event_id_type::type_id: + { + auto& persistent_events_by_event_id = db->get_index_type().indices().get(); + auto iter = persistent_events_by_event_id.find(id.as()); + if (iter != persistent_events_by_event_id.end()) + return iter->ephemeral_event_object.to_variant(); + else + return {}; + } + case bet_id_type::type_id: + { + auto& persistent_bets_by_bet_id = db->get_index_type().indices().get(); + auto iter = persistent_bets_by_bet_id.find(id.as()); + if (iter != persistent_bets_by_bet_id.end()) + return iter->ephemeral_bet_object.to_variant(); + else + return {}; + } + case betting_market_object::type_id: + { + auto& persistent_betting_markets_by_betting_market_id = db->get_index_type().indices().get(); + auto iter = persistent_betting_markets_by_betting_market_id.find(id.as()); + if (iter != persistent_betting_markets_by_betting_market_id.end()) + return iter->ephemeral_betting_market_object.to_variant(); + else + return {}; + } + case betting_market_group_object::type_id: + { + auto& persistent_betting_market_groups_by_betting_market_group_id = db->get_index_type().indices().get(); + auto iter = persistent_betting_market_groups_by_betting_market_group_id.find(id.as()); + if (iter != persistent_betting_market_groups_by_betting_market_group_id.end()) + return iter->ephemeral_betting_market_group_object.to_variant(); + else + return {}; + } + default: + return {}; + } + }); + + return result; +} + +std::vector bookie_api_impl::get_matched_bets_for_bettor(account_id_type bettor_id) const +{ + std::vector result; + std::shared_ptr db = app.chain_database(); + auto& persistent_bets_by_bettor_id = db->get_index_type().indices().get(); + auto iter = persistent_bets_by_bettor_id.lower_bound(std::make_tuple(bettor_id, true)); + while (iter != persistent_bets_by_bettor_id.end() && + iter->get_bettor_id() == bettor_id && + iter->is_matched()) + { + matched_bet_object match; + match.id = iter->ephemeral_bet_object.id; + match.bettor_id = iter->ephemeral_bet_object.bettor_id; + match.betting_market_id = iter->ephemeral_bet_object.betting_market_id; + match.amount_to_bet = iter->ephemeral_bet_object.amount_to_bet; + match.back_or_lay = iter->ephemeral_bet_object.back_or_lay; + match.end_of_delay = iter->ephemeral_bet_object.end_of_delay; + match.amount_matched = iter->amount_matched; + match.associated_operations = iter->associated_operations; + result.emplace_back(std::move(match)); + + ++iter; + } + return result; +} + +std::vector bookie_api_impl::get_all_matched_bets_for_bettor(account_id_type bettor_id, bet_id_type start, unsigned limit) const +{ + FC_ASSERT(limit <= 1000, "You may request at most 1000 matched bets at a time"); + + std::vector result; + std::shared_ptr db = app.chain_database(); + auto& persistent_bets_by_bettor_id = db->get_index_type().indices().get(); + persistent_bet_multi_index_type::index::type::iterator iter; + if (start == bet_id_type()) + iter = persistent_bets_by_bettor_id.lower_bound(std::make_tuple(bettor_id, true)); + else + iter = persistent_bets_by_bettor_id.lower_bound(std::make_tuple(bettor_id, true, start)); + while (iter != persistent_bets_by_bettor_id.end() && + iter->get_bettor_id() == bettor_id && + iter->is_matched() && + result.size() < limit) + { + matched_bet_object match; + match.id = iter->ephemeral_bet_object.id; + match.bettor_id = iter->ephemeral_bet_object.bettor_id; + match.betting_market_id = iter->ephemeral_bet_object.betting_market_id; + match.amount_to_bet = iter->ephemeral_bet_object.amount_to_bet; + match.back_or_lay = iter->ephemeral_bet_object.back_or_lay; + match.end_of_delay = iter->ephemeral_bet_object.end_of_delay; + match.amount_matched = iter->amount_matched; + result.emplace_back(std::move(match)); + + ++iter; + } + return result; +} + +std::shared_ptr bookie_api_impl::get_plugin() +{ + return app.get_plugin("bookie"); +} + +asset bookie_api_impl::get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id) +{ + return get_plugin()->get_total_matched_bet_amount_for_betting_market_group(group_id); +} + +std::vector bookie_api_impl::get_events_containing_sub_string(const std::string& sub_string, const std::string& language) +{ + return get_plugin()->get_events_containing_sub_string(sub_string, language); +} + +} // detail + +bookie_api::bookie_api(graphene::app::application& app) : + my(std::make_shared(app)) +{ +} + +binned_order_book bookie_api::get_binned_order_book(graphene::chain::betting_market_id_type betting_market_id, int32_t precision) +{ + return my->get_binned_order_book(betting_market_id, precision); +} + +asset bookie_api::get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id) +{ + return my->get_total_matched_bet_amount_for_betting_market_group(group_id); +} + +std::vector bookie_api::get_events_containing_sub_string(const std::string& sub_string, const std::string& language) +{ + return my->get_events_containing_sub_string(sub_string, language); +} + +fc::variants bookie_api::get_objects(const vector& ids) const +{ + return my->get_objects(ids); +} + +std::vector bookie_api::get_matched_bets_for_bettor(account_id_type bettor_id) const +{ + return my->get_matched_bets_for_bettor(bettor_id); +} + +std::vector bookie_api::get_all_matched_bets_for_bettor(account_id_type bettor_id, + bet_id_type start /* = bet_id_type() */, + unsigned limit /* = 1000 */) const +{ + return my->get_all_matched_bets_for_bettor(bettor_id, start, limit); +} + +} } // graphene::bookie + + diff --git a/libraries/plugins/bookie/bookie_plugin.cpp b/libraries/plugins/bookie/bookie_plugin.cpp new file mode 100644 index 00000000..564df3a1 --- /dev/null +++ b/libraries/plugins/bookie/bookie_plugin.cpp @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +#if 0 +# ifdef DEFAULT_LOGGER +# undef DEFAULT_LOGGER +# endif +# define DEFAULT_LOGGER "bookie_plugin" +#endif + +namespace graphene { namespace bookie { + +namespace detail +{ +/* As a plugin, we get notified of new/changed objects at the end of every block processed. + * For most objects, that's fine, because we expect them to always be around until the end of + * the block. However, with bet objects, it's possible that the user places a bet and it fills + * and is removed during the same block, so need another strategy to detect them immediately after + * they are created. + * We do this by creating a secondary index on bet_object. We don't actually use it + * to index any property of the bet, we just use it to register for callbacks. + */ +class persistent_bet_object_helper : public secondary_index +{ + public: + virtual ~persistent_bet_object_helper() {} + + virtual void object_inserted(const object& obj) override; + //virtual void object_removed( const object& obj ) override; + //virtual void about_to_modify( const object& before ) override; + virtual void object_modified(const object& after) override; + void set_plugin_instance(bookie_plugin* instance) { _bookie_plugin = instance; } + private: + bookie_plugin* _bookie_plugin; +}; + +void persistent_bet_object_helper::object_inserted(const object& obj) +{ + const bet_object& bet_obj = *boost::polymorphic_downcast(&obj); + _bookie_plugin->database().create([&](persistent_bet_object& saved_bet_obj) { + saved_bet_obj.ephemeral_bet_object = bet_obj; + }); +} +void persistent_bet_object_helper::object_modified(const object& after) +{ + database& db = _bookie_plugin->database(); + auto& persistent_bets_by_bet_id = db.get_index_type().indices().get(); + const bet_object& bet_obj = *boost::polymorphic_downcast(&after); + auto iter = persistent_bets_by_bet_id.find(bet_obj.id); + assert (iter != persistent_bets_by_bet_id.end()); + if (iter != persistent_bets_by_bet_id.end()) + db.modify(*iter, [&](persistent_bet_object& saved_bet_obj) { + saved_bet_obj.ephemeral_bet_object = bet_obj; + }); +} + +//////////// end bet_object /////////////////// +class persistent_betting_market_object_helper : public secondary_index +{ + public: + virtual ~persistent_betting_market_object_helper() {} + + virtual void object_inserted(const object& obj) override; + //virtual void object_removed( const object& obj ) override; + //virtual void about_to_modify( const object& before ) override; + virtual void object_modified(const object& after) override; + void set_plugin_instance(bookie_plugin* instance) { _bookie_plugin = instance; } + private: + bookie_plugin* _bookie_plugin; +}; + +void persistent_betting_market_object_helper::object_inserted(const object& obj) +{ + const betting_market_object& betting_market_obj = *boost::polymorphic_downcast(&obj); + _bookie_plugin->database().create([&](persistent_betting_market_object& saved_betting_market_obj) { + saved_betting_market_obj.ephemeral_betting_market_object = betting_market_obj; + }); +} +void persistent_betting_market_object_helper::object_modified(const object& after) +{ + database& db = _bookie_plugin->database(); + auto& persistent_betting_markets_by_betting_market_id = db.get_index_type().indices().get(); + const betting_market_object& betting_market_obj = *boost::polymorphic_downcast(&after); + auto iter = persistent_betting_markets_by_betting_market_id.find(betting_market_obj.id); + assert (iter != persistent_betting_markets_by_betting_market_id.end()); + if (iter != persistent_betting_markets_by_betting_market_id.end()) + db.modify(*iter, [&](persistent_betting_market_object& saved_betting_market_obj) { + saved_betting_market_obj.ephemeral_betting_market_object = betting_market_obj; + }); +} + +//////////// end betting_market_object /////////////////// +class persistent_betting_market_group_object_helper : public secondary_index +{ + public: + virtual ~persistent_betting_market_group_object_helper() {} + + virtual void object_inserted(const object& obj) override; + //virtual void object_removed( const object& obj ) override; + //virtual void about_to_modify( const object& before ) override; + virtual void object_modified(const object& after) override; + void set_plugin_instance(bookie_plugin* instance) { _bookie_plugin = instance; } + private: + bookie_plugin* _bookie_plugin; +}; + +void persistent_betting_market_group_object_helper::object_inserted(const object& obj) +{ + const betting_market_group_object& betting_market_group_obj = *boost::polymorphic_downcast(&obj); + _bookie_plugin->database().create([&](persistent_betting_market_group_object& saved_betting_market_group_obj) { + saved_betting_market_group_obj.ephemeral_betting_market_group_object = betting_market_group_obj; + }); +} +void persistent_betting_market_group_object_helper::object_modified(const object& after) +{ + database& db = _bookie_plugin->database(); + auto& persistent_betting_market_groups_by_betting_market_group_id = db.get_index_type().indices().get(); + const betting_market_group_object& betting_market_group_obj = *boost::polymorphic_downcast(&after); + auto iter = persistent_betting_market_groups_by_betting_market_group_id.find(betting_market_group_obj.id); + assert (iter != persistent_betting_market_groups_by_betting_market_group_id.end()); + if (iter != persistent_betting_market_groups_by_betting_market_group_id.end()) + db.modify(*iter, [&](persistent_betting_market_group_object& saved_betting_market_group_obj) { + saved_betting_market_group_obj.ephemeral_betting_market_group_object = betting_market_group_obj; + }); +} + +//////////// end betting_market_group_object /////////////////// +class persistent_event_object_helper : public secondary_index +{ + public: + virtual ~persistent_event_object_helper() {} + + virtual void object_inserted(const object& obj) override; + //virtual void object_removed( const object& obj ) override; + //virtual void about_to_modify( const object& before ) override; + virtual void object_modified(const object& after) override; + void set_plugin_instance(bookie_plugin* instance) { _bookie_plugin = instance; } + private: + bookie_plugin* _bookie_plugin; +}; + +void persistent_event_object_helper::object_inserted(const object& obj) +{ + const event_object& event_obj = *boost::polymorphic_downcast(&obj); + _bookie_plugin->database().create([&](persistent_event_object& saved_event_obj) { + saved_event_obj.ephemeral_event_object = event_obj; + }); +} +void persistent_event_object_helper::object_modified(const object& after) +{ + database& db = _bookie_plugin->database(); + auto& persistent_events_by_event_id = db.get_index_type().indices().get(); + const event_object& event_obj = *boost::polymorphic_downcast(&after); + auto iter = persistent_events_by_event_id.find(event_obj.id); + assert (iter != persistent_events_by_event_id.end()); + if (iter != persistent_events_by_event_id.end()) + db.modify(*iter, [&](persistent_event_object& saved_event_obj) { + saved_event_obj.ephemeral_event_object = event_obj; + }); +} + +//////////// end event_object /////////////////// +class bookie_plugin_impl +{ + public: + bookie_plugin_impl(bookie_plugin& _plugin) + : _self( _plugin ) + { } + virtual ~bookie_plugin_impl(); + + + /** + * Called After a block has been applied and committed. The callback + * should not yield and should execute quickly. + */ + void on_objects_changed(const vector& changed_object_ids); + + void on_objects_new(const vector& new_object_ids); + void on_objects_removed(const vector& removed_object_ids); + + /** this method is called as a callback after a block is applied + * and will process/index all operations that were applied in the block. + */ + void on_block_applied( const signed_block& b ); + + asset get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id); + + void fill_localized_event_strings(); + + std::vector get_events_containing_sub_string(const std::string& sub_string, const std::string& language); + + graphene::chain::database& database() + { + return _self.database(); + } + + // 1.18. "Washington Capitals/Chicago Blackhawks" + typedef std::pair event_string; + struct event_string_less : public std::less + { + bool operator()(const event_string &_left, const event_string &_right) const + { + return (_left.first.instance < _right.first.instance); + } + }; + + typedef flat_set event_string_set; + // "en" + std::map localized_event_strings; + + bookie_plugin& _self; + flat_set _tracked_accounts; +}; + +bookie_plugin_impl::~bookie_plugin_impl() +{ +} + +void bookie_plugin_impl::on_objects_new(const vector& new_object_ids) +{ +} + +void bookie_plugin_impl::on_objects_removed(const vector& removed_object_ids) +{ +} + +void bookie_plugin_impl::on_objects_changed(const vector& changed_object_ids) +{ +} + +bool is_operation_history_object_stored(operation_history_id_type id) +{ + if (id == operation_history_id_type()) + { + elog("Warning: the operation history object for an operation the bookie plugin needs to track " + "has id of ${id}, which means the account history plugin isn't storing this operation, or that " + "it is running after the bookie plugin. Make sure the account history plugin is tracking operations for " + "all accounts,, and that it is loaded before the bookie plugin", ("id", id)); + return false; + } + else + return true; +} + +void bookie_plugin_impl::on_block_applied( const signed_block& ) +{ try { + + graphene::chain::database& db = database(); + const vector >& hist = db.get_applied_operations(); + for( const optional& o_op : hist ) + { + if( !o_op.valid() ) + continue; + + const operation_history_object& op = *o_op; + if( op.op.which() == operation::tag::value ) + { + const bet_matched_operation& bet_matched_op = op.op.get(); + //idump((bet_matched_op)); + const asset& amount_bet = bet_matched_op.amount_bet; + // object may no longer exist + //const bet_object& bet = bet_matched_op.bet_id(db); + auto& persistent_bets_by_bet_id = db.get_index_type().indices().get(); + auto bet_iter = persistent_bets_by_bet_id.find(bet_matched_op.bet_id); + assert(bet_iter != persistent_bets_by_bet_id.end()); + if (bet_iter != persistent_bets_by_bet_id.end()) + { + db.modify(*bet_iter, [&]( persistent_bet_object& obj ) { + obj.amount_matched += amount_bet.amount; + if (is_operation_history_object_stored(op.id)) + obj.associated_operations.emplace_back(op.id); + }); + const bet_object& bet_obj = bet_iter->ephemeral_bet_object; + + auto& persistent_betting_market_idx = db.get_index_type().indices().get(); + auto persistent_betting_market_object_iter = persistent_betting_market_idx.find(bet_obj.betting_market_id); + FC_ASSERT(persistent_betting_market_object_iter != persistent_betting_market_idx.end()); + const betting_market_object& betting_market = persistent_betting_market_object_iter->ephemeral_betting_market_object; + + auto& persistent_betting_market_group_idx = db.get_index_type().indices().get(); + auto persistent_betting_market_group_object_iter = persistent_betting_market_group_idx.find(betting_market.group_id); + FC_ASSERT(persistent_betting_market_group_object_iter != persistent_betting_market_group_idx.end()); + const betting_market_group_object& betting_market_group = persistent_betting_market_group_object_iter->ephemeral_betting_market_group_object; + + // if the object is still in the main database, keep the running total there + // otherwise, add it directly to the persistent version + auto& betting_market_group_idx = db.get_index_type().indices().get(); + auto betting_market_group_iter = betting_market_group_idx.find(betting_market_group.id); + if (betting_market_group_iter != betting_market_group_idx.end()) + db.modify( *betting_market_group_iter, [&]( betting_market_group_object& obj ){ + obj.total_matched_bets_amount += amount_bet.amount; + }); + else + db.modify( *persistent_betting_market_group_object_iter, [&]( persistent_betting_market_group_object& obj ){ + obj.ephemeral_betting_market_group_object.total_matched_bets_amount += amount_bet.amount; + }); + } + } + else if( op.op.which() == operation::tag::value ) + { + FC_ASSERT(op.result.which() == operation_result::tag::value); + //object_id_type object_id = op.result.get(); + event_id_type object_id = op.result.get(); + FC_ASSERT( db.find_object(object_id), "invalid event specified" ); + const event_create_operation& event_create_op = op.op.get(); + for(const std::pair& pair : event_create_op.name) + localized_event_strings[pair.first].insert(event_string(object_id, pair.second)); + } + else if( op.op.which() == operation::tag::value ) + { + const event_update_operation& event_create_op = op.op.get(); + if (!event_create_op.new_name.valid()) + continue; + event_id_type event_id = event_create_op.event_id; + for(const std::pair& pair : *event_create_op.new_name) + { + // try insert + std::pair result = + localized_event_strings[pair.first].insert(event_string(event_id, pair.second)); + if (!result.second) + // update string only + result.first->second = pair.second; + } + } + else if ( op.op.which() == operation::tag::value ) + { + const bet_canceled_operation& bet_canceled_op = op.op.get(); + auto& persistent_bets_by_bet_id = db.get_index_type().indices().get(); + auto bet_iter = persistent_bets_by_bet_id.find(bet_canceled_op.bet_id); + assert(bet_iter != persistent_bets_by_bet_id.end()); + if (bet_iter != persistent_bets_by_bet_id.end()) + { + ilog("Adding bet_canceled_operation ${canceled_id} to bet ${bet_id}'s associated operations", + ("canceled_id", op.id)("bet_id", bet_canceled_op.bet_id)); + if (is_operation_history_object_stored(op.id)) + db.modify(*bet_iter, [&]( persistent_bet_object& obj ) { + obj.associated_operations.emplace_back(op.id); + }); + } + } + else if ( op.op.which() == operation::tag::value ) + { + const bet_adjusted_operation& bet_adjusted_op = op.op.get(); + auto& persistent_bets_by_bet_id = db.get_index_type().indices().get(); + auto bet_iter = persistent_bets_by_bet_id.find(bet_adjusted_op.bet_id); + assert(bet_iter != persistent_bets_by_bet_id.end()); + if (bet_iter != persistent_bets_by_bet_id.end()) + { + ilog("Adding bet_adjusted_operation ${adjusted_id} to bet ${bet_id}'s associated operations", + ("adjusted_id", op.id)("bet_id", bet_adjusted_op.bet_id)); + if (is_operation_history_object_stored(op.id)) + db.modify(*bet_iter, [&]( persistent_bet_object& obj ) { + obj.associated_operations.emplace_back(op.id); + }); + } + } + + } +} FC_RETHROW_EXCEPTIONS( warn, "" ) } + +void bookie_plugin_impl::fill_localized_event_strings() +{ + graphene::chain::database& db = database(); + const auto& event_index = db.get_index_type().indices().get(); + auto event_itr = event_index.cbegin(); + while (event_itr != event_index.cend()) + { + const event_object& event_obj = *event_itr; + ++event_itr; + for(const std::pair& pair : event_obj.name) + { + localized_event_strings[pair.first].insert(event_string(event_obj.id, pair.second)); + } + } +} + +std::vector bookie_plugin_impl::get_events_containing_sub_string(const std::string& sub_string, const std::string& language) +{ + graphene::chain::database& db = database(); + std::vector events; + if (localized_event_strings.find(language) != localized_event_strings.end()) + { + std::string lower_case_sub_string = boost::algorithm::to_lower_copy(sub_string); + const event_string_set& language_set = localized_event_strings[language]; + for (const event_string& pair : language_set) + { + std::string lower_case_string = boost::algorithm::to_lower_copy(pair.second); + if (lower_case_string.find(lower_case_sub_string) != std::string::npos) + events.push_back(pair.first(db)); + } + } + return events; +} + +asset bookie_plugin_impl::get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id) +{ + graphene::chain::database& db = database(); + FC_ASSERT( db.find_object(group_id), "Invalid betting market group specified" ); + const betting_market_group_object& betting_market_group = group_id(db); + return asset(betting_market_group.total_matched_bets_amount, betting_market_group.asset_id); +} +} // end namespace detail + +bookie_plugin::bookie_plugin() : + my( new detail::bookie_plugin_impl(*this) ) +{ +} + +bookie_plugin::~bookie_plugin() +{ +} + +std::string bookie_plugin::plugin_name()const +{ + return "bookie"; +} + +void bookie_plugin::plugin_set_program_options(boost::program_options::options_description& cli, + boost::program_options::options_description& cfg) +{ + //cli.add_options() + // ("track-account", boost::program_options::value>()->composing()->multitoken(), "Account ID to track history for (may specify multiple times)") + // ; + //cfg.add(cli); +} + +void bookie_plugin::plugin_initialize(const boost::program_options::variables_map& options) +{ + ilog("bookie plugin: plugin_startup() begin"); + database().force_slow_replays(); + database().applied_block.connect( [&]( const signed_block& b){ my->on_block_applied(b); } ); + database().changed_objects.connect([&](const vector& changed_object_ids, const fc::flat_set& impacted_accounts){ my->on_objects_changed(changed_object_ids); }); + database().new_objects.connect([this](const vector& ids, const flat_set& impacted_accounts) { my->on_objects_new(ids); }); + database().removed_objects.connect([this](const vector& ids, const vector& objs, const flat_set& impacted_accounts) { my->on_objects_removed(ids); }); + + + //auto event_index = + database().add_index >(); + database().add_index >(); + database().add_index >(); + database().add_index >(); + const primary_index& bet_object_idx = database().get_index_type >(); + primary_index& nonconst_bet_object_idx = const_cast&>(bet_object_idx); + detail::persistent_bet_object_helper* persistent_bet_object_helper_index = nonconst_bet_object_idx.add_secondary_index(); + persistent_bet_object_helper_index->set_plugin_instance(this); + + const primary_index& betting_market_object_idx = database().get_index_type >(); + primary_index& nonconst_betting_market_object_idx = const_cast&>(betting_market_object_idx); + detail::persistent_betting_market_object_helper* persistent_betting_market_object_helper_index = nonconst_betting_market_object_idx.add_secondary_index(); + persistent_betting_market_object_helper_index->set_plugin_instance(this); + + const primary_index& betting_market_group_object_idx = database().get_index_type >(); + primary_index& nonconst_betting_market_group_object_idx = const_cast&>(betting_market_group_object_idx); + detail::persistent_betting_market_group_object_helper* persistent_betting_market_group_object_helper_index = nonconst_betting_market_group_object_idx.add_secondary_index(); + persistent_betting_market_group_object_helper_index->set_plugin_instance(this); + + const primary_index& event_object_idx = database().get_index_type >(); + primary_index& nonconst_event_object_idx = const_cast&>(event_object_idx); + detail::persistent_event_object_helper* persistent_event_object_helper_index = nonconst_event_object_idx.add_secondary_index(); + persistent_event_object_helper_index->set_plugin_instance(this); + + ilog("bookie plugin: plugin_startup() end"); + } + +void bookie_plugin::plugin_startup() +{ + ilog("bookie plugin: plugin_startup()"); + my->fill_localized_event_strings(); +} + +flat_set bookie_plugin::tracked_accounts() const +{ + return my->_tracked_accounts; +} + +asset bookie_plugin::get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id) +{ + ilog("bookie plugin: get_total_matched_bet_amount_for_betting_market_group($group_id)", ("group_d", group_id)); + return my->get_total_matched_bet_amount_for_betting_market_group(group_id); +} +std::vector bookie_plugin::get_events_containing_sub_string(const std::string& sub_string, const std::string& language) +{ + ilog("bookie plugin: get_events_containing_sub_string(${sub_string}, ${language})", (sub_string)(language)); + return my->get_events_containing_sub_string(sub_string, language); +} + +} } + diff --git a/libraries/plugins/bookie/include/graphene/bookie/bookie_api.hpp b/libraries/plugins/bookie/include/graphene/bookie/bookie_api.hpp new file mode 100644 index 00000000..34a554a5 --- /dev/null +++ b/libraries/plugins/bookie/include/graphene/bookie/bookie_api.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include + +using namespace graphene::chain; + +namespace graphene { namespace app { + class application; +} } + +namespace graphene { namespace bookie { + +namespace detail { + class bookie_api_impl; +} + +struct order_bin { + graphene::chain::share_type amount_to_bet; + graphene::chain::bet_multiplier_type backer_multiplier; +}; + +struct binned_order_book { + std::vector aggregated_back_bets; + std::vector aggregated_lay_bets; +}; + +struct matched_bet_object { + // all fields from bet_object + bet_id_type id; + + account_id_type bettor_id; + + betting_market_id_type betting_market_id; + + asset amount_to_bet; // this is the original amount, not the amount remaining + + bet_multiplier_type backer_multiplier; + + bet_type back_or_lay; + + fc::optional end_of_delay; + + // plus fields from this plugin + share_type amount_matched; + + std::vector associated_operations; +}; + +class bookie_api +{ + public: + bookie_api(graphene::app::application& app); + + /** + * Returns the current order book, binned according to the given precision. + * precision = 1 means bin using one decimal place. for backs, (1 - 1.1], (1.1 - 1.2], etc. + * precision = 2 would bin on (1 - 1.01], (1.01 - 1.02] + */ + binned_order_book get_binned_order_book(graphene::chain::betting_market_id_type betting_market_id, int32_t precision); + asset get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id); + std::vector get_events_containing_sub_string(const std::string& sub_string, const std::string& language); + fc::variants get_objects(const vector& ids)const; + std::vector get_matched_bets_for_bettor(account_id_type bettor_id) const; + std::vector get_all_matched_bets_for_bettor(account_id_type bettor_id, bet_id_type start = bet_id_type(), unsigned limit = 1000) const; + std::shared_ptr my; +}; + +} } + +FC_REFLECT(graphene::bookie::order_bin, (amount_to_bet)(backer_multiplier)) +FC_REFLECT(graphene::bookie::binned_order_book, (aggregated_back_bets)(aggregated_lay_bets)) +FC_REFLECT(graphene::bookie::matched_bet_object, (id)(bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(back_or_lay)(end_of_delay)(amount_matched)(associated_operations)) + +FC_API(graphene::bookie::bookie_api, + (get_binned_order_book) + (get_total_matched_bet_amount_for_betting_market_group) + (get_events_containing_sub_string) + (get_objects) + (get_matched_bets_for_bettor) + (get_all_matched_bets_for_bettor)) + diff --git a/libraries/plugins/bookie/include/graphene/bookie/bookie_objects.hpp b/libraries/plugins/bookie/include/graphene/bookie/bookie_objects.hpp new file mode 100644 index 00000000..3604aac5 --- /dev/null +++ b/libraries/plugins/bookie/include/graphene/bookie/bookie_objects.hpp @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include +#include +#include + +namespace graphene { namespace bookie { +using namespace chain; + +enum bookie_object_type +{ + persistent_event_object_type, + persistent_betting_market_group_object_type, + persistent_betting_market_object_type, + persistent_bet_object_type, + BOOKIE_OBJECT_TYPE_COUNT ///< Sentry value which contains the number of different object types +}; + +namespace detail +{ + +class persistent_event_object : public graphene::db::abstract_object +{ + public: + static const uint8_t space_id = bookie_objects; + static const uint8_t type_id = persistent_event_object_type; + + event_object ephemeral_event_object; + + event_id_type get_event_id() const { return ephemeral_event_object.id; } +}; + +typedef object_id persistent_event_id_type; + +struct by_event_id; +typedef multi_index_container< + persistent_event_object, + indexed_by< + ordered_unique, member >, + ordered_unique, const_mem_fun > > > persistent_event_multi_index_type; +typedef generic_index persistent_event_index; + +#if 0 // we no longer have competitors, just leaving this here as an example of how to do a secondary index +class events_by_competitor_index : public secondary_index +{ + public: + virtual ~events_by_competitor_index() {} + + virtual void object_inserted( const object& obj ) override; + virtual void object_removed( const object& obj ) override; + virtual void about_to_modify( const object& before ) override; + virtual void object_modified( const object& after ) override; + protected: + + map > competitor_to_events; +}; + +void events_by_competitor_index::object_inserted( const object& obj ) +{ + const persistent_event_object& event_obj = *boost::polymorphic_downcast(&obj); + for (const competitor_id_type& competitor_id : event_obj.competitors) + competitor_to_events[competitor_id].insert(event_obj.id); + for (const competitor_id_type& competitor_id : event_obj.competitors) + competitor_to_events[competitor_id].insert(event_obj.id); +} +void events_by_competitor_index::object_removed( const object& obj ) +{ + const persistent_event_object& event_obj = *boost::polymorphic_downcast(&obj); + for (const competitor_id_type& competitor_id : event_obj.competitors) + competitor_to_events[competitor_id].erase(event_obj.id); +} +void events_by_competitor_index::about_to_modify( const object& before ) +{ + object_removed(before); +} +void events_by_competitor_index::object_modified( const object& after ) +{ + object_inserted(after); +} +#endif + +//////////// betting_market_group_object ////////////////// +class persistent_betting_market_group_object : public graphene::db::abstract_object +{ + public: + static const uint8_t space_id = bookie_objects; + static const uint8_t type_id = persistent_betting_market_group_object_type; + + betting_market_group_object ephemeral_betting_market_group_object; + + share_type total_matched_bets_amount; + + betting_market_group_id_type get_betting_market_group_id() const { return ephemeral_betting_market_group_object.id; } +}; + +struct by_betting_market_group_id; +typedef multi_index_container< + persistent_betting_market_group_object, + indexed_by< + ordered_unique, member >, + ordered_unique, const_mem_fun > > > persistent_betting_market_group_multi_index_type; + +typedef generic_index persistent_betting_market_group_index; + +//////////// betting_market_object ////////////////// +class persistent_betting_market_object : public graphene::db::abstract_object +{ + public: + static const uint8_t space_id = bookie_objects; + static const uint8_t type_id = persistent_betting_market_object_type; + + betting_market_object ephemeral_betting_market_object; + + share_type total_matched_bets_amount; + + betting_market_id_type get_betting_market_id() const { return ephemeral_betting_market_object.id; } +}; + +struct by_betting_market_id; +typedef multi_index_container< + persistent_betting_market_object, + indexed_by< + ordered_unique, member >, + ordered_unique, const_mem_fun > > > persistent_betting_market_multi_index_type; + +typedef generic_index persistent_betting_market_index; + +//////////// bet_object ////////////////// +class persistent_bet_object : public graphene::db::abstract_object +{ + public: + static const uint8_t space_id = bookie_objects; + static const uint8_t type_id = persistent_bet_object_type; + + bet_object ephemeral_bet_object; + + // total amount of the bet that matched + share_type amount_matched; + + std::vector associated_operations; + + bet_id_type get_bet_id() const { return ephemeral_bet_object.id; } + account_id_type get_bettor_id() const { return ephemeral_bet_object.bettor_id; } + bool is_matched() const { return amount_matched != share_type(); } +}; + +struct by_bet_id; +struct by_bettor_id; +typedef multi_index_container< + persistent_bet_object, + indexed_by< + ordered_unique, member >, + ordered_unique, const_mem_fun >, + ordered_unique, + composite_key< + persistent_bet_object, + const_mem_fun, + const_mem_fun, + const_mem_fun >, + composite_key_compare< + std::less, + std::less, + std::greater > > > > persistent_bet_multi_index_type; + +typedef generic_index persistent_bet_index; + +} } } //graphene::bookie::detail + +FC_REFLECT_DERIVED( graphene::bookie::detail::persistent_event_object, (graphene::db::object), (ephemeral_event_object) ) +FC_REFLECT_DERIVED( graphene::bookie::detail::persistent_betting_market_group_object, (graphene::db::object), (ephemeral_betting_market_group_object)(total_matched_bets_amount) ) +FC_REFLECT_DERIVED( graphene::bookie::detail::persistent_betting_market_object, (graphene::db::object), (ephemeral_betting_market_object) ) +FC_REFLECT_DERIVED( graphene::bookie::detail::persistent_bet_object, (graphene::db::object), (ephemeral_bet_object)(amount_matched)(associated_operations) ) + diff --git a/libraries/plugins/bookie/include/graphene/bookie/bookie_plugin.hpp b/libraries/plugins/bookie/include/graphene/bookie/bookie_plugin.hpp new file mode 100644 index 00000000..7ea15450 --- /dev/null +++ b/libraries/plugins/bookie/include/graphene/bookie/bookie_plugin.hpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include + +#include + +namespace graphene { namespace bookie { +using namespace chain; + +// +// Plugins should #define their SPACE_ID's so plugins with +// conflicting SPACE_ID assignments can be compiled into the +// same binary (by simply re-assigning some of the conflicting #defined +// SPACE_ID's in a build script). +// +// Assignment of SPACE_ID's cannot be done at run-time because +// various template automagic depends on them being known at compile +// time. +// +enum spaces { + bookie_objects = 6 +}; + +namespace detail +{ + class bookie_plugin_impl; +} + +class bookie_plugin : public graphene::app::plugin +{ + public: + bookie_plugin(); + virtual ~bookie_plugin(); + + std::string plugin_name()const override; + virtual void plugin_set_program_options(boost::program_options::options_description& cli, + boost::program_options::options_description& cfg) override; + virtual void plugin_initialize(const boost::program_options::variables_map& options) override; + virtual void plugin_startup() override; + + flat_set tracked_accounts()const; + asset get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id); + std::vector get_events_containing_sub_string(const std::string& sub_string, const std::string& language); + + friend class detail::bookie_plugin_impl; + std::unique_ptr my; +}; + +} } //graphene::bookie + diff --git a/libraries/plugins/debug_witness/debug_api.cpp b/libraries/plugins/debug_witness/debug_api.cpp index 5c2d9a37..6236482b 100644 --- a/libraries/plugins/debug_witness/debug_api.cpp +++ b/libraries/plugins/debug_witness/debug_api.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include diff --git a/libraries/plugins/debug_witness/debug_witness.cpp b/libraries/plugins/debug_witness/debug_witness.cpp index 7bb5562d..17f852c5 100644 --- a/libraries/plugins/debug_witness/debug_witness.cpp +++ b/libraries/plugins/debug_witness/debug_witness.cpp @@ -25,7 +25,6 @@ #include #include -#include #include @@ -99,13 +98,13 @@ void debug_witness_plugin::plugin_startup() // connect needed signals _applied_block_conn = db.applied_block.connect([this](const graphene::chain::signed_block& b){ on_applied_block(b); }); - _changed_objects_conn = db.changed_objects.connect([this](const std::vector& ids){ on_changed_objects(ids); }); - _removed_objects_conn = db.removed_objects.connect([this](const std::vector& objs){ on_removed_objects(objs); }); + _changed_objects_conn = db.changed_objects.connect([this](const std::vector& ids, const fc::flat_set& impacted_accounts){ on_changed_objects(ids, impacted_accounts); }); + _removed_objects_conn = db.removed_objects.connect([this](const std::vector& ids, const std::vector& objs, const fc::flat_set& impacted_accounts){ on_removed_objects(ids, objs, impacted_accounts); }); return; } -void debug_witness_plugin::on_changed_objects( const std::vector& ids ) +void debug_witness_plugin::on_changed_objects( const std::vector& ids, const fc::flat_set& impacted_accounts ) { if( _json_object_stream && (ids.size() > 0) ) { @@ -113,11 +112,7 @@ void debug_witness_plugin::on_changed_objects( const std::vectorto_variant() ) << '\n'; } @@ -125,9 +120,8 @@ void debug_witness_plugin::on_changed_objects( const std::vector objs ) +void debug_witness_plugin::on_removed_objects( const std::vector& ids, const std::vector objs, const fc::flat_set& impacted_accounts ) { - /* if( _json_object_stream ) { for( const graphene::db::object* obj : objs ) @@ -135,7 +129,6 @@ void debug_witness_plugin::on_removed_objects( const std::vectorid ) << "}\n"; } } - */ } void debug_witness_plugin::on_applied_block( const graphene::chain::signed_block& b ) diff --git a/libraries/plugins/debug_witness/include/graphene/debug_witness/debug_witness.hpp b/libraries/plugins/debug_witness/include/graphene/debug_witness/debug_witness.hpp index 0e5c173f..907d26ae 100644 --- a/libraries/plugins/debug_witness/include/graphene/debug_witness/debug_witness.hpp +++ b/libraries/plugins/debug_witness/include/graphene/debug_witness/debug_witness.hpp @@ -25,8 +25,10 @@ #include #include +#include #include +#include namespace graphene { namespace debug_witness_plugin { @@ -50,8 +52,8 @@ public: private: - void on_changed_objects( const std::vector& ids ); - void on_removed_objects( const std::vector objs ); + void on_changed_objects( const std::vector& ids, const fc::flat_set& impacted_accounts ); + void on_removed_objects( const std::vector& ids, const std::vector objs, const fc::flat_set& impacted_accounts ); void on_applied_block( const graphene::chain::signed_block& b ); boost::program_options::variables_map _options; diff --git a/libraries/plugins/market_history/CMakeLists.txt b/libraries/plugins/market_history/CMakeLists.txt index 21b8211f..47410d74 100644 --- a/libraries/plugins/market_history/CMakeLists.txt +++ b/libraries/plugins/market_history/CMakeLists.txt @@ -19,3 +19,5 @@ install( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +INSTALL( FILES ${HEADERS} DESTINATION "include/graphene/market_history" ) + diff --git a/libraries/plugins/market_history/market_history_plugin.cpp b/libraries/plugins/market_history/market_history_plugin.cpp index 28cbb7c7..6ec38968 100644 --- a/libraries/plugins/market_history/market_history_plugin.cpp +++ b/libraries/plugins/market_history/market_history_plugin.cpp @@ -110,7 +110,7 @@ struct operation_process_fill_order hkey.sequence += 200; itr = history_idx.lower_bound( hkey ); - + /* while( itr != history_idx.end() ) { if( itr->key.base == hkey.base && itr->key.quote == hkey.quote ) @@ -120,6 +120,7 @@ struct operation_process_fill_order } else break; } + */ auto max_history = _plugin.max_history(); diff --git a/libraries/plugins/witness/CMakeLists.txt b/libraries/plugins/witness/CMakeLists.txt index c82442ff..95759bbf 100644 --- a/libraries/plugins/witness/CMakeLists.txt +++ b/libraries/plugins/witness/CMakeLists.txt @@ -4,7 +4,7 @@ add_library( graphene_witness witness.cpp ) -target_link_libraries( graphene_witness graphene_chain graphene_app graphene_time ) +target_link_libraries( graphene_witness graphene_chain graphene_app ) target_include_directories( graphene_witness PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) diff --git a/libraries/plugins/witness/witness.cpp b/libraries/plugins/witness/witness.cpp index dceaf586..f358e5ea 100644 --- a/libraries/plugins/witness/witness.cpp +++ b/libraries/plugins/witness/witness.cpp @@ -25,7 +25,6 @@ #include #include -#include #include @@ -53,7 +52,7 @@ void new_chain_banner( const graphene::chain::database& db ) "* *\n" "********************************\n" "\n"; - if( db.get_slot_at_time( graphene::time::now() ) > 200 ) + if( db.get_slot_at_time( fc::time_point::now() ) > 200 ) { std::cerr << "Your genesis seems to have an old timestamp\n" "Please consider using the --genesis-timestamp option to give your genesis a recent timestamp\n" @@ -103,7 +102,8 @@ void witness_plugin::plugin_initialize(const boost::program_options::variables_m for (const std::string& key_id_to_wif_pair_string : key_id_to_wif_pair_strings) { auto key_id_to_wif_pair = graphene::app::dejsonify >(key_id_to_wif_pair_string); - idump((key_id_to_wif_pair)); + //idump((key_id_to_wif_pair)); + ilog("Public Key: ${public}", ("public", key_id_to_wif_pair.first)); fc::optional private_key = graphene::utilities::wif_to_key(key_id_to_wif_pair.second); if (!private_key) { @@ -128,8 +128,6 @@ void witness_plugin::plugin_startup() { try { ilog("witness plugin: plugin_startup() begin"); chain::database& d = database(); - //Start NTP time client - graphene::time::now(); if( !_witnesses.empty() ) { @@ -149,7 +147,6 @@ void witness_plugin::plugin_startup() void witness_plugin::plugin_shutdown() { - graphene::time::shutdown_ntp_time(); return; } @@ -157,13 +154,12 @@ void witness_plugin::schedule_production_loop() { //Schedule for the next second's tick regardless of chain state // If we would wait less than 50ms, wait for the whole second. - fc::time_point ntp_now = graphene::time::now(); - fc::time_point fc_now = fc::time_point::now(); - int64_t time_to_next_second = 1000000 - (ntp_now.time_since_epoch().count() % 1000000); + fc::time_point now = fc::time_point::now(); + int64_t time_to_next_second = 1000000 - (now.time_since_epoch().count() % 1000000); if( time_to_next_second < 50000 ) // we must sleep for at least 50ms time_to_next_second += 1000000; - fc::time_point next_wakeup( fc_now + fc::microseconds( time_to_next_second ) ); + fc::time_point next_wakeup( now + fc::microseconds( time_to_next_second ) ); //wdump( (now.time_since_epoch().count())(next_wakeup.time_since_epoch().count()) ); _block_production_task = fc::schedule([this]{block_production_loop();}, @@ -192,22 +188,25 @@ block_production_condition::block_production_condition_enum witness_plugin::bloc switch( result ) { case block_production_condition::produced: - ilog("Generated block #${n} with timestamp ${t} at time ${c}", (capture)); + ilog("Generated block #${n} with timestamp ${t} at time ${c}", + ("n", capture["n"])("t", capture["t"])("c", capture["c"])); break; case block_production_condition::not_synced: ilog("Not producing block because production is disabled until we receive a recent block (see: --enable-stale-production)"); break; case block_production_condition::not_my_turn: - ilog("Not producing block because it isn't my turn"); + //ilog("Not producing block because it isn't my turn"); break; case block_production_condition::not_time_yet: dlog("Not producing block because slot has not yet arrived"); break; case block_production_condition::no_private_key: - ilog("Not producing block because I don't have the private key for ${scheduled_key}", (capture) ); + ilog("Not producing block because I don't have the private key for ${scheduled_key}", + ("n", capture["n"])("t", capture["t"])("c", capture["c"])); break; case block_production_condition::low_participation: - elog("Not producing block because node appears to be on a minority fork with only ${pct}% witness participation", (capture) ); + elog("Not producing block because node appears to be on a minority fork with only ${pct}% witness participation", + ("n", capture["n"])("t", capture["t"])("c", capture["c"])); break; case block_production_condition::lag: elog("Not producing block because node didn't wake up within 500ms of the slot time."); @@ -227,7 +226,7 @@ block_production_condition::block_production_condition_enum witness_plugin::bloc block_production_condition::block_production_condition_enum witness_plugin::maybe_produce_block( fc::mutable_variant_object& capture ) { chain::database& db = database(); - fc::time_point now_fine = graphene::time::now(); + fc::time_point now_fine = fc::time_point::now(); fc::time_point_sec now = now_fine + fc::microseconds( 500000 ); // If the next block production opportunity is in the present or future, we're synced. diff --git a/libraries/utilities/CMakeLists.txt b/libraries/utilities/CMakeLists.txt index cce16644..f2d646d5 100644 --- a/libraries/utilities/CMakeLists.txt +++ b/libraries/utilities/CMakeLists.txt @@ -7,14 +7,14 @@ if(NOT GRAPHENE_GIT_REVISION_DESCRIPTION) set(GRAPHENE_GIT_REVISION_DESCRIPTION "unknown") endif(NOT GRAPHENE_GIT_REVISION_DESCRIPTION) -file(GLOB headers "include/graphene/utilities/*.hpp") +file(GLOB HEADERS "include/graphene/utilities/*.hpp") set(sources key_conversion.cpp string_escape.cpp tempdir.cpp words.cpp - ${headers}) + ${HEADERS}) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/git_revision.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/git_revision.cpp" @ONLY) list(APPEND sources "${CMAKE_CURRENT_BINARY_DIR}/git_revision.cpp") @@ -37,3 +37,4 @@ install( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +install( FILES ${HEADERS} DESTINATION "include/graphene/utilities" ) diff --git a/libraries/wallet/CMakeLists.txt b/libraries/wallet/CMakeLists.txt index 3d66c48e..53e75abd 100644 --- a/libraries/wallet/CMakeLists.txt +++ b/libraries/wallet/CMakeLists.txt @@ -8,7 +8,7 @@ if( PERL_FOUND AND DOXYGEN_FOUND AND NOT "${CMAKE_GENERATOR}" STREQUAL "Ninja" ) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/doxygen/perlmod/DoxyDocs.pm WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${DOXYGEN_EXECUTABLE} - DEPENDS Doxyfile include/graphene/wallet/wallet.hpp ) + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile include/graphene/wallet/wallet.hpp ) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/api_documentation.cpp COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/generate_api_documentation.pl ${CMAKE_CURRENT_BINARY_DIR}/api_documentation.cpp.new @@ -37,3 +37,4 @@ install( TARGETS LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +install( FILES ${HEADERS} DESTINATION "include/graphene/wallet" ) diff --git a/libraries/wallet/include/graphene/wallet/reflect_util.hpp b/libraries/wallet/include/graphene/wallet/reflect_util.hpp index 8ffe765e..b8d38473 100644 --- a/libraries/wallet/include/graphene/wallet/reflect_util.hpp +++ b/libraries/wallet/include/graphene/wallet/reflect_util.hpp @@ -62,7 +62,7 @@ struct static_variant_map_visitor template< typename T > result_type operator()( const T& dummy ) { - assert( which == (int)m.which_to_name.size() ); + //assert( which == (int)m.which_to_name.size() ); std::string name = clean_name( fc::get_typename::name() ); m.name_to_which[ name ] = which; m.which_to_name.push_back( name ); diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 0ac0b88a..602e80ae 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -29,6 +29,7 @@ using namespace graphene::app; using namespace graphene::chain; using namespace graphene::utilities; +using namespace graphene::bookie; using namespace std; namespace fc @@ -260,6 +261,26 @@ namespace detail { class wallet_api_impl; } +/*** + * A utility class for performing various state-less actions that are related to wallets + */ +class utility { + public: + /** + * Derive any number of *possible* owner keys from a given brain key. + * + * NOTE: These keys may or may not match with the owner keys of any account. + * This function is merely intended to assist with account or key recovery. + * + * @see suggest_brain_key() + * + * @param brain_key Brain key + * @param number_of_desired_keys Number of desired keys + * @return A list of keys that are deterministically derived from the brainkey + */ + static vector derive_owner_keys_from_brain_key(string brain_key, int number_of_desired_keys = 1); +}; + struct operation_detail { string memo; string description; @@ -332,16 +353,25 @@ class wallet_api * * This returns a list of operation history objects, which describe activity on the account. * - * @note this API doesn't give a way to retrieve more than the most recent 100 transactions, - * you can interface directly with the blockchain to get more history * @param name the name or id of the account - * @param limit the number of entries to return (starting from the most recent) (max 100) + * @param limit the number of entries to return (starting from the most recent) * @returns a list of \c operation_history_objects */ vector get_account_history(string name, int limit)const; + /** Returns the relative operations on the named account from start number. + * + * @param name the name or id of the account + * @param stop Sequence number of earliest operation. + * @param limit the number of entries to return + * @param start the sequence number where to start looping back throw the history + * @returns a list of \c operation_history_objects + */ + vector get_relative_account_history(string name, uint32_t stop, int limit, uint32_t start)const; - vector get_market_history(string symbol, string symbol2, uint32_t bucket)const; + vector list_core_accounts()const; + + vector get_market_history(string symbol, string symbol2, uint32_t bucket, fc::time_point_sec start, fc::time_point_sec end)const; vector get_limit_orders(string a, string b, uint32_t limit)const; vector get_call_orders(string a, uint32_t limit)const; vector get_settle_orders(string a, uint32_t limit)const; @@ -580,6 +610,29 @@ class wallet_api */ brain_key_info suggest_brain_key()const; + /** + * Derive any number of *possible* owner keys from a given brain key. + * + * NOTE: These keys may or may not match with the owner keys of any account. + * This function is merely intended to assist with account or key recovery. + * + * @see suggest_brain_key() + * + * @param brain_key Brain key + * @param numberOfDesiredKeys Number of desired keys + * @return A list of keys that are deterministically derived from the brainkey + */ + vector derive_owner_keys_from_brain_key(string brain_key, int number_of_desired_keys = 1) const; + + /** + * Determine whether a textual representation of a public key + * (in Base-58 format) is *currently* linked + * to any *registered* (i.e. non-stealth) account on the blockchain + * @param public_key Public key + * @return Whether a public key is known + */ + bool is_public_key_registered(string public_key) const; + /** * @param role - active | owner | memo */ @@ -590,9 +643,7 @@ class wallet_api * TODO: I don't see a broadcast_transaction() function, do we need one? * * @param tx the transaction to serialize - * @returns the binary form of the transaction. It will not be hex encoded, - * this returns a raw string that may have null characters embedded - * in it + * @returns the be hex encoded form of the serialized transaction */ string serialize_transaction(signed_transaction tx) const; @@ -1519,6 +1570,162 @@ class wallet_api order_book get_order_book( const string& base, const string& quote, unsigned limit = 50); + asset get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id); + std::vector get_events_containing_sub_string(const std::string& sub_string, const std::string& language); + + /** Get an order book for a betting market, with orders aggregated into bins with similar + * odds + * + * @param betting_market_id the betting market + * @param precision the number of digits of precision for binning + */ + binned_order_book get_binned_order_book(graphene::chain::betting_market_id_type betting_market_id, int32_t precision); + + std::vector get_matched_bets_for_bettor(account_id_type bettor_id) const; + + std::vector get_all_matched_bets_for_bettor(account_id_type bettor_id, bet_id_type start = bet_id_type(), unsigned limit = 1000) const; + + vector list_sports() const; + vector list_event_groups(sport_id_type sport_id) const; + vector list_betting_market_groups(event_id_type event_id) const; + vector list_betting_markets(betting_market_group_id_type betting_market_group_id) const; + global_betting_statistics_object get_global_betting_statistics() const; + vector list_events_in_group(event_group_id_type event_group_id) const; + vector get_unmatched_bets_for_bettor(betting_market_id_type betting_market_id, account_id_type account_id) const; + vector get_all_unmatched_bets_for_bettor(account_id_type account_id) const; + + signed_transaction propose_create_sport( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + bool broadcast = false); + + signed_transaction propose_update_sport( + const string& proposing_account, + fc::time_point_sec expiration_time, + sport_id_type sport_id, + fc::optional name, + bool broadcast = false); + + signed_transaction propose_create_event_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + sport_id_type sport_id, + bool broadcast = false); + + signed_transaction propose_update_event_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + event_group_id_type event_group, + fc::optional sport_id, + fc::optional name, + bool broadcast = false); + + signed_transaction propose_create_event( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + internationalized_string_type season, + fc::optional start_time, + event_group_id_type event_group_id, + bool broadcast = false); + + signed_transaction propose_update_event( + const string& proposing_account, + fc::time_point_sec expiration_time, + event_id_type event_id, + fc::optional event_group_id, + fc::optional name, + fc::optional season, + fc::optional status, + fc::optional start_time, + bool broadcast = false); + + signed_transaction propose_create_betting_market_rules( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + internationalized_string_type description, + bool broadcast = false); + + signed_transaction propose_update_betting_market_rules( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_rules_id_type rules_id, + fc::optional name, + fc::optional description, + bool broadcast = false); + + signed_transaction propose_create_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type description, + event_id_type event_id, + betting_market_rules_id_type rules_id, + asset_id_type asset_id, + bool broadcast = false); + + signed_transaction propose_update_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type betting_market_group_id, + fc::optional description, + fc::optional rules_id, + fc::optional status, + bool broadcast = false); + + signed_transaction propose_create_betting_market( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type group_id, + internationalized_string_type description, + internationalized_string_type payout_condition, + bool broadcast = false); + + signed_transaction propose_update_betting_market( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_id_type market_id, + fc::optional group_id, + fc::optional description, + fc::optional payout_condition, + bool broadcast = false); + + /** Place a bet + * @param bettor the account placing the bet + * @param betting_market_id the market on which to bet + * @param back_or_lay back or lay + * @param amount the amount to bet + * @param asset_symbol the asset to bet with (must be the same as required by the betting market group) + * @param backer_multiplier the odds (use 2.0 for a 1:1 bet) + * @param broadcast true to broadcast the transaction + */ + signed_transaction place_bet(string bettor, + betting_market_id_type betting_market_id, + bet_type back_or_lay, + string amount, + string asset_symbol, + double backer_multiplier, + bool broadcast = false); + + signed_transaction cancel_bet(string betting_account, + bet_id_type bet_id, + bool broadcast = false); + + signed_transaction propose_resolve_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type betting_market_group_id, + const std::map& resolutions, + bool broadcast = false); + + signed_transaction propose_cancel_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type betting_market_group_id, + bool broadcast = false); + /** Creates a new tournament * @param creator the accout that is paying the fee to create the tournament * @param options the options detailing the specifics of the tournament @@ -1693,6 +1900,7 @@ FC_API( graphene::wallet::wallet_api, (import_account_keys) (import_balance) (suggest_brain_key) + (derive_owner_keys_from_brain_key) (get_private_key_from_password) (register_account) (upgrade_account) @@ -1740,6 +1948,9 @@ FC_API( graphene::wallet::wallet_api, (get_block) (get_account_count) (get_account_history) + (get_relative_account_history) + (is_public_key_registered) + (list_core_accounts) (get_market_history) (get_global_properties) (get_dynamic_global_properties) @@ -1779,6 +1990,29 @@ FC_API( graphene::wallet::wallet_api, (blind_transfer) (blind_history) (receive_blind_transfer) + (list_sports) + (list_event_groups) + (list_betting_market_groups) + (list_betting_markets) + (list_events_in_group) + (get_unmatched_bets_for_bettor) + (get_all_unmatched_bets_for_bettor) + (get_global_betting_statistics) + (propose_create_sport) + (propose_create_event_group) + (propose_create_event) + (propose_create_betting_market_group) + (propose_create_betting_market) + (propose_create_betting_market_rules) + (propose_update_betting_market_rules) + (propose_update_sport) + (propose_update_event_group) + (propose_update_event) + (propose_update_betting_market_group) + (propose_update_betting_market) + (place_bet) + (cancel_bet) + (propose_resolve_betting_market_group) (tournament_create) (tournament_join) (tournament_leave) @@ -1788,4 +2022,9 @@ FC_API( graphene::wallet::wallet_api, (get_tournaments_by_state) (get_tournament) (get_order_book) + (get_total_matched_bet_amount_for_betting_market_group) + (get_events_containing_sub_string) + (get_binned_order_book) + (get_matched_bets_for_bettor) + (get_all_matched_bets_for_bettor) ) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 8f24192a..1497a996 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -72,6 +72,8 @@ #include #include +#include + #include #include #include @@ -135,6 +137,10 @@ public: std::string operator()(const asset_create_operation& op)const; std::string operator()(const asset_dividend_distribution_operation& op)const; std::string operator()(const tournament_payout_operation& op)const; + std::string operator()(const bet_place_operation& op)const; + std::string operator()(const bet_matched_operation& op)const; + std::string operator()(const bet_canceled_operation& op)const; + std::string operator()(const bet_adjusted_operation& op)const; }; template @@ -525,7 +531,8 @@ public: _remote_api(rapi), _remote_db(rapi->database()), _remote_net_broadcast(rapi->network_broadcast()), - _remote_hist(rapi->history()) + _remote_hist(rapi->history()), + _remote_bookie(rapi->bookie()) { chain_id_type remote_chain_id = _remote_db->get_chain_id(); if( remote_chain_id != _chain_id ) @@ -2445,6 +2452,23 @@ public: return ss.str(); }; + m["get_relative_account_history"] = [this](variant result, const fc::variants& a) + { + auto r = result.as>(); + std::stringstream ss; + + for( operation_detail& d : r ) + { + operation_history_object& i = d.op; + auto b = _remote_db->get_block_header(i.block_num); + FC_ASSERT(b); + ss << b->timestamp.to_iso_string() << " "; + i.op.visit(operation_printer(ss, *this, i.result)); + ss << " \n"; + } + + return ss.str(); + }; m["list_account_balances"] = [this](variant result, const fc::variants& a) { @@ -2461,6 +2485,21 @@ public: return ss.str(); }; + m["list_core_accounts"] = [this](variant result, const fc::variants& a) + { + std::stringstream ss; + + auto balances = result.as>(); + for (const account_balance_object& balance: balances) + { + const account_object& account = get_account(balance.owner); + //ss << account.name << " " << std::string(balance.id) << " " << balance.balance.value << "\n"; + ss << account.name << " " << std::string(balance.id) << " " << get_asset(balance.asset_type).amount_to_pretty_string(balance.balance) << "\n"; + } + + return ss.str(); + }; + m["get_blind_balances"] = [this](variant result, const fc::variants& a) { auto r = result.as>(); @@ -3127,6 +3166,7 @@ public: fc::api _remote_db; fc::api _remote_net_broadcast; fc::api _remote_hist; + fc::api _remote_bookie; optional< fc::api > _remote_net_node; optional< fc::api > _remote_debug; @@ -3290,6 +3330,46 @@ std::string operation_printer::operator()(const tournament_payout_operation& op) return ""; } +std::string operation_printer::operator()(const bet_place_operation& op)const +{ + auto fee_asset = wallet.get_asset(op.fee.asset_id); + auto asset = wallet.get_asset(op.amount_to_bet.asset_id); + auto bettor = wallet.get_account(op.bettor_id); + + out << bettor.name << " placed a " << fc::json::to_string(op.back_or_lay) << " bet for " + << asset.amount_to_pretty_string(op.amount_to_bet) << " at odds " << ((double)op.backer_multiplier / GRAPHENE_BETTING_ODDS_PRECISION) + << " on market " << fc::json::to_string(op.betting_market_id) + << " fee: " << fee_asset.amount_to_pretty_string(op.fee); + return ""; +} + +std::string operation_printer::operator()(const bet_matched_operation& op)const +{ + auto asset = wallet.get_asset(op.amount_bet.asset_id); + auto bettor = wallet.get_account(op.bettor_id); + + out << " " << bettor.name << "'s bet " << fc::json::to_string(op.bet_id) << " matched " << asset.amount_to_pretty_string(op.amount_bet) << " at odds " << ((double)op.backer_multiplier / GRAPHENE_BETTING_ODDS_PRECISION); + return ""; +} + +std::string operation_printer::operator()(const bet_canceled_operation& op)const +{ + auto asset = wallet.get_asset(op.stake_returned.asset_id); + auto bettor = wallet.get_account(op.bettor_id); + + out << " " << bettor.name << "'s bet " << fc::json::to_string(op.bet_id) << " was canceled, " << asset.amount_to_pretty_string(op.stake_returned) << " returned"; + return ""; +} + +std::string operation_printer::operator()(const bet_adjusted_operation& op)const +{ + auto asset = wallet.get_asset(op.stake_returned.asset_id); + auto bettor = wallet.get_account(op.bettor_id); + + out << " " << bettor.name << "'s bet " << fc::json::to_string(op.bet_id) << " was adjusted, " << asset.amount_to_pretty_string(op.stake_returned) << " returned"; + return ""; +} + std::string operation_result_printer::operator()(const void_result& x) const { return ""; @@ -3307,7 +3387,29 @@ std::string operation_result_printer::operator()(const asset& a) }}} +namespace graphene { namespace wallet { + vector utility::derive_owner_keys_from_brain_key(string brain_key, int number_of_desired_keys) + { + // Safety-check + FC_ASSERT( number_of_desired_keys >= 1 ); + // Create as many derived owner keys as requested + vector results; + brain_key = graphene::wallet::detail::normalize_brain_key(brain_key); + for (int i = 0; i < number_of_desired_keys; ++i) { + fc::ecc::private_key priv_key = graphene::wallet::detail::derive_private_key( brain_key, i ); + + brain_key_info result; + result.brain_priv_key = brain_key; + result.wif_priv_key = key_to_wif( priv_key ); + result.pub_key = priv_key.get_public_key(); + + results.push_back(result); + } + + return results; + } +}} namespace graphene { namespace wallet { @@ -3386,10 +3488,38 @@ vector wallet_api::get_account_history(string name, int limit) return result; } - -vector wallet_api::get_market_history( string symbol1, string symbol2, uint32_t bucket )const +vector wallet_api::get_relative_account_history(string name, uint32_t stop, int limit, uint32_t start)const { - return my->_remote_hist->get_market_history( get_asset_id(symbol1), get_asset_id(symbol2), bucket, fc::time_point_sec(), fc::time_point::now() ); + + FC_ASSERT( start > 0 || limit <= 100 ); + + vector result; + auto account_id = get_account(name).get_id(); + + while( limit > 0 ) + { + vector current = my->_remote_hist->get_relative_account_history(account_id, stop, std::min(100, limit), start); + for (auto &o : current) { + std::stringstream ss; + auto memo = o.op.visit(detail::operation_printer(ss, *my, o.result)); + result.push_back(operation_detail{memo, ss.str(), o}); + } + if (current.size() < std::min(100, limit)) + break; + limit -= current.size(); + start -= 100; + if( start == 0 ) break; + } + return result; +} +vector wallet_api::list_core_accounts()const +{ + return my->_remote_hist->list_core_accounts(); +} + +vector wallet_api::get_market_history( string symbol1, string symbol2, uint32_t bucket , fc::time_point_sec start, fc::time_point_sec end )const +{ + return my->_remote_hist->get_market_history( get_asset_id(symbol1), get_asset_id(symbol2), bucket, start, end ); } vector wallet_api::get_limit_orders(string a, string b, uint32_t limit)const @@ -3437,6 +3567,17 @@ brain_key_info wallet_api::suggest_brain_key()const return result; } +vector wallet_api::derive_owner_keys_from_brain_key(string brain_key, int number_of_desired_keys) const +{ + return graphene::wallet::utility::derive_owner_keys_from_brain_key(brain_key, number_of_desired_keys); +} + +bool wallet_api::is_public_key_registered(string public_key) const +{ + bool is_known = my->_remote_db->is_public_key_registered(public_key); + return is_known; +} + pair wallet_api::get_private_key_from_password( string account, string role, string password )const { auto seed = password + account + role; FC_ASSERT( seed.size() ); @@ -4897,6 +5038,531 @@ vector wallet_api::blind_history( string key_or_account ) return result; } +/////////////// +// peerplays // +/////////////// + +vector wallet_api::list_sports() const +{ + return my->_remote_db->list_sports(); +} + +vector wallet_api::list_event_groups(sport_id_type sport_id) const +{ + return my->_remote_db->list_event_groups(sport_id); +} + +vector wallet_api::list_betting_market_groups(event_id_type event_id) const +{ + return my->_remote_db->list_betting_market_groups(event_id); +} + +vector wallet_api::list_betting_markets(betting_market_group_id_type betting_market_group_id) const +{ + return my->_remote_db->list_betting_markets(betting_market_group_id); +} + +global_betting_statistics_object wallet_api::get_global_betting_statistics() const +{ + return my->_remote_db->get_global_betting_statistics(); +} + +vector wallet_api::list_events_in_group(event_group_id_type event_group_id) const +{ + return my->_remote_db->list_events_in_group(event_group_id); +} + +vector wallet_api::get_unmatched_bets_for_bettor(betting_market_id_type betting_market_id, account_id_type account_id) const +{ + return my->_remote_db->get_unmatched_bets_for_bettor(betting_market_id, account_id); +} + +vector wallet_api::get_all_unmatched_bets_for_bettor(account_id_type account_id) const +{ + return my->_remote_db->get_all_unmatched_bets_for_bettor(account_id); +} + +signed_transaction wallet_api::propose_create_sport( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + sport_create_operation sport_create_op; + sport_create_op.name = name; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( sport_create_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_update_sport( + const string& proposing_account, + fc::time_point_sec expiration_time, + sport_id_type sport_id, + fc::optional name, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + sport_update_operation sport_update_op; + sport_update_op.sport_id = sport_id; + sport_update_op.new_name = name; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( sport_update_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_create_event_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + sport_id_type sport_id, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + event_group_create_operation event_group_create_op; + event_group_create_op.name = name; + event_group_create_op.sport_id = sport_id; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( event_group_create_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_update_event_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + event_group_id_type event_group, + fc::optional sport_id, + fc::optional name, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + event_group_update_operation event_group_update_op; + event_group_update_op.new_sport_id = sport_id; + event_group_update_op.new_name = name; + event_group_update_op.event_group_id = event_group; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( event_group_update_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_create_event( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + internationalized_string_type season, + fc::optional start_time, + event_group_id_type event_group_id, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + event_create_operation event_create_op; + event_create_op.start_time = start_time; + event_create_op.name = name; + event_create_op.season = season; + event_create_op.event_group_id = event_group_id; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( event_create_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_update_event( + const string& proposing_account, + fc::time_point_sec expiration_time, + event_id_type event_id, + fc::optional event_group_id, + fc::optional name, + fc::optional season, + fc::optional status, + fc::optional start_time, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + event_update_operation event_update_op; + event_update_op.event_id = event_id; + event_update_op.new_event_group_id = event_group_id; + event_update_op.new_start_time = start_time; + event_update_op.new_name = name; + event_update_op.new_season = season; + event_update_op.new_status = status; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( event_update_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_create_betting_market_rules( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type name, + internationalized_string_type description, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_rules_create_operation betting_market_rules_create_op; + betting_market_rules_create_op.name = name; + betting_market_rules_create_op.description = description; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_rules_create_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_update_betting_market_rules( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_rules_id_type rules_id, + fc::optional name, + fc::optional description, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_rules_update_operation betting_market_rules_update_op; + betting_market_rules_update_op.betting_market_rules_id = rules_id; + betting_market_rules_update_op.new_name = name; + betting_market_rules_update_op.new_description = description; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_rules_update_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_create_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + internationalized_string_type description, + event_id_type event_id, + betting_market_rules_id_type rules_id, + asset_id_type asset_id, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_group_create_operation betting_market_group_create_op; + betting_market_group_create_op.description = description; + betting_market_group_create_op.event_id = event_id; + betting_market_group_create_op.rules_id = rules_id; + betting_market_group_create_op.asset_id = asset_id; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_group_create_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_update_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type betting_market_group_id, + fc::optional description, + fc::optional rules_id, + fc::optional status, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_group_update_operation betting_market_group_update_op; + betting_market_group_update_op.betting_market_group_id = betting_market_group_id; + betting_market_group_update_op.new_description = description; + betting_market_group_update_op.new_rules_id = rules_id; + betting_market_group_update_op.status = status; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_group_update_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_create_betting_market( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type group_id, + internationalized_string_type description, + internationalized_string_type payout_condition, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_create_operation betting_market_create_op; + betting_market_create_op.group_id = group_id; + betting_market_create_op.description = description; + betting_market_create_op.payout_condition = payout_condition; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_create_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_update_betting_market( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_id_type market_id, + fc::optional group_id, + fc::optional description, + fc::optional payout_condition, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_update_operation betting_market_update_op; + betting_market_update_op.betting_market_id = market_id; + betting_market_update_op.new_group_id = group_id; + betting_market_update_op.new_description = description; + betting_market_update_op.new_payout_condition = payout_condition; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_update_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::place_bet(string betting_account, + betting_market_id_type betting_market_id, + bet_type back_or_lay, + string amount, + string asset_symbol, + double backer_multiplier, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + fc::optional asset_obj = get_asset(asset_symbol); + FC_ASSERT(asset_obj, "Could not find asset matching ${asset}", ("asset", asset_symbol)); + + const chain_parameters& current_params = get_global_properties().parameters; + + bet_place_operation bet_place_op; + bet_place_op.bettor_id = get_account(betting_account).id; + bet_place_op.betting_market_id = betting_market_id; + bet_place_op.amount_to_bet = asset_obj->amount_from_string(amount); + bet_place_op.backer_multiplier = (bet_multiplier_type)(backer_multiplier * GRAPHENE_BETTING_ODDS_PRECISION); + bet_place_op.back_or_lay = back_or_lay; + + signed_transaction tx; + tx.operations.push_back(bet_place_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::cancel_bet(string betting_account, + bet_id_type bet_id, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + + const chain_parameters& current_params = get_global_properties().parameters; + + bet_cancel_operation bet_cancel_op; + bet_cancel_op.bettor_id = get_account(betting_account).id; + bet_cancel_op.bet_to_cancel = bet_id; + + signed_transaction tx; + tx.operations.push_back(bet_cancel_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_resolve_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type betting_market_group_id, + const std::map& resolutions, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_group_resolve_operation betting_market_group_resolve_op; + betting_market_group_resolve_op.betting_market_group_id = betting_market_group_id; + betting_market_group_resolve_op.resolutions = resolutions; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_group_resolve_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + +signed_transaction wallet_api::propose_cancel_betting_market_group( + const string& proposing_account, + fc::time_point_sec expiration_time, + betting_market_group_id_type betting_market_group_id, + bool broadcast /*= false*/) +{ + FC_ASSERT( !is_locked() ); + const chain_parameters& current_params = get_global_properties().parameters; + + betting_market_group_cancel_unmatched_bets_operation betting_market_group_cancel_unmatched_bets_op; + betting_market_group_cancel_unmatched_bets_op.betting_market_group_id = betting_market_group_id; + + proposal_create_operation prop_op; + prop_op.expiration_time = expiration_time; + prop_op.review_period_seconds = current_params.committee_proposal_review_period; + prop_op.fee_paying_account = get_account(proposing_account).id; + prop_op.proposed_ops.emplace_back( betting_market_group_cancel_unmatched_bets_op ); + current_params.current_fees->set_fee( prop_op.proposed_ops.back().op ); + + signed_transaction tx; + tx.operations.push_back(prop_op); + my->set_operation_fees(tx, current_params.current_fees); + tx.validate(); + + return my->sign_transaction(tx, broadcast); +} + signed_transaction wallet_api::tournament_create( string creator, tournament_options options, bool broadcast ) { FC_ASSERT( !is_locked() ); @@ -5046,6 +5712,32 @@ order_book wallet_api::get_order_book( const string& base, const string& quote, return( my->_remote_db->get_order_book( base, quote, limit ) ); } +asset wallet_api::get_total_matched_bet_amount_for_betting_market_group(betting_market_group_id_type group_id) +{ + return( my->_remote_bookie->get_total_matched_bet_amount_for_betting_market_group(group_id) ); +} + +std::vector wallet_api::get_events_containing_sub_string(const std::string& sub_string, const std::string& language) +{ + return( my->_remote_bookie->get_events_containing_sub_string(sub_string, language) ); +} + +binned_order_book wallet_api::get_binned_order_book(graphene::chain::betting_market_id_type betting_market_id, int32_t precision) +{ + return( my->_remote_bookie->get_binned_order_book(betting_market_id, precision) ); +} + +std::vector wallet_api::get_matched_bets_for_bettor(account_id_type bettor_id) const +{ + return( my->_remote_bookie->get_matched_bets_for_bettor(bettor_id) ); +} + +std::vector wallet_api::get_all_matched_bets_for_bettor(account_id_type bettor_id, bet_id_type start, unsigned limit) const +{ + return( my->_remote_bookie->get_all_matched_bets_for_bettor(bettor_id, start, limit) ); +} + +// default ctor necessary for FC_REFLECT signed_block_with_info::signed_block_with_info( const signed_block& block ) : signed_block( block ) { diff --git a/programs/CMakeLists.txt b/programs/CMakeLists.txt index 0e5d8950..ba73cdca 100644 --- a/programs/CMakeLists.txt +++ b/programs/CMakeLists.txt @@ -3,6 +3,7 @@ if( BUILD_BITSHARES_PROGRAMS ) add_subdirectory( cli_wallet ) add_subdirectory( genesis_util ) add_subdirectory( witness_node ) + add_subdirectory( debug_node ) add_subdirectory( delayed_node ) add_subdirectory( js_operation_serializer ) add_subdirectory( size_checker ) diff --git a/programs/debug_node/CMakeLists.txt b/programs/debug_node/CMakeLists.txt index 8ec7362b..232d265e 100644 --- a/programs/debug_node/CMakeLists.txt +++ b/programs/debug_node/CMakeLists.txt @@ -10,7 +10,7 @@ if( GPERFTOOLS_FOUND ) endif() target_link_libraries( debug_node - PRIVATE graphene_app graphene_account_history graphene_market_history graphene_witness graphene_debug_witness graphene_chain graphene_egenesis_full fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} ) + PRIVATE graphene_app graphene_account_history graphene_market_history graphene_witness graphene_debug_witness graphene_bookie graphene_chain graphene_egenesis_full fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} ) install( TARGETS debug_node diff --git a/programs/debug_node/README.md b/programs/debug_node/README.md new file mode 100644 index 00000000..53d56733 --- /dev/null +++ b/programs/debug_node/README.md @@ -0,0 +1,104 @@ + +Introduction +------------ + +The `debug_node` is a tool to allow developers to run many interesting sorts of "what-if" tests using state from a production blockchain. +Like "what happens if I produce enough blocks for the next hardfork time to arrive?" or "what would happen if this account (which I don't have a private key for) did this transaction?" + +Setup +----- + +Be sure you've built the right build targets: + + $ make get_dev_key debug_node cli_wallet witness_node + +Use the `get_dev_key` utility to generate a keypair: + + $ programs/genesis_util/get_dev_key "" nathan + [{"private_key":"5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3","public_key":"BTS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV","address":"BTSFAbAx7yuxt725qSZvfwWqkdCwp9ZnUama"}] + +Obtain a copy of the blockchain in `block_db` directory: + $ programs/witness_node/witness_node --data-dir data/mydatadir + # ... wait for chain to sync + ^C + $ cp -Rp data/mydatadir/blockchain/database/block_num_to_block ./block_db + +Set up a new datadir with the following `config.ini` settings: + + # setup API endpoint + rpc-endpoint = 127.0.0.1:8090 + # setting this to empty effectively disables the p2p network + seed-nodes = [] + # set apiaccess.json so we can set up + api-access = "data/debug_datadir/api-access.json" + +Then set up `data/debug_datadir/api-access.json` to allow access to the debug API like this: + + { + "permission_map" : + [ + [ + "bytemaster", + { + "password_hash_b64" : "9e9GF7ooXVb9k4BoSfNIPTelXeGOZ5DrgOYMj94elaY=", + "password_salt_b64" : "INDdM6iCi/8=", + "allowed_apis" : ["database_api", "network_broadcast_api", "history_api", "network_node_api", "debug_api"] + } + ], + [ + "*", + { + "password_hash_b64" : "*", + "password_salt_b64" : "*", + "allowed_apis" : ["database_api", "network_broadcast_api", "history_api"] + } + ] + ] + } + +See [here](https://github.com/cryptonomex/graphene#accessing-restricted-apis) for more detail on the `api-access.json` format. + +Once that is set up, run `debug_node` against your newly prepared datadir: + + programs/debug_node/debug_node --data-dir data/debug_datadir + +Run `cli_wallet` to connect to the `debug_node` port, using the username and password to access the new `debug_api` (and also a different wallet file): + + programs/cli_wallet/cli_wallet -s 127.0.0.1:8090 -w debug.wallet -u bytemaster -p supersecret + +Example usage +------------- + +Load some blocks from the datadir: + + dbg_push_blocks block_db 20000 + +Note, when pushing a very large number of blocks sometimes `cli_wallet` hangs and you must Ctrl+C and restart it (leaving the `debug_node` running). + +Generate (fake) blocks with our own private key: + + dbg_generate_blocks 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 1000 + +Update `angel` account to be controlled by our own private key and generate a (fake) transfer: + + dbg_update_object {"_action":"update", "id":"1.2.1090", "active":{"weight_threshold":1,"key_auths":[["BTS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV",1]]}} + import_key angel 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 + transfer angel init0 999999 BTS "" true + +How it works +------------ + +The commands work by creating diff(s) from the main chain that are applied to the local chain at specified block height(s). It lets you easily check out "what-if" +scenarios in a fantasy debug toy world forked from the real chain, e.g. "if we take all of the blocks until today, then generate a bunch more until a hardfork time +in the future arrives, does the chain stay up? Can I do transactions X, Y, and Z in the wallet after the hardfork?" Anyone connecting to this node sees the same +fantasy world, so you can e.g. make changes with the `cli_wallet` and see them exist in other `cli_wallet` instances (or GUI wallets or API scripts). + +Limitations +----------- + +The main limitations are: + +- No export format for the diffs, so you can't really [1] connect multiple `debug_node` to each other. +- Once faked block(s) or tx(s) have been produced on your chain, you can't really [1] stream blocks or tx's from the main network to your chain. + +[1] It should theoretically be possible, but it's non-trivial and totally untested. diff --git a/programs/debug_node/main.cpp b/programs/debug_node/main.cpp index 4b89c199..7c3d358a 100644 --- a/programs/debug_node/main.cpp +++ b/programs/debug_node/main.cpp @@ -222,7 +222,7 @@ void write_default_logging_config_to_stream(std::ostream& out) "appenders=stderr\n\n" "# route messages sent to the \"p2p\" logger to the p2p appender declared above\n" "[logger.p2p]\n" - "level=debug\n" + "level=info\n" "appenders=p2p\n\n"; } diff --git a/programs/delayed_node/main.cpp b/programs/delayed_node/main.cpp index 430fcfa3..74cd8fc3 100644 --- a/programs/delayed_node/main.cpp +++ b/programs/delayed_node/main.cpp @@ -207,7 +207,7 @@ void write_default_logging_config_to_stream(std::ostream& out) "appenders=stderr\n\n" "# route messages sent to the \"p2p\" logger to the p2p appender declared above\n" "[logger.p2p]\n" - "level=debug\n" + "level=info\n" "appenders=p2p\n\n"; } diff --git a/programs/js_operation_serializer/main.cpp b/programs/js_operation_serializer/main.cpp index 6c60d943..8994b36b 100644 --- a/programs/js_operation_serializer/main.cpp +++ b/programs/js_operation_serializer/main.cpp @@ -36,6 +36,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -378,7 +382,6 @@ struct serializer_init_helper { } }; - template struct serializer { @@ -388,7 +391,6 @@ struct serializer { serializer_init_helper< T, typename fc::reflector::is_enum >::init(); } - }; } // namespace detail_ns diff --git a/programs/witness_node/CMakeLists.txt b/programs/witness_node/CMakeLists.txt index 7535d3ad..efb4acf8 100644 --- a/programs/witness_node/CMakeLists.txt +++ b/programs/witness_node/CMakeLists.txt @@ -11,7 +11,7 @@ endif() # We have to link against graphene_debug_witness because deficiency in our API infrastructure doesn't allow plugins to be fully abstracted #246 target_link_libraries( witness_node - PRIVATE graphene_app graphene_account_history graphene_market_history graphene_witness graphene_chain graphene_debug_witness graphene_egenesis_full fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} ) + PRIVATE graphene_app graphene_account_history graphene_market_history graphene_witness graphene_chain graphene_debug_witness graphene_bookie graphene_egenesis_full fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} ) # also add dependencies to graphene_generate_genesis graphene_generate_uia_sharedrop_genesis if you want those plugins install( TARGETS diff --git a/programs/witness_node/main.cpp b/programs/witness_node/main.cpp index 2f41676c..5bfb6070 100644 --- a/programs/witness_node/main.cpp +++ b/programs/witness_node/main.cpp @@ -25,9 +25,12 @@ #include #include +#include #include //#include //#include +#include +#include #include #include @@ -68,6 +71,7 @@ int main(int argc, char** argv) { bpo::options_description cfg_options("Graphene Witness Node"); app_options.add_options() ("help,h", "Print this help message and exit.") + ("version", "Display the version info and exit") ("data-dir,d", bpo::value()->default_value("witness_node_data_dir"), "Directory containing databases, configuration file, etc.") ; @@ -78,6 +82,8 @@ int main(int argc, char** argv) { auto market_history_plug = node->register_plugin(); //auto generate_genesis_plug = node->register_plugin(); //auto generate_uia_sharedrop_genesis_plug = node->register_plugin(); + auto list_plug = node->register_plugin(); + auto bookie_plug = node->register_plugin(); try { @@ -98,6 +104,17 @@ int main(int argc, char** argv) { std::cout << app_options << "\n"; return 0; } + if (options.count("version")) + { + std::string witness_version(graphene::utilities::git_revision_description); + const size_t pos = witness_version.find('/'); + if( pos != std::string::npos && witness_version.size() > pos ) + witness_version = witness_version.substr( pos + 1 ); + std::cerr << "Version: " << witness_version << "\n"; + std::cerr << "Git Revision: " << graphene::utilities::git_revision_sha << "\n"; + std::cerr << "Built: " << __DATE__ " at " __TIME__ << "\n"; + return 0; + } fc::path data_dir; if( options.count("data-dir") ) @@ -226,7 +243,7 @@ void write_default_logging_config_to_stream(std::ostream& out) "appenders=stderr\n\n" "# route messages sent to the \"p2p\" logger to the p2p appender declared above\n" "[logger.p2p]\n" - "level=debug\n" + "level=info\n" "appenders=p2p\n\n"; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c8de1558..57a451aa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,26 +8,30 @@ endif() file(GLOB UNIT_TESTS "tests/*.cpp") add_executable( chain_test ${UNIT_TESTS} ${COMMON_SOURCES} ) -target_link_libraries( chain_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) +target_link_libraries( chain_test graphene_chain graphene_app graphene_account_history graphene_bookie graphene_egenesis_none fc graphene_wallet ${PLATFORM_SPECIFIC_LIBS} ) if(MSVC) set_source_files_properties( tests/serialization_tests.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) endif(MSVC) file(GLOB PERFORMANCE_TESTS "performance/*.cpp") add_executable( performance_test ${PERFORMANCE_TESTS} ${COMMON_SOURCES} ) -target_link_libraries( performance_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) +target_link_libraries( performance_test graphene_chain graphene_app graphene_account_history graphene_bookie graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) file(GLOB BENCH_MARKS "benchmarks/*.cpp") add_executable( chain_bench ${BENCH_MARKS} ${COMMON_SOURCES} ) -target_link_libraries( chain_bench graphene_chain graphene_app graphene_account_history graphene_time graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) +target_link_libraries( chain_bench graphene_chain graphene_app graphene_account_history graphene_bookie graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) file(GLOB APP_SOURCES "app/*.cpp") add_executable( app_test ${APP_SOURCES} ) -target_link_libraries( app_test graphene_app graphene_account_history graphene_net graphene_chain graphene_time graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) +target_link_libraries( app_test graphene_app graphene_account_history graphene_bookie graphene_net graphene_chain graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) file(GLOB INTENSE_SOURCES "intense/*.cpp") add_executable( intense_test ${INTENSE_SOURCES} ${COMMON_SOURCES} ) -target_link_libraries( intense_test graphene_chain graphene_app graphene_account_history graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) +target_link_libraries( intense_test graphene_chain graphene_app graphene_account_history graphene_bookie graphene_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) + +file(GLOB BETTING_TESTS "betting/*.cpp") +add_executable( betting_test ${BETTING_TESTS} ${COMMON_SOURCES} ) +target_link_libraries( betting_test graphene_chain graphene_app graphene_account_history graphene_bookie graphene_egenesis_none fc graphene_wallet ${PLATFORM_SPECIFIC_LIBS} ) file(GLOB TOURNAMENT_TESTS "tournament/*.cpp") add_executable( tournament_test ${TOURNAMENT_TESTS} ${COMMON_SOURCES} ) diff --git a/tests/app/main.cpp b/tests/app/main.cpp index 50ed9f07..20f140ee 100644 --- a/tests/app/main.cpp +++ b/tests/app/main.cpp @@ -26,8 +26,6 @@ #include -#include - #include #include diff --git a/tests/benchmarks/genesis_allocation.cpp b/tests/benchmarks/genesis_allocation.cpp index 470d586d..61a3b1b8 100644 --- a/tests/benchmarks/genesis_allocation.cpp +++ b/tests/benchmarks/genesis_allocation.cpp @@ -25,8 +25,6 @@ #include #include -#include - #include #include diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp new file mode 100644 index 00000000..60c3804f --- /dev/null +++ b/tests/betting/betting_tests.cpp @@ -0,0 +1,2651 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +// disable auto_ptr deprecated warning, see https://svn.boost.org/trac10/ticket/11622 +#include +#include +#pragma GCC diagnostic pop + +#include "../common/database_fixture.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +//#include + +struct enable_betting_logging_config { + enable_betting_logging_config() + { + fc::logger::get("betting").add_appender(fc::appender::get("stdout")); + fc::logger::get("betting").set_log_level(fc::log_level::debug); + } + ~enable_betting_logging_config() { + fc::logger::get("betting").remove_appender(fc::appender::get("stdout")); + } +}; +BOOST_GLOBAL_FIXTURE( enable_betting_logging_config ); + +using namespace graphene::chain; +using namespace graphene::chain::test; +using namespace graphene::chain::keywords; + + +// While the bets are placed, stored, and sorted using the decimal form of their odds, matching +// uses the ratios to ensure no rounding takes place. +// The allowed odds are defined by rules that can be changed at runtime. +// For reference when designing/debugging tests, here is the list of allowed decimal odds and their +// corresponding ratios as set in the genesis block. +// +// decimal ratio | decimal ratio | decimal ratio | decimal ratio | decimal ratio | decimal ratio +// ------------------+-----------------+-------------------+-------------------+-------------------+----------------- +// 1.01 100:1 | 1.6 5:3 | 2.38 50:69 | 4.8 5:19 | 26 1:25 | 440 1:439 +// 1.02 50:1 | 1.61 100:61 | 2.4 5:7 | 4.9 10:39 | 27 1:26 | 450 1:449 +// 1.03 100:3 | 1.62 50:31 | 2.42 50:71 | 5 1:4 | 28 1:27 | 460 1:459 +// 1.04 25:1 | 1.63 100:63 | 2.44 25:36 | 5.1 10:41 | 29 1:28 | 470 1:469 +// 1.05 20:1 | 1.64 25:16 | 2.46 50:73 | 5.2 5:21 | 30 1:29 | 480 1:479 +// 1.06 50:3 | 1.65 20:13 | 2.48 25:37 | 5.3 10:43 | 32 1:31 | 490 1:489 +// 1.07 100:7 | 1.66 50:33 | 2.5 2:3 | 5.4 5:22 | 34 1:33 | 500 1:499 +// 1.08 25:2 | 1.67 100:67 | 2.52 25:38 | 5.5 2:9 | 36 1:35 | 510 1:509 +// 1.09 100:9 | 1.68 25:17 | 2.54 50:77 | 5.6 5:23 | 38 1:37 | 520 1:519 +// 1.1 10:1 | 1.69 100:69 | 2.56 25:39 | 5.7 10:47 | 40 1:39 | 530 1:529 +// 1.11 100:11 | 1.7 10:7 | 2.58 50:79 | 5.8 5:24 | 42 1:41 | 540 1:539 +// 1.12 25:3 | 1.71 100:71 | 2.6 5:8 | 5.9 10:49 | 44 1:43 | 550 1:549 +// 1.13 100:13 | 1.72 25:18 | 2.62 50:81 | 6 1:5 | 46 1:45 | 560 1:559 +// 1.14 50:7 | 1.73 100:73 | 2.64 25:41 | 6.2 5:26 | 48 1:47 | 570 1:569 +// 1.15 20:3 | 1.74 50:37 | 2.66 50:83 | 6.4 5:27 | 50 1:49 | 580 1:579 +// 1.16 25:4 | 1.75 4:3 | 2.68 25:42 | 6.6 5:28 | 55 1:54 | 590 1:589 +// 1.17 100:17 | 1.76 25:19 | 2.7 10:17 | 6.8 5:29 | 60 1:59 | 600 1:599 +// 1.18 50:9 | 1.77 100:77 | 2.72 25:43 | 7 1:6 | 65 1:64 | 610 1:609 +// 1.19 100:19 | 1.78 50:39 | 2.74 50:87 | 7.2 5:31 | 70 1:69 | 620 1:619 +// 1.2 5:1 | 1.79 100:79 | 2.76 25:44 | 7.4 5:32 | 75 1:74 | 630 1:629 +// 1.21 100:21 | 1.8 5:4 | 2.78 50:89 | 7.6 5:33 | 80 1:79 | 640 1:639 +// 1.22 50:11 | 1.81 100:81 | 2.8 5:9 | 7.8 5:34 | 85 1:84 | 650 1:649 +// 1.23 100:23 | 1.82 50:41 | 2.82 50:91 | 8 1:7 | 90 1:89 | 660 1:659 +// 1.24 25:6 | 1.83 100:83 | 2.84 25:46 | 8.2 5:36 | 95 1:94 | 670 1:669 +// 1.25 4:1 | 1.84 25:21 | 2.86 50:93 | 8.4 5:37 | 100 1:99 | 680 1:679 +// 1.26 50:13 | 1.85 20:17 | 2.88 25:47 | 8.6 5:38 | 110 1:109 | 690 1:689 +// 1.27 100:27 | 1.86 50:43 | 2.9 10:19 | 8.8 5:39 | 120 1:119 | 700 1:699 +// 1.28 25:7 | 1.87 100:87 | 2.92 25:48 | 9 1:8 | 130 1:129 | 710 1:709 +// 1.29 100:29 | 1.88 25:22 | 2.94 50:97 | 9.2 5:41 | 140 1:139 | 720 1:719 +// 1.3 10:3 | 1.89 100:89 | 2.96 25:49 | 9.4 5:42 | 150 1:149 | 730 1:729 +// 1.31 100:31 | 1.9 10:9 | 2.98 50:99 | 9.6 5:43 | 160 1:159 | 740 1:739 +// 1.32 25:8 | 1.91 100:91 | 3 1:2 | 9.8 5:44 | 170 1:169 | 750 1:749 +// 1.33 100:33 | 1.92 25:23 | 3.05 20:41 | 10 1:9 | 180 1:179 | 760 1:759 +// 1.34 50:17 | 1.93 100:93 | 3.1 10:21 | 10.5 2:19 | 190 1:189 | 770 1:769 +// 1.35 20:7 | 1.94 50:47 | 3.15 20:43 | 11 1:10 | 200 1:199 | 780 1:779 +// 1.36 25:9 | 1.95 20:19 | 3.2 5:11 | 11.5 2:21 | 210 1:209 | 790 1:789 +// 1.37 100:37 | 1.96 25:24 | 3.25 4:9 | 12 1:11 | 220 1:219 | 800 1:799 +// 1.38 50:19 | 1.97 100:97 | 3.3 10:23 | 12.5 2:23 | 230 1:229 | 810 1:809 +// 1.39 100:39 | 1.98 50:49 | 3.35 20:47 | 13 1:12 | 240 1:239 | 820 1:819 +// 1.4 5:2 | 1.99 100:99 | 3.4 5:12 | 13.5 2:25 | 250 1:249 | 830 1:829 +// 1.41 100:41 | 2 1:1 | 3.45 20:49 | 14 1:13 | 260 1:259 | 840 1:839 +// 1.42 50:21 | 2.02 50:51 | 3.5 2:5 | 14.5 2:27 | 270 1:269 | 850 1:849 +// 1.43 100:43 | 2.04 25:26 | 3.55 20:51 | 15 1:14 | 280 1:279 | 860 1:859 +// 1.44 25:11 | 2.06 50:53 | 3.6 5:13 | 15.5 2:29 | 290 1:289 | 870 1:869 +// 1.45 20:9 | 2.08 25:27 | 3.65 20:53 | 16 1:15 | 300 1:299 | 880 1:879 +// 1.46 50:23 | 2.1 10:11 | 3.7 10:27 | 16.5 2:31 | 310 1:309 | 890 1:889 +// 1.47 100:47 | 2.12 25:28 | 3.75 4:11 | 17 1:16 | 320 1:319 | 900 1:899 +// 1.48 25:12 | 2.14 50:57 | 3.8 5:14 | 17.5 2:33 | 330 1:329 | 910 1:909 +// 1.49 100:49 | 2.16 25:29 | 3.85 20:57 | 18 1:17 | 340 1:339 | 920 1:919 +// 1.5 2:1 | 2.18 50:59 | 3.9 10:29 | 18.5 2:35 | 350 1:349 | 930 1:929 +// 1.51 100:51 | 2.2 5:6 | 3.95 20:59 | 19 1:18 | 360 1:359 | 940 1:939 +// 1.52 25:13 | 2.22 50:61 | 4 1:3 | 19.5 2:37 | 370 1:369 | 950 1:949 +// 1.53 100:53 | 2.24 25:31 | 4.1 10:31 | 20 1:19 | 380 1:379 | 960 1:959 +// 1.54 50:27 | 2.26 50:63 | 4.2 5:16 | 21 1:20 | 390 1:389 | 970 1:969 +// 1.55 20:11 | 2.28 25:32 | 4.3 10:33 | 22 1:21 | 400 1:399 | 980 1:979 +// 1.56 25:14 | 2.3 10:13 | 4.4 5:17 | 23 1:22 | 410 1:409 | 990 1:989 +// 1.57 100:57 | 2.32 25:33 | 4.5 2:7 | 24 1:23 | 420 1:419 | 1000 1:999 +// 1.58 50:29 | 2.34 50:67 | 4.6 5:18 | 25 1:24 | 430 1:429 | +// 1.59 100:59 | 2.36 25:34 | 4.7 10:37 + +#define CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ + create_sport({{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ + generate_blocks(1); \ + const sport_object& ice_hockey = *db.get_index_type().indices().get().rbegin(); \ + create_event_group({{"en", "NHL"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_hockey.id); \ + generate_blocks(1); \ + const event_group_object& nhl = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}, {{"en", "2016-17"}}, nhl.id); \ + generate_blocks(1); \ + const event_object& capitals_vs_blackhawks = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_rules({{"en", "NHL Rules v1.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); \ + generate_blocks(1); \ + const betting_market_rules_object& betting_market_rules = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)capitals_win_market; (void)blackhawks_win_market; + +// create the basic betting market, plus groups for the first, second, and third period results +#define CREATE_EXTENDED_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ + CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ + create_betting_market_group({{"en", "First Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& first_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(first_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& first_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(first_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& first_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)first_period_capitals_win_market; (void)first_period_blackhawks_win_market; \ + \ + create_betting_market_group({{"en", "Second Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& second_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(second_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& second_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(second_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& second_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)second_period_capitals_win_market; (void)second_period_blackhawks_win_market; \ + \ + create_betting_market_group({{"en", "Third Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& third_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(third_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& third_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(third_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& third_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)third_period_capitals_win_market; (void)third_period_blackhawks_win_market; + +#define CREATE_TENNIS_BETTING_MARKET() \ + create_betting_market_rules({{"en", "Tennis Rules v1.0"}}, {{"en", "The winner is the player who wins the last ball in the match."}}); \ + generate_blocks(1); \ + const betting_market_rules_object& tennis_rules = *db.get_index_type().indices().get().rbegin(); \ + create_sport({{"en", "Tennis"}}); \ + generate_blocks(1); \ + const sport_object& tennis = *db.get_index_type().indices().get().rbegin(); \ + create_event_group({{"en", "Wimbledon"}}, tennis.id); \ + generate_blocks(1); \ + const event_group_object& wimbledon = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "R. Federer/T. Berdych"}}, {{"en", "2017"}}, wimbledon.id); \ + generate_blocks(1); \ + const event_object& berdych_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "M. Cilic/S. Querrye"}}, {{"en", "2017"}}, wimbledon.id); \ + generate_blocks(1); \ + const event_object& cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline 1st sf"}}, berdych_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_berdych_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline 2nd sf"}}, cilic_vs_querrey.id, tennis_rules.id, asset_id_type(), false, 0); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "T. Berdych defeats R. Federer"}}); \ + generate_blocks(1); \ + const betting_market_object& berdych_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "R. Federer defeats T. Berdych"}}); \ + generate_blocks(1); \ + const betting_market_object& federer_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "M. Cilic defeats S. Querrey"}}); \ + generate_blocks(1); \ + const betting_market_object& cilic_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "S. Querrey defeats M. Cilic"}});\ + generate_blocks(1); \ + const betting_market_object& querrey_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "R. Federer/M. Cilic"}}, {{"en", "2017"}}, wimbledon.id); \ + generate_blocks(1); \ + const event_object& cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline final"}}, cilic_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "R. Federer defeats M. Cilic"}}); \ + generate_blocks(1); \ + const betting_market_object& federer_wins_final_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "M. Cilic defeats R. Federer"}}); \ + generate_blocks(1); \ + const betting_market_object& cilic_wins_final_market = *db.get_index_type().indices().get().rbegin(); \ + (void)federer_wins_market;(void)cilic_wins_market;(void)federer_wins_final_market; (void)cilic_wins_final_market; (void)berdych_wins_market; (void)querrey_wins_market; + +BOOST_FIXTURE_TEST_SUITE( betting_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE(try_create_sport) +{ + try + { + sport_create_operation sport_create_op; + sport_create_op.name = {{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}; + + proposal_id_type proposal_id = propose_operation(sport_create_op); + + const auto& active_witnesses = db.get_global_properties().active_witnesses; + int voting_count = active_witnesses.size() / 2; + + // 5 for + std::vector witnesses; + for (const witness_id_type& witness_id : active_witnesses) + { + witnesses.push_back(witness_id); + if (--voting_count == 0) + break; + } + process_proposal_by_witnesses(witnesses, proposal_id); + + // 1st out + witnesses.clear(); + auto itr = active_witnesses.begin(); + witnesses.push_back(*itr); + process_proposal_by_witnesses(witnesses, proposal_id, true); + + const auto& sport_index = db.get_index_type().indices().get(); + // not yet approved + BOOST_REQUIRE(sport_index.rbegin() == sport_index.rend()); + + // 6th for + witnesses.clear(); + itr += 5; + witnesses.push_back(*itr); + process_proposal_by_witnesses(witnesses, proposal_id); + + // not yet approved + BOOST_REQUIRE(sport_index.rbegin() == sport_index.rend()); + + // 7th for + witnesses.clear(); + ++itr; + witnesses.push_back(*itr); + process_proposal_by_witnesses(witnesses, proposal_id); + + // done + BOOST_REQUIRE(sport_index.rbegin() != sport_index.rend()); + sport_id_type sport_id = (*sport_index.rbegin()).id; + BOOST_REQUIRE(sport_id == sport_id_type()); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(simple_bet_win) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // give alice and bob 10k each + transfer(account_id_type(), alice_id, asset(10000)); + transfer(account_id_type(), bob_id, asset(10000)); + + // place bets at 10:1 + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + + // reverse positions at 1:1 + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(binned_order_books) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + + // give alice and bob 10k each + transfer(account_id_type(), alice_id, asset(10000)); + transfer(account_id_type(), bob_id, asset(10000)); + + // place back bets at decimal odds of 1.55, 1.6, 1.65, 1.66, and 1.67 + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 155 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 165 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 166 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 167 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + + const auto& bet_odds_idx = db.get_index_type().indices().get(); + + auto bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + graphene::bookie::binned_order_book binned_orders_point_one = bookie_api.get_binned_order_book(capitals_win_market.id, 1); + idump((binned_orders_point_one)); + + // the binned orders returned should be chosen so that we if we assume those orders are real and we place + // matching lay orders, we will completely consume the underlying orders and leave no orders on the books + // + // for the bets bob placed above, we should get: 200 @ 1.6, 300 @ 1.7 + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 2u); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 0u); + for (const graphene::bookie::order_bin& binned_order : binned_orders_point_one.aggregated_back_bets) + { + // compute the matching lay order + share_type lay_amount = bet_object::get_approximate_matching_amount(binned_order.amount_to_bet, binned_order.backer_multiplier, bet_type::back, true /* round up */); + ilog("Alice is laying with ${lay_amount} at odds ${odds} to match the binned back amount ${back_amount}", ("lay_amount", lay_amount)("odds", binned_order.backer_multiplier)("back_amount", binned_order.amount_to_bet)); + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(lay_amount, asset_id_type()), binned_order.backer_multiplier); + } + + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + BOOST_CHECK(bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)) == bet_odds_idx.end()); + + // place lay bets at decimal odds of 1.55, 1.6, 1.65, 1.66, and 1.67 + // these bets will get rounded down, actual amounts are 99, 99, 91, 99, and 67 + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 155 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 165 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 166 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 167 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + + binned_orders_point_one = bookie_api.get_binned_order_book(capitals_win_market.id, 1); + idump((binned_orders_point_one)); + + // the binned orders returned should be chosen so that we if we assume those orders are real and we place + // matching lay orders, we will completely consume the underlying orders and leave no orders on the books + // + // for the bets bob placed above, we shoudl get 356 @ 1.6, 99 @ 1.5 + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 0u); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 2u); + for (const graphene::bookie::order_bin& binned_order : binned_orders_point_one.aggregated_lay_bets) + { + // compute the matching lay order + share_type back_amount = bet_object::get_approximate_matching_amount(binned_order.amount_to_bet, binned_order.backer_multiplier, bet_type::lay, true /* round up */); + ilog("Alice is backing with ${back_amount} at odds ${odds} to match the binned lay amount ${lay_amount}", ("back_amount", back_amount)("odds", binned_order.backer_multiplier)("lay_amount", binned_order.amount_to_bet)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(back_amount, asset_id_type()), binned_order.backer_multiplier); + + ilog("After alice's bet, order book is:"); + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + } + + + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + BOOST_CHECK(bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)) == bet_odds_idx.end()); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // give alice and bob 10M each + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + // have bob lay a bet for 1M (+20k fees) at 1:1 odds + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + // have alice back a matching bet at 1:1 odds (also costing 1.02M) + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + + generate_blocks(1); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( cancel_unmatched_in_betting_group_test ) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // give alice and bob 10M each + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + // have bob lay a bet for 1M (+20k fees) at 1:1 odds + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + // have alice back a matching bet at 1:1 odds (also costing 1.02M) + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + // place unmatched + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(500, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, blackhawks_win_market.id, bet_type::lay, asset(600, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 500); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 600); + + // cancel unmatched + cancel_unmatched_bets(moneyline_betting_markets.id); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's + // nothing for it to match, so it should be canceled + BOOST_TEST_MESSAGE("lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's nothing for it to match, so it should be canceled"); + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + BOOST_TEST_MESSAGE("alice's balance should be " << alice_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); +ilog("message"); + // lay 47 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here + BOOST_TEST_MESSAGE("alice lays 470 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here"); + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(47, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 47; + BOOST_TEST_MESSAGE("alice's balance should be " << alice_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 100 at 1.91 odds (100:91) -- this is an inexact match, we should get refunded 9 and leave a bet for 91 on the books + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 91; + BOOST_TEST_MESSAGE("alice's balance should be " << alice_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_TEST_MESSAGE("bob's balance should be " << bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // now have bob match it with a back of 300 at 1.5 + // This should: + // match the full 47 @ 1.94 with 50 + // match the full 91 @ 1.91 with 100 + // bob's balance goes down by 300 (150 is matched, 150 is still on the books) + // leaves a back bet of 150 @ 1.5 on the books + BOOST_TEST_MESSAGE("now have bob match it with a back of 300 at 1.5"); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(300, asset_id_type()), 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + bob_expected_balance -= 300; + BOOST_TEST_MESSAGE("bob's balance should be " << bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts2) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 470 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here + BOOST_TEST_MESSAGE("alice lays 470 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here"); + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(470, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 470; + BOOST_TEST_MESSAGE("alice's balance should be " << alice_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // now have bob match it with a back of 900 at 1.5 + // This should: + // match all 500 of bob's bet, and leave 400 @ 1.5 on the books + // bob's balance goes down by the 900 he paid (500 matched, 400 unmatched) + // alice's bet is completely removed from the books. + BOOST_TEST_MESSAGE("now have bob match it with a back of 900 at 1.5"); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(900, asset_id_type()), 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + bob_expected_balance -= 900; + + BOOST_TEST_MESSAGE("bob's balance should be " << bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts3) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 470 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(470, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 470; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // now have bob match it with a back of 1000 at 1.5 + // This should: + // match all of the 470 @ 1.94 with 500, and leave 500 left on the books + // bob's balance goes down by the 1000 he paid, 500 matching, 500 unmatching + // alice's bet is removed from the books + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + bob_expected_balance -= 1000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts4) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // back 1000 at 1.89 odds (100:89) -- this is an exact amount, nothing surprising should happen here + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 189 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 1000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // back 1000 at 1.97 odds (100:97) -- again, this is an exact amount, nothing surprising should happen here + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 197 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 1000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // now have bob match it with a lay of 3000 at 2.66 + // * This means bob expects to pay 3000 and match against 1807.2289. Or, + // put another way, bob wants to buy a payout of up to 1807.2289 if the + // capitals win, and he is willing to pay up to 3000 to do so. + // * The first thing that happens is bob's bet gets rounded down to something + // that can match exactly. 2.66 is 50:83 odds, so bob's bet is + // reduced to 2988, which should match against 1800. + // So bob gets an immediate refund of 12 + // * The next thing that happens is a match against the top bet on the order book. + // That's 1000 @ 1.89. We match at those odds (100:89), so bob will fully match + // this bet, paying 890 to get 1000 of his desired win position. + // * this top bet is removed from the order books + // * bob now has 1000 of the 1800 win position he wants. we adjust his bet + // so that what is left will only match 800. This means we will + // refund bob 770. His remaining bet is now lay 1328 @ 2.66 + // * Now we match the next bet on the order books, which is 1000 @ 1.97 (100:97). + // Bob only wants 800 of 1000 win position being offered, so he will not + // completely consume this bet. + // * Bob pays 776 to get his 800 win position. + // * alice's top bet on the books is reduced 200 @ 1.97 + // * Bob now has as much of a position as he was willing to buy. We refund + // the remainder of his bet, which is 552 + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(3000, asset_id_type()), 266 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + bob_expected_balance -= 3000 - 12 - 770 - 552; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts5) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // back 1100 at 1.86 odds (50:43) -- this is an exact amount, nothing surprising should happen here + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 186 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 1100; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // now have bob match it with a lay of 1100 at 1.98 + // * This means bob expects to pay 1100 and match against 1122.4, Or, + // put another way, bob wants to buy a payout of up to 1122.4 if the + // capitals win, and he is willing to pay up to 1100 to do so. + // * The first thing that happens is bob's bet gets rounded down to something + // that can match exactly. 1.98 (50:49) odds, so bob's bet is + // reduced to 1078, which should match against 1100. + // So bob gets an immediate refund of 22 + // * The next thing that happens is a match against the top bet on the order book. + // That's 1100 @ 1.86, At these odds, bob's 980 can buy all 1100 of bet, he + // pays 1100:946. + // + // * alice's bet is fully matched, it is removed from the books + // * bob's bet is fully matched, he is refunded the remaining 132 and his + // bet is complete + // * bob's balance is reduced by the 946 he paid + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1100, asset_id_type()), 198 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + bob_expected_balance -= 1100 - 22 - 132; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(match_using_takers_expected_amounts6) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + + transfer(account_id_type(), alice_id, asset(1000000000)); + share_type alice_expected_balance = 1000000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(10000000, asset_id_type()), 13 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(10000000, asset_id_type()), 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(10000000, asset_id_type()), 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + alice_expected_balance -= 30000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // check order books to see they match the bets we placed + const auto& bet_odds_idx = db.get_index_type().indices().get(); + auto bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + BOOST_REQUIRE(bet_iter != bet_odds_idx.end()); + BOOST_REQUIRE(bet_iter->betting_market_id == capitals_win_market.id); + BOOST_REQUIRE(bet_iter->bettor_id == alice_id); + BOOST_REQUIRE(bet_iter->amount_to_bet == asset(10000000, asset_id_type())); + BOOST_REQUIRE(bet_iter->backer_multiplier == 13 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + BOOST_REQUIRE(bet_iter->back_or_lay == bet_type::back); + ++bet_iter; + BOOST_REQUIRE(bet_iter != bet_odds_idx.end()); + BOOST_REQUIRE(bet_iter->betting_market_id == capitals_win_market.id); + BOOST_REQUIRE(bet_iter->bettor_id == alice_id); + BOOST_REQUIRE(bet_iter->amount_to_bet == asset(10000000, asset_id_type())); + BOOST_REQUIRE(bet_iter->backer_multiplier == 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + BOOST_REQUIRE(bet_iter->back_or_lay == bet_type::back); + ++bet_iter; + BOOST_REQUIRE(bet_iter != bet_odds_idx.end()); + BOOST_REQUIRE(bet_iter->betting_market_id == capitals_win_market.id); + BOOST_REQUIRE(bet_iter->bettor_id == alice_id); + BOOST_REQUIRE(bet_iter->amount_to_bet == asset(10000000, asset_id_type())); + BOOST_REQUIRE(bet_iter->backer_multiplier == 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + BOOST_REQUIRE(bet_iter->back_or_lay == bet_type::back); + ++bet_iter; + BOOST_REQUIRE(bet_iter == bet_odds_idx.end() || bet_iter->betting_market_id != capitals_win_market.id); + + // check the binned order books from the bookie plugin to make sure they match + graphene::bookie::binned_order_book binned_orders_point_one = bookie_api.get_binned_order_book(capitals_win_market.id, 1); + auto aggregated_back_bets_iter = binned_orders_point_one.aggregated_back_bets.begin(); + BOOST_REQUIRE(aggregated_back_bets_iter != binned_orders_point_one.aggregated_back_bets.end()); + BOOST_REQUIRE(aggregated_back_bets_iter->amount_to_bet.value == 10000000); + BOOST_REQUIRE(aggregated_back_bets_iter->backer_multiplier == 13 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + ++aggregated_back_bets_iter; + BOOST_REQUIRE(aggregated_back_bets_iter != binned_orders_point_one.aggregated_back_bets.end()); + BOOST_REQUIRE(aggregated_back_bets_iter->amount_to_bet.value == 10000000); + BOOST_REQUIRE(aggregated_back_bets_iter->backer_multiplier == 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + ++aggregated_back_bets_iter; + BOOST_REQUIRE(aggregated_back_bets_iter != binned_orders_point_one.aggregated_back_bets.end()); + BOOST_REQUIRE(aggregated_back_bets_iter->amount_to_bet.value == 10000000); + BOOST_REQUIRE(aggregated_back_bets_iter->backer_multiplier == 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + ++aggregated_back_bets_iter; + BOOST_REQUIRE(aggregated_back_bets_iter == binned_orders_point_one.aggregated_back_bets.end()); + BOOST_REQUIRE(binned_orders_point_one.aggregated_lay_bets.empty()); + + transfer(account_id_type(), bob_id, asset(1000000000)); + share_type bob_expected_balance = 1000000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(5000000, asset_id_type()), 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + ilog("Order books after bob's matching lay bet:"); + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + while (bet_iter != bet_odds_idx.end() && + bet_iter->betting_market_id == capitals_win_market.id) + { + idump((*bet_iter)); + ++bet_iter; + } + + bob_expected_balance -= 5000000 - 2000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // check order books to see they match after bob's bet matched + bet_iter = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + BOOST_REQUIRE(bet_iter != bet_odds_idx.end()); + BOOST_REQUIRE(bet_iter->betting_market_id == capitals_win_market.id); + BOOST_REQUIRE(bet_iter->bettor_id == alice_id); + BOOST_REQUIRE(bet_iter->amount_to_bet == asset(10000000, asset_id_type())); + BOOST_REQUIRE(bet_iter->backer_multiplier == 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + BOOST_REQUIRE(bet_iter->back_or_lay == bet_type::back); + ++bet_iter; + BOOST_REQUIRE(bet_iter != bet_odds_idx.end()); + BOOST_REQUIRE(bet_iter->betting_market_id == capitals_win_market.id); + BOOST_REQUIRE(bet_iter->bettor_id == alice_id); + BOOST_REQUIRE(bet_iter->amount_to_bet == asset(10000000, asset_id_type())); + BOOST_REQUIRE(bet_iter->backer_multiplier == 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + BOOST_REQUIRE(bet_iter->back_or_lay == bet_type::back); + ++bet_iter; + BOOST_REQUIRE(bet_iter == bet_odds_idx.end() || bet_iter->betting_market_id != capitals_win_market.id); + + // check the binned order books from the bookie plugin to make sure they match + binned_orders_point_one = bookie_api.get_binned_order_book(capitals_win_market.id, 1); + aggregated_back_bets_iter = binned_orders_point_one.aggregated_back_bets.begin(); + BOOST_REQUIRE(aggregated_back_bets_iter != binned_orders_point_one.aggregated_back_bets.end()); + BOOST_REQUIRE(aggregated_back_bets_iter->amount_to_bet.value == 10000000); + BOOST_REQUIRE(aggregated_back_bets_iter->backer_multiplier == 15 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + ++aggregated_back_bets_iter; + BOOST_REQUIRE(aggregated_back_bets_iter != binned_orders_point_one.aggregated_back_bets.end()); + BOOST_REQUIRE(aggregated_back_bets_iter->amount_to_bet.value == 10000000); + BOOST_REQUIRE(aggregated_back_bets_iter->backer_multiplier == 16 * GRAPHENE_BETTING_ODDS_PRECISION / 10); + ++aggregated_back_bets_iter; + BOOST_REQUIRE(aggregated_back_bets_iter == binned_orders_point_one.aggregated_back_bets.end()); + BOOST_REQUIRE(binned_orders_point_one.aggregated_lay_bets.empty()); + + + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(inexact_odds) +{ + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's + // nothing for it to match, so it should be canceled + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 47 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(47, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 47; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // lay 100 at 1.91 odds (100:91) -- this is an inexact match, we should get refunded 9 and leave a bet for 91 on the books + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 91; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + + // now have bob match it with a back of 300 at 1.91 + // This should: + // match the full 47 @ 1.94 with 50 + // match the full 91 @ 1.91 with 100 + // leaving 150 + // back bets at 100:91 must be a multiple of 100, so refund 50 + // leaves a back bet of 100 @ 1.91 on the books + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(300, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + bob_expected_balance -= 250; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(bet_reversal_test) +{ + // test whether we can bet our entire balance in one direction, then reverse our bet (while having zero balance) + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + // back with our entire balance + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(10000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), 0); + + // reverse the bet + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(20000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), 0); + + // try to re-reverse it, but go too far + BOOST_CHECK_THROW( place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(30000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION), fc::exception); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), 0); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(bet_against_exposure_test) +{ + // test whether we can bet our entire balance in one direction, have it match, then reverse our bet (while having zero balance) + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + int64_t alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance); + int64_t bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance); + + // back with alice's entire balance + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(10000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + alice_expected_balance -= 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance); + + // lay with bob's entire balance, which fully matches bob's bet + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(10000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + bob_expected_balance -= 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance); + + // reverse the bet + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(20000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance); + + // try to re-reverse it, but go too far + BOOST_CHECK_THROW( place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(30000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION), fc::exception); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(persistent_objects_test) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + idump((capitals_win_market.get_status())); + + // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's + // nothing for it to match, so it should be canceled + bet_id_type automatically_canceled_bet_id = place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + generate_blocks(1); + BOOST_CHECK_MESSAGE(!db.find(automatically_canceled_bet_id), "Bet should have been canceled, but the blockchain still knows about it"); + fc::variants objects_from_bookie = bookie_api.get_objects({automatically_canceled_bet_id}); + idump((objects_from_bookie)); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 1u); + BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == automatically_canceled_bet_id, "Bookie Plugin didn't return a deleted bet it"); + + // lay 47 at 1.94 odds (50:47) -- this bet should go on the order books normally + bet_id_type first_bet_on_books = place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(47, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + generate_blocks(1); + BOOST_CHECK_MESSAGE(db.find(first_bet_on_books), "Bet should exist on the blockchain"); + objects_from_bookie = bookie_api.get_objects({first_bet_on_books}); + idump((objects_from_bookie)); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 1u); + BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == first_bet_on_books, "Bookie Plugin didn't return a bet that is currently on the books"); + + // place a bet that exactly matches 'first_bet_on_books', should result in empty books (thus, no bet_objects from the blockchain) + bet_id_type matching_bet = place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(50, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + BOOST_CHECK_MESSAGE(!db.find(first_bet_on_books), "Bet should have been filled, but the blockchain still knows about it"); + BOOST_CHECK_MESSAGE(!db.find(matching_bet), "Bet should have been filled, but the blockchain still knows about it"); + generate_blocks(1); // the bookie plugin doesn't detect matches until a block is generated + + objects_from_bookie = bookie_api.get_objects({first_bet_on_books, matching_bet}); + idump((objects_from_bookie)); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 2u); + BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == first_bet_on_books, "Bookie Plugin didn't return a bet that has been filled"); + BOOST_CHECK_MESSAGE(objects_from_bookie[1]["id"].as() == matching_bet, "Bookie Plugin didn't return a bet that has been filled"); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::cancel}, + {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); + + // as soon as the market is resolved during the generate_block(), these markets + // should be deleted and our references will go out of scope. Save the + // market ids here so we can verify that they were really deleted + betting_market_id_type capitals_win_market_id = capitals_win_market.id; + betting_market_id_type blackhawks_win_market_id = blackhawks_win_market.id; + + generate_blocks(1); + + // test get_matched_bets_for_bettor + std::vector alice_matched_bets = bookie_api.get_matched_bets_for_bettor(alice_id); + for (const graphene::bookie::matched_bet_object& matched_bet : alice_matched_bets) + { + idump((matched_bet)); + for (operation_history_id_type id : matched_bet.associated_operations) + idump((id(db))); + } + BOOST_REQUIRE_EQUAL(alice_matched_bets.size(), 1u); + BOOST_CHECK(alice_matched_bets[0].amount_matched == 47); + std::vector bob_matched_bets = bookie_api.get_matched_bets_for_bettor(bob_id); + for (const graphene::bookie::matched_bet_object& matched_bet : bob_matched_bets) + { + idump((matched_bet)); + for (operation_history_id_type id : matched_bet.associated_operations) + idump((id(db))); + } + BOOST_REQUIRE_EQUAL(bob_matched_bets.size(), 1u); + BOOST_CHECK(bob_matched_bets[0].amount_matched == 50); + + // test getting markets + // test that we cannot get them from the database directly + BOOST_CHECK_THROW(capitals_win_market_id(db), fc::exception); + BOOST_CHECK_THROW(blackhawks_win_market_id(db), fc::exception); + + objects_from_bookie = bookie_api.get_objects({capitals_win_market_id, blackhawks_win_market_id}); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 2u); + idump((objects_from_bookie)); + BOOST_CHECK(!objects_from_bookie[0].is_null()); + BOOST_CHECK(!objects_from_bookie[1].is_null()); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(test_settled_market_states) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + idump((capitals_win_market.get_status())); + + BOOST_TEST_MESSAGE("setting the event to in_progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + + // as soon as the market is resolved during the generate_block(), these markets + // should be deleted and our references will go out of scope. Save the + // market ids here so we can verify that they were really deleted + betting_market_id_type capitals_win_market_id = capitals_win_market.id; + betting_market_id_type blackhawks_win_market_id = blackhawks_win_market.id; + + generate_blocks(1); + + // test getting markets + // test that we cannot get them from the database directly + BOOST_CHECK_THROW(capitals_win_market_id(db), fc::exception); + BOOST_CHECK_THROW(blackhawks_win_market_id(db), fc::exception); + + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_win_market_id, blackhawks_win_market_id}); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 2u); + idump((objects_from_bookie)); + BOOST_CHECK(!objects_from_bookie[0].is_null()); + BOOST_CHECK(!objects_from_bookie[1].is_null()); + } + FC_LOG_AND_RETHROW() +} + + +BOOST_AUTO_TEST_CASE(delayed_bets_test) // test live betting +{ + try + { + const auto& bet_odds_idx = db.get_index_type().indices().get(); + + ACTORS( (alice)(bob) ); + + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + generate_blocks(1); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::in_play); + generate_blocks(1); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + generate_blocks(1); + + BOOST_TEST_MESSAGE("Testing basic delayed bet mechanics"); + // alice backs 100 at odds 2 + BOOST_TEST_MESSAGE("Alice places a back bet of 100 at odds 2.0"); + bet_id_type delayed_back_bet = place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + generate_blocks(1); + + // verify the bet hasn't been placed in the active book + auto first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + BOOST_CHECK(first_bet_in_market == bet_odds_idx.end()); + + // after 3 blocks, the delay should have expired and it will be promoted to the active book + generate_blocks(2); + first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + auto last_bet_in_market = bet_odds_idx.upper_bound(std::make_tuple(capitals_win_market.id)); + BOOST_CHECK(first_bet_in_market != bet_odds_idx.end()); + BOOST_CHECK(std::distance(first_bet_in_market, last_bet_in_market) == 1); + + for (const auto& bet : boost::make_iterator_range(first_bet_in_market, last_bet_in_market)) + edump((bet)); + // bob lays 100 at odds 2 to match alice's bet currently on the books + BOOST_TEST_MESSAGE("Bob places a lay bet of 100 at odds 2.0"); + /* bet_id_type delayed_lay_bet = */ place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + edump((db.get_global_properties().parameters.block_interval)(db.head_block_time())); + // the bet should not enter the order books before a block has been generated + first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + last_bet_in_market = bet_odds_idx.upper_bound(std::make_tuple(capitals_win_market.id)); + for (const auto& bet : bet_odds_idx) + edump((bet)); + generate_blocks(1); + + // bob's bet will still be delayed, so the active order book will only contain alice's bet + first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + last_bet_in_market = bet_odds_idx.upper_bound(std::make_tuple(capitals_win_market.id)); + edump((std::distance(first_bet_in_market, last_bet_in_market))); + BOOST_CHECK(std::distance(first_bet_in_market, last_bet_in_market) == 1); + for (const auto& bet : boost::make_iterator_range(first_bet_in_market, last_bet_in_market)) + edump((bet)); + + // once bob's bet's delay has expired, the two bets will annihilate each other, leaving + // an empty order book + generate_blocks(2); + BOOST_CHECK(bet_odds_idx.empty()); + + // now test that when we cancel all bets on a market, delayed bets get canceled too + BOOST_TEST_MESSAGE("Alice places a back bet of 100 at odds 2.0"); + delayed_back_bet = place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + generate_blocks(1); + first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + BOOST_CHECK(!bet_odds_idx.empty()); + BOOST_CHECK(first_bet_in_market == bet_odds_idx.end()); + BOOST_TEST_MESSAGE("Cancel all bets"); + cancel_unmatched_bets(moneyline_betting_markets.id); + BOOST_CHECK(bet_odds_idx.empty()); + } + FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( chained_market_create_test ) +{ + // Often you will want to create several objects that reference each other at the same time. + // To facilitate this, many of the betting market operations allow you to use "relative" object ids, + // which let you can create, for example, an event in the 2nd operation in a transaction where the + // event group id is set to the id of an event group created in the 1st operation in a tranasction. + try + { + { + const flat_set& active_witnesses = db.get_global_properties().active_witnesses; + + BOOST_TEST_MESSAGE("Creating a sport and competitors in the same proposal"); + { + // operation 0 in the transaction + sport_create_operation sport_create_op; + sport_create_op.name.insert(internationalized_string_type::value_type("en", "Ice Hockey")); + sport_create_op.name.insert(internationalized_string_type::value_type("zh_Hans", "冰球")); + sport_create_op.name.insert(internationalized_string_type::value_type("ja", "アイスホッケー")); + + // operation 1 + event_group_create_operation event_group_create_op; + event_group_create_op.name.insert(internationalized_string_type::value_type("en", "NHL")); + event_group_create_op.name.insert(internationalized_string_type::value_type("zh_Hans", "國家冰球聯盟")); + event_group_create_op.name.insert(internationalized_string_type::value_type("ja", "ナショナルホッケーリーグ")); + event_group_create_op.sport_id = object_id_type(relative_protocol_ids, 0, 0); + + // operation 2 + // leave name and start time blank + event_create_operation event_create_op; + event_create_op.name = {{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}; + event_create_op.season.insert(internationalized_string_type::value_type("en", "2016-17")); + event_create_op.event_group_id = object_id_type(relative_protocol_ids, 0, 1); + + // operation 3 + betting_market_rules_create_operation rules_create_op; + rules_create_op.name = {{"en", "NHL Rules v1.0"}}; + rules_create_op.description = {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}; + + // operation 4 + betting_market_group_create_operation betting_market_group_create_op; + betting_market_group_create_op.description = {{"en", "Moneyline"}}; + betting_market_group_create_op.event_id = object_id_type(relative_protocol_ids, 0, 2); + betting_market_group_create_op.rules_id = object_id_type(relative_protocol_ids, 0, 3); + betting_market_group_create_op.asset_id = asset_id_type(); + + // operation 5 + betting_market_create_operation caps_win_betting_market_create_op; + caps_win_betting_market_create_op.group_id = object_id_type(relative_protocol_ids, 0, 4); + caps_win_betting_market_create_op.payout_condition.insert(internationalized_string_type::value_type("en", "Washington Capitals win")); + + // operation 6 + betting_market_create_operation blackhawks_win_betting_market_create_op; + blackhawks_win_betting_market_create_op.group_id = object_id_type(relative_protocol_ids, 0, 4); + blackhawks_win_betting_market_create_op.payout_condition.insert(internationalized_string_type::value_type("en", "Chicago Blackhawks win")); + + + proposal_create_operation proposal_op; + proposal_op.fee_paying_account = (*active_witnesses.begin())(db).witness_account; + proposal_op.proposed_ops.emplace_back(sport_create_op); + proposal_op.proposed_ops.emplace_back(event_group_create_op); + proposal_op.proposed_ops.emplace_back(event_create_op); + proposal_op.proposed_ops.emplace_back(rules_create_op); + proposal_op.proposed_ops.emplace_back(betting_market_group_create_op); + proposal_op.proposed_ops.emplace_back(caps_win_betting_market_create_op); + proposal_op.proposed_ops.emplace_back(blackhawks_win_betting_market_create_op); + proposal_op.expiration_time = db.head_block_time() + fc::days(1); + + signed_transaction tx; + tx.operations.push_back(proposal_op); + set_expiration(db, tx); + sign(tx, init_account_priv_key); + + db.push_transaction(tx); + } + + BOOST_REQUIRE_EQUAL(db.get_index_type().indices().size(), 1u); + { + const proposal_object& prop = *db.get_index_type().indices().begin(); + + for (const witness_id_type& witness_id : active_witnesses) + { + BOOST_TEST_MESSAGE("Approving sport+competitors creation from witness " << fc::variant(witness_id).as()); + const witness_object& witness = witness_id(db); + const account_object& witness_account = witness.witness_account(db); + + proposal_update_operation pup; + pup.proposal = prop.id; + pup.fee_paying_account = witness_account.id; + //pup.key_approvals_to_add.insert(witness.signing_key); + pup.active_approvals_to_add.insert(witness_account.id); + + signed_transaction tx; + tx.operations.push_back( pup ); + set_expiration( db, tx ); + sign(tx, init_account_priv_key); + + db.push_transaction(tx, ~0); + if (db.get_index_type().indices().size() == 1) + { + //BOOST_TEST_MESSAGE("The sport creation operation has been approved, new sport object on the blockchain is " << fc::json::to_pretty_string(*db.get_index_type().indices().rbegin())); + break; + } + } + } + + } + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( testnet_witness_block_production_error ) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + create_betting_market_group({{"en", "Unused"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), false, 0); + generate_blocks(1); + const betting_market_group_object& unused_betting_markets = *db.get_index_type().indices().get().rbegin(); + + BOOST_TEST_MESSAGE("setting the event in progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(unused_betting_markets.get_status() == betting_market_group_status::in_play); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(unused_betting_markets.get_status() == betting_market_group_status::closed); + + BOOST_TEST_MESSAGE("setting the event to canceled"); + update_event(capitals_vs_blackhawks.id, _status = event_status::canceled); + generate_blocks(1); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( cancel_one_event_in_group ) +{ + // test that creates an event group with two events in it. We walk one event through the + // usual sequence and cancel it, verify that it doesn't alter the other event in the group + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // create a second event in the same betting market group + create_event({{"en", "Boston Bruins/Pittsburgh Penguins"}}, {{"en", "2016-17"}}, nhl.id); + generate_blocks(1); + const event_object& bruins_vs_penguins = *db.get_index_type().indices().get().rbegin(); + create_betting_market_group({{"en", "Moneyline"}}, bruins_vs_penguins.id, betting_market_rules.id, asset_id_type(), false, 0); + generate_blocks(1); + const betting_market_group_object& bruins_penguins_moneyline_betting_markets = *db.get_index_type().indices().get().rbegin(); + create_betting_market(bruins_penguins_moneyline_betting_markets.id, {{"en", "Boston Bruins win"}}); + generate_blocks(1); + const betting_market_object& bruins_win_market = *db.get_index_type().indices().get().rbegin(); + create_betting_market(bruins_penguins_moneyline_betting_markets.id, {{"en", "Pittsburgh Penguins win"}}); + generate_blocks(1); + const betting_market_object& penguins_win_market = *db.get_index_type().indices().get().rbegin(); + (void)bruins_win_market; (void)penguins_win_market; + + // check the initial state + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(bruins_vs_penguins.get_status() == event_status::upcoming); + + BOOST_TEST_MESSAGE("setting the capitals_vs_blackhawks event to in-progress, leaving bruins_vs_penguins in upcoming"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + + BOOST_CHECK(bruins_vs_penguins.get_status() == event_status::upcoming); + BOOST_CHECK(bruins_penguins_moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(bruins_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(penguins_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the capitals_vs_blackhawks event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(bruins_vs_penguins.get_status() == event_status::upcoming); + BOOST_CHECK(bruins_penguins_moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(bruins_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(penguins_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the capitals_vs_blackhawks event to canceled"); + update_event(capitals_vs_blackhawks.id, _status = event_status::canceled); + generate_blocks(1); + BOOST_CHECK(bruins_vs_penguins.get_status() == event_status::upcoming); + BOOST_CHECK(bruins_penguins_moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(bruins_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(penguins_win_market.get_status() == betting_market_status::unresolved); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + +// set up a fixture that places a series of two matched bets, we'll use this fixture to verify +// the result in all three possible outcomes +struct simple_bet_test_fixture : database_fixture { + betting_market_id_type capitals_win_betting_market_id; + betting_market_id_type blackhawks_win_betting_market_id; + betting_market_group_id_type moneyline_betting_markets_id; + + simple_bet_test_fixture() + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // give alice and bob 10k each + transfer(account_id_type(), alice_id, asset(10000)); + transfer(account_id_type(), bob_id, asset(10000)); + + // place bets at 10:1 + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + + // reverse positions at 1:1 + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + capitals_win_betting_market_id = capitals_win_market.id; + blackhawks_win_betting_market_id = blackhawks_win_market.id; + moneyline_betting_markets_id = moneyline_betting_markets.id; + + // close betting to prepare for the next operation which will be grading or cancel + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + generate_blocks(1); + } +}; + +BOOST_FIXTURE_TEST_SUITE( simple_bet_tests, simple_bet_test_fixture ) + +BOOST_AUTO_TEST_CASE( win ) +{ + try + { + resolve_betting_market_group(moneyline_betting_markets_id, + {{capitals_win_betting_market_id, betting_market_resolution_type::win}, + {blackhawks_win_betting_market_id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + GET_ACTOR(alice); + GET_ACTOR(bob); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value; + //rake_value = (-100 + 1100 - 1100) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + // alice starts with 10000, pays 100 (bet), wins 1100, then pays 1100 (bet), wins 0 + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000 - 100 + 1100 - 1100 + 0); + + rake_value = (-1000 - 1100 + 2200) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + edump((rake_fee_percentage)(rake_value)); + // bob starts with 10000, pays 1000 (bet), wins 0, then pays 1100 (bet), wins 2200 + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000 - 1000 + 0 - 1100 + 2200 - rake_value); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( not_win ) +{ + try + { + resolve_betting_market_group(moneyline_betting_markets_id, + {{capitals_win_betting_market_id, betting_market_resolution_type::not_win}, + {blackhawks_win_betting_market_id, betting_market_resolution_type::win}}); + generate_blocks(1); + + GET_ACTOR(alice); + GET_ACTOR(bob); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-100 - 1100 + 2200) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + // alice starts with 10000, pays 100 (bet), wins 0, then pays 1100 (bet), wins 2200 + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000 - 100 + 0 - 1100 + 2200 - rake_value); + + //rake_value = (-1000 + 1100 - 1100) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + // bob starts with 10000, pays 1000 (bet), wins 1100, then pays 1100 (bet), wins 0 + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000 - 1000 + 1100 - 1100 + 0); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( cancel ) +{ + try + { + resolve_betting_market_group(moneyline_betting_markets_id, + {{capitals_win_betting_market_id, betting_market_resolution_type::cancel}, + {blackhawks_win_betting_market_id, betting_market_resolution_type::cancel}}); + generate_blocks(1); + + GET_ACTOR(alice); + GET_ACTOR(bob); + + // alice and bob both start with 10000, they should end with 10000 + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + +struct simple_bet_test_fixture_2 : database_fixture { + betting_market_id_type capitals_win_betting_market_id; + simple_bet_test_fixture_2() + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // give alice and bob 10k each + transfer(account_id_type(), alice_id, asset(10000)); + transfer(account_id_type(), bob_id, asset(10000)); + + // alice backs 1000 at 1:1, matches + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + // now alice lays at 2500 at 1:1. This should require a deposit of 500, with the remaining 200 being funded from exposure + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(2500, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + // match the bet bit by bit. bob matches 500 of alice's 2500 bet. This effectively cancels half of bob's lay position + // so he immediately gets 500 back. It reduces alice's back position, but doesn't return any money to her (all 2000 of her exposure + // was already "promised" to her lay bet, so the 500 she would have received is placed in her refundable_unmatched_bets) + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(500, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + // match another 500, which will fully cancel bob's lay position and return the other 500 he had locked up in his position. + // alice's back position is now canceled, 1500 remains of her unmatched lay bet, and the 500 from canceling her position has + // been moved to her refundable_unmatched_bets + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(500, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + capitals_win_betting_market_id = capitals_win_market.id; + } +}; + +BOOST_FIXTURE_TEST_SUITE( update_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE(sport_update_test) +{ + try + { + ACTORS( (alice) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + update_sport(ice_hockey.id, {{"en", "Hockey on Ice"}, {"zh_Hans", "冰"}, {"ja", "アイスホッケ"}}); + + transfer(account_id_type(), alice_id, asset(10000000)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(event_group_update_test) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ + fc::optional sport_id = ice_on_hockey.id; + + fc::optional name = internationalized_string_type({{"en", "IBM"}, {"zh_Hans", "國家冰球聯"}, {"ja", "ナショナルホッケーリー"}}); + + update_event_group(nhl.id, fc::optional(), name); + update_event_group(nhl.id, sport_id, fc::optional()); + update_event_group(nhl.id, sport_id, name); + + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() +} + + +BOOST_AUTO_TEST_CASE(event_update_test) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + fc::optional name = internationalized_string_type({{"en", "Washington Capitals vs. Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホーク"}}); + fc::optional season = internationalized_string_type({{"en", "2017-18"}}); + + update_event(capitals_vs_blackhawks.id, _name = name); + update_event(capitals_vs_blackhawks.id, _season = season); + update_event(capitals_vs_blackhawks.id, _name = name, _season = season); + + const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); + const event_group_object& nhl2 = create_event_group({{"en", "NHL2"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_on_hockey.id); + + update_event(capitals_vs_blackhawks.id, _event_group_id = nhl2.id); + + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(betting_market_rules_update_test) +{ + try + { + ACTORS( (alice) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + fc::optional empty; + fc::optional name = internationalized_string_type({{"en", "NHL Rules v1.1"}}); + fc::optional desc = internationalized_string_type({{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); + + update_betting_market_rules(betting_market_rules.id, name, empty); + update_betting_market_rules(betting_market_rules.id, empty, desc); + update_betting_market_rules(betting_market_rules.id, name, desc); + + transfer(account_id_type(), alice_id, asset(10000000)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(betting_market_group_update_test) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + internationalized_string_type new_description = internationalized_string_type({{"en", "Money line"}}); + + const betting_market_rules_object& new_betting_market_rules = create_betting_market_rules({{"en", "NHL Rules v2.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); + fc::optional new_rule = new_betting_market_rules.id; + + update_betting_market_group(moneyline_betting_markets.id, _description = new_description); + update_betting_market_group(moneyline_betting_markets.id, _rules_id = new_betting_market_rules.id); + update_betting_market_group(moneyline_betting_markets.id, _description = new_description, _rules_id = new_betting_market_rules.id); + + transfer(account_id_type(), bob_id, asset(10000000)); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(betting_market_update_test) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + fc::optional payout_condition = internationalized_string_type({{"en", "Washington Capitals lose"}}); + + // update the payout condition + update_betting_market(capitals_win_market.id, fc::optional(), payout_condition); + + transfer(account_id_type(), bob_id, asset(10000000)); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE( event_status_tests, database_fixture ) + +// This tests a normal progression by setting the event state and +// letting it trickle down: +// - upcoming +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_1) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the event state and +// letting it trickle down. Like the above, with delayed settling: +// - upcoming +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_1_with_delay) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 60 /* seconds */); + graphene::bookie::bookie_api bookie_api(app); + + // save the ids for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + betting_market_group_id_type moneyline_betting_markets_id = moneyline_betting_markets.id; + betting_market_id_type capitals_win_market_id = capitals_win_market.id; + betting_market_id_type blackhawks_win_market_id = blackhawks_win_market.id; + + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // it should be waiting 60 seconds before it settles + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::graded); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::graded); + BOOST_CHECK(capitals_win_market.resolution == betting_market_resolution_type::win); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::graded); + BOOST_CHECK(blackhawks_win_market.resolution == betting_market_resolution_type::not_win); + + generate_blocks(60); + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id, + moneyline_betting_markets_id, + capitals_win_market_id, + blackhawks_win_market_id}); + + idump((objects_from_bookie)); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + BOOST_CHECK_EQUAL(objects_from_bookie[1]["status"].as(), "settled"); + BOOST_CHECK_EQUAL(objects_from_bookie[2]["status"].as(), "settled"); + BOOST_CHECK_EQUAL(objects_from_bookie[2]["resolution"].as(), "win"); + BOOST_CHECK_EQUAL(objects_from_bookie[3]["status"].as(), "settled"); + BOOST_CHECK_EQUAL(objects_from_bookie[3]["resolution"].as(), "not_win"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the event state and +// letting it trickle down: +// - upcoming +// - frozen +// - upcoming +// - in_progress +// - frozen +// - in_progress +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_2) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back to upcoming"); + update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the event state and +// letting it trickle down. This is the same as the above test, but the +// never-in-play flag is set: +// - upcoming +// - frozen +// - upcoming +// - in_progress +// - frozen +// - in_progress +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_2_never_in_play) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(true, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back to upcoming"); + update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a slightly different normal progression by setting the event state and +// letting it trickle down: +// - upcoming +// - frozen +// - in_progress +// - frozen +// - in_progress +// - finished +// - canceled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_3) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event in progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to canceled"); + update_event(capitals_vs_blackhawks.id, _status = event_status::canceled); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will cancel, and the market + // and group will cease to exist. The event should transition to "canceled", then be removed + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "canceled"); + + } FC_LOG_AND_RETHROW() +} + +// This tests that we reject illegal transitions +// - anything -> settled +// - in_progress -> upcoming +// - frozen after in_progress -> upcoming +// - finished -> upcoming, in_progress, frozen +// - canceled -> anything +BOOST_AUTO_TEST_CASE(event_driven_progression_errors_1) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + // settled is the only illegal transition from upcoming + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::frozen); + + // settled is the only illegal transition from this frozen event + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event in progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + + // we can't go back to upcoming from in_progress. + // settled is disallowed everywhere + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::frozen); + + // we can't go back to upcoming from frozen once we've gone in_progress. + // settled is disallowed everywhere + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + // we can't go back to upcoming, in_progress, or frozen once we're finished. + // settled is disallowed everywhere + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to in_progress"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to frozen"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::frozen, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event to canceled"); + update_event(capitals_vs_blackhawks.id, _status = event_status::canceled); + generate_blocks(1); + + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "canceled"); + + // we can't go back to upcoming, in_progress, frozen, or finished once we're canceled. + // (this won't work because the event has been deleted) + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to in_progress"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::in_progress, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to frozen"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::frozen, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to finished"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::finished, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::settled, _force = true), fc::exception); + } FC_LOG_AND_RETHROW() +} + +// This tests that we can't transition out of settled (all other transitions tested in the previous test) +// - settled -> anything +BOOST_AUTO_TEST_CASE(event_driven_progression_errors_2) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then removed + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + + // we can't go back to upcoming, in_progress, frozen, or finished once we're canceled. + // (this won't work because the event has been deleted) + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to in_progress"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::in_progress, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to frozen"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::frozen, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to finished"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::finished, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to canceled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::canceled, _force = true), fc::exception); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the betting_market_group state and +// letting it trickle up to the event: +BOOST_AUTO_TEST_CASE(betting_market_group_driven_standard_progression) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting betting market to frozen"); + // the event should stay in the upcoming state + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event frozen"); + // this should only change the status of the event, just verify that nothing weird happens when + // we try to set the bmg to frozen when it's already frozen + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting betting market to closed"); + // the event should go to finished automatically + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled" + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression in an event with multiple betting market groups +// letting info it trickle up from the group to the event: +BOOST_AUTO_TEST_CASE(multi_betting_market_group_driven_standard_progression) +{ + try + { + CREATE_EXTENDED_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_result_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(first_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_result_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(second_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_result_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(third_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("the game is starting, setting the main betting market and the first period to in_play"); + // the event should stay in the upcoming state + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::in_play); + update_betting_market_group(first_period_result_betting_markets.id, _status = betting_market_group_status::in_play); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_result_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(first_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + + BOOST_TEST_MESSAGE("the first period is over, starting the second period"); + update_betting_market_group(first_period_result_betting_markets.id, _status = betting_market_group_status::closed); + update_betting_market_group(second_period_result_betting_markets.id, _status = betting_market_group_status::in_play); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_result_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(first_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_result_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(second_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading the first period market"); + resolve_betting_market_group(first_period_result_betting_markets.id, + {{first_period_capitals_win_market.id, betting_market_resolution_type::win}, + {first_period_blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + BOOST_TEST_MESSAGE("the second period is over, starting the third period"); + update_betting_market_group(second_period_result_betting_markets.id, _status = betting_market_group_status::closed); + update_betting_market_group(third_period_result_betting_markets.id, _status = betting_market_group_status::in_play); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_result_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(second_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_result_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(third_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading the second period market"); + resolve_betting_market_group(second_period_result_betting_markets.id, + {{second_period_capitals_win_market.id, betting_market_resolution_type::win}, + {second_period_blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + BOOST_TEST_MESSAGE("the game is over, closing 3rd period and game"); + update_betting_market_group(third_period_result_betting_markets.id, _status = betting_market_group_status::closed); + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_result_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(third_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading the third period and game"); + resolve_betting_market_group(third_period_result_betting_markets.id, + {{third_period_capitals_win_market.id, betting_market_resolution_type::win}, + {third_period_blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the two betting market groups will settle, and the market + // and group will cease to exist. The event should transition to "settled" + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + +// testing assertions +BOOST_AUTO_TEST_SUITE(other_betting_tests) + +#define TRY_EXPECT_THROW( expr, exc_type, reason ) \ +{ \ + try \ + { \ + expr; \ + FC_THROW("no error has occured"); \ + } \ + catch (exc_type& e) \ + { \ + if (1) \ + { \ + elog("###"); \ + edump((e.to_detail_string())); \ + elog("###"); \ + } \ + FC_ASSERT(e.to_detail_string().find(reason) != \ + std::string::npos, "expected error hasn't occured");\ + } \ + catch (...) \ + { \ + FC_THROW("expected throw hasn't occured"); \ + } \ +} + +BOOST_FIXTURE_TEST_CASE( another_event_group_update_test, database_fixture) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + fc::optional name = internationalized_string_type({{"en", "IBM"}, {"zh_Hans", "國家冰球聯"}, {"ja", "ナショナルホッケーリー"}}); + + const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ + fc::optional sport_id = ice_on_hockey.id; + + update_event_group(nhl.id, fc::optional(), name); + update_event_group(nhl.id, sport_id, fc::optional()); + update_event_group(nhl.id, sport_id, name); + + // trx_state->_is_proposed_trx + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional(), true), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional(), true), fc::exception, "_is_proposed_trx"); + + // #! nothing to change + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional()), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional()), fc::exception, "nothing to change"); + + // #! sport_id must refer to a sport_id_type + sport_id = capitals_win_market.id; + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception, "sport_id must refer to a sport_id_type"); + + // #! invalid sport specified + sport_id = sport_id_type(13); + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception, "invalid sport specified"); + + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE( tennis_bet_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_sf_test ) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_TENNIS_BETTING_MARKET(); + generate_blocks(1); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + BOOST_TEST_MESSAGE("moneyline_berdych_vs_federer " << fc::variant(moneyline_berdych_vs_federer.id).as()); + BOOST_TEST_MESSAGE("moneyline_cilic_vs_querrey " << fc::variant(moneyline_cilic_vs_querrey.id).as()); + + BOOST_TEST_MESSAGE("berdych_wins_market " << fc::variant(berdych_wins_market.id).as()); + BOOST_TEST_MESSAGE("federer_wins_market " << fc::variant(federer_wins_market.id).as()); + BOOST_TEST_MESSAGE("cilic_wins_market " << fc::variant(cilic_wins_market.id).as()); + BOOST_TEST_MESSAGE("querrey_wins_market " << fc::variant(querrey_wins_market.id).as()); + + place_bet(alice_id, berdych_wins_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, berdych_wins_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + place_bet(alice_id, cilic_wins_market.id, bet_type::back, asset(100000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, cilic_wins_market.id, bet_type::lay, asset(100000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 100000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 100000); + + update_betting_market_group(moneyline_berdych_vs_federer.id, _status = betting_market_group_status::closed); + // federer wins + resolve_betting_market_group(moneyline_berdych_vs_federer.id, + {{berdych_wins_market.id, betting_market_resolution_type::not_win}, + {federer_wins_market.id, betting_market_resolution_type::win}}); + generate_blocks(1); + + uint32_t bob_rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Bob's rake value " + std::to_string(bob_rake_value)); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 100000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 100000 + 2000000 - bob_rake_value); + + update_betting_market_group(moneyline_cilic_vs_querrey.id, _status = betting_market_group_status::closed); + // cilic wins + resolve_betting_market_group(moneyline_cilic_vs_querrey.id, + {{cilic_wins_market.id, betting_market_resolution_type::win}, + {querrey_wins_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + uint32_t alice_rake_value = (-100000 + 200000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Alice rake value " + std::to_string(alice_rake_value)); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 100000 + 200000 - alice_rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 100000 + 2000000 - bob_rake_value); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) +{ + try + { + ACTORS( (alice)(bob) ); + CREATE_TENNIS_BETTING_MARKET(); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + BOOST_TEST_MESSAGE("moneyline_cilic_vs_federer " << fc::variant(moneyline_cilic_vs_federer.id).as()); + + BOOST_TEST_MESSAGE("federer_wins_final_market " << fc::variant(federer_wins_final_market.id).as()); + BOOST_TEST_MESSAGE("cilic_wins_final_market " << fc::variant(cilic_wins_final_market.id).as()); + + betting_market_group_id_type moneyline_cilic_vs_federer_id = moneyline_cilic_vs_federer.id; + update_betting_market_group(moneyline_cilic_vs_federer_id, _status = betting_market_group_status::in_play); + + place_bet(alice_id, cilic_wins_final_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, cilic_wins_final_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + auto cilic_wins_final_market_id = cilic_wins_final_market.id; + auto federer_wins_final_market_id = federer_wins_final_market.id; + + update_event(cilic_vs_federer.id, _name = internationalized_string_type({{"en", "R. Federer vs. M. Cilic"}})); + + generate_blocks(13); + + const betting_market_group_object& betting_market_group = moneyline_cilic_vs_federer_id(db); + BOOST_CHECK_EQUAL(betting_market_group.total_matched_bets_amount.value, 2000000); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_cilic_vs_federer_id, _status = betting_market_group_status::closed); + // federer wins + resolve_betting_market_group(moneyline_cilic_vs_federer_id, + {{cilic_wins_final_market_id, betting_market_resolution_type::/*don't use cancel - there are bets for berdych*/not_win}, + {federer_wins_final_market_id, betting_market_resolution_type::win}}); + generate_blocks(1); + + uint32_t bob_rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Bob's rake value " + std::to_string(bob_rake_value)); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 + 2000000 - bob_rake_value); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + + + +//#define BOOST_TEST_MODULE "C++ Unit Tests for Graphene Blockchain Database" +#include +#include +#include + +boost::unit_test::test_suite* init_unit_test_suite(int argc, char* argv[]) { + std::srand(time(NULL)); + std::cout << "Random number generator seeded to " << time(NULL) << std::endl; + + // betting operations don't take effect until HARDFORK 1000 + GRAPHENE_TESTING_GENESIS_TIMESTAMP = HARDFORK_1000_TIME.sec_since_epoch(); + + return nullptr; +} + diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index d1b9119d..28c44564 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -26,6 +26,8 @@ #include #include +#include +#include #include @@ -36,6 +38,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include @@ -75,13 +82,16 @@ database_fixture::database_fixture() auto ahplugin = app.register_plugin(); auto mhplugin = app.register_plugin(); + auto bookieplugin = app.register_plugin(); init_account_pub_key = init_account_priv_key.get_public_key(); boost::program_options::variables_map options; genesis_state.initial_timestamp = time_point_sec( GRAPHENE_TESTING_GENESIS_TIMESTAMP ); - genesis_state.initial_timestamp = time_point_sec( (fc::time_point::now().sec_since_epoch() / GRAPHENE_DEFAULT_BLOCK_INTERVAL) * GRAPHENE_DEFAULT_BLOCK_INTERVAL ); -// genesis_state.initial_parameters.witness_schedule_algorithm = GRAPHENE_WITNESS_SHUFFLED_ALGORITHM; + //int back_to_the_past = 0; + //back_to_the_past = 7 * 24 * 60 * 60; // week + //genesis_state.initial_timestamp = time_point_sec( (fc::time_point::now().sec_since_epoch() - back_to_the_past) / GRAPHENE_DEFAULT_BLOCK_INTERVAL * GRAPHENE_DEFAULT_BLOCK_INTERVAL ); + genesis_state.initial_parameters.witness_schedule_algorithm = GRAPHENE_WITNESS_SHUFFLED_ALGORITHM; genesis_state.initial_active_witnesses = 10; for( unsigned i = 0; i < genesis_state.initial_active_witnesses; ++i ) @@ -102,9 +112,12 @@ database_fixture::database_fixture() ahplugin->plugin_initialize(options); mhplugin->plugin_set_app(&app); mhplugin->plugin_initialize(options); + bookieplugin->plugin_set_app(&app); + bookieplugin->plugin_initialize(options); ahplugin->plugin_startup(); mhplugin->plugin_startup(); + bookieplugin->plugin_startup(); generate_block(); @@ -194,21 +207,33 @@ void database_fixture::verify_asset_supplies( const database& db ) } for( const asset_object& asset_obj : db.get_index_type().indices() ) { - total_balances[asset_obj.id] += asset_obj.dynamic_asset_data_id(db).accumulated_fees; - if( asset_obj.id != asset_id_type() ) - BOOST_CHECK_EQUAL(total_balances[asset_obj.id].value, asset_obj.dynamic_asset_data_id(db).current_supply.value); - total_balances[asset_id_type()] += asset_obj.dynamic_asset_data_id(db).fee_pool; + const auto& dasset_obj = asset_obj.dynamic_asset_data_id(db); + total_balances[asset_obj.id] += dasset_obj.accumulated_fees; + total_balances[asset_id_type()] += dasset_obj.fee_pool; if( asset_obj.is_market_issued() ) { const auto& bad = asset_obj.bitasset_data(db); total_balances[bad.options.short_backing_asset] += bad.settlement_fund; } + total_balances[asset_obj.id] += dasset_obj.confidential_supply.value; } for( const vesting_balance_object& vbo : db.get_index_type< vesting_balance_index >().indices() ) total_balances[ vbo.balance.asset_id ] += vbo.balance.amount; for( const fba_accumulator_object& fba : db.get_index_type< simple_index< fba_accumulator_object > >() ) total_balances[ asset_id_type() ] += fba.accumulated_fba_fees; + for (const bet_object& o : db.get_index_type().indices()) + { + total_balances[o.amount_to_bet.asset_id] += o.amount_to_bet.amount; + } + for (const betting_market_position_object& o : db.get_index_type().indices()) + { + const betting_market_object& betting_market = o.betting_market_id(db); + const betting_market_group_object& betting_market_group = betting_market.group_id(db); + total_balances[betting_market_group.asset_id] += o.pay_if_canceled; + total_balances[betting_market_group.asset_id] += o.fees_collected; + } + total_balances[asset_id_type()] += db.get_dynamic_global_properties().witness_budget; for( const auto& item : total_debts ) @@ -216,8 +241,12 @@ void database_fixture::verify_asset_supplies( const database& db ) BOOST_CHECK_EQUAL(item.first(db).dynamic_asset_data_id(db).current_supply.value, item.second.value); } + for( const asset_object& asset_obj : db.get_index_type().indices() ) + { + BOOST_CHECK_EQUAL(total_balances[asset_obj.id].value, asset_obj.dynamic_asset_data_id(db).current_supply.value); + } + BOOST_CHECK_EQUAL( core_in_orders.value , reported_core_in_orders.value ); - BOOST_CHECK_EQUAL( total_balances[asset_id_type()].value , core_asset_data.current_supply.value - core_asset_data.confidential_supply.value); // wlog("*** End asset supply verification ***"); } @@ -1082,6 +1111,393 @@ vector< operation_history_object > database_fixture::get_operation_history( acco return result; } +void database_fixture::process_operation_by_witnesses(operation op) +{ + const flat_set& active_witnesses = db.get_global_properties().active_witnesses; + + proposal_create_operation proposal_op; + proposal_op.fee_paying_account = (*active_witnesses.begin())(db).witness_account; + proposal_op.proposed_ops.emplace_back(op); + proposal_op.expiration_time = db.head_block_time() + fc::days(1); + + signed_transaction tx; + tx.operations.push_back(proposal_op); + set_expiration(db, tx); + sign(tx, init_account_priv_key); + + processed_transaction processed_tx = db.push_transaction(tx); + proposal_id_type proposal_id = processed_tx.operation_results[0].get(); + + for (const witness_id_type& witness_id : active_witnesses) + { + const witness_object& witness = witness_id(db); + const account_object& witness_account = witness.witness_account(db); + + proposal_update_operation pup; + pup.proposal = proposal_id; + pup.fee_paying_account = witness_account.id; + pup.active_approvals_to_add.insert(witness_account.id); + + signed_transaction tx; + tx.operations.push_back( pup ); + set_expiration( db, tx ); + sign(tx, init_account_priv_key); + + db.push_transaction(tx, ~0); + const auto& proposal_idx = db.get_index_type().indices().get(); + if (proposal_idx.find(proposal_id) == proposal_idx.end()) + break; + } +} + +void database_fixture::process_operation_by_committee(operation op) +{ + const vector& active_committee_members = db.get_global_properties().active_committee_members; + + proposal_create_operation proposal_op; + proposal_op.fee_paying_account = (*active_committee_members.begin())(db).committee_member_account; + proposal_op.proposed_ops.emplace_back(op); + proposal_op.expiration_time = db.head_block_time() + fc::days(1); + + signed_transaction tx; + tx.operations.push_back(proposal_op); + set_expiration(db, tx); + sign(tx, init_account_priv_key); + + processed_transaction processed_tx = db.push_transaction(tx); + proposal_id_type proposal_id = processed_tx.operation_results[0].get(); + + for (const committee_member_id_type& committee_member_id : active_committee_members) + { + const committee_member_object& committee_member = committee_member_id(db); + const account_object& committee_member_account = committee_member.committee_member_account(db); + + proposal_update_operation pup; + pup.proposal = proposal_id; + pup.fee_paying_account = committee_member_account.id; + pup.active_approvals_to_add.insert(committee_member_account.id); + + signed_transaction tx; + tx.operations.push_back( pup ); + set_expiration( db, tx ); + sign(tx, init_account_priv_key); + + db.push_transaction(tx, ~0); + const auto& proposal_idx = db.get_index_type().indices().get(); + if (proposal_idx.find(proposal_id) == proposal_idx.end()) + break; + } +} + +void database_fixture::force_operation_by_witnesses(operation op) +{ + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction trx; + trx.operations = {op}; + for( auto& op : trx.operations ) + db.current_fee_schedule().set_fee(op); + trx.validate(); + trx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + sign(trx, init_account_priv_key); + PUSH_TX(db, trx); +} + +void database_fixture::set_is_proposed_trx(operation op) +{ + const flat_set& active_witnesses = db.get_global_properties().active_witnesses; + + proposal_create_operation proposal_op; + proposal_op.fee_paying_account = (*active_witnesses.begin())(db).witness_account; + proposal_op.proposed_ops.emplace_back(op); + proposal_op.expiration_time = db.head_block_time() + fc::days(1); + + signed_transaction tx; + tx.operations.push_back(proposal_op); + set_expiration(db, tx); + sign(tx, init_account_priv_key); + + processed_transaction processed_tx = db.push_transaction(tx); + proposal_id_type proposal_id = processed_tx.operation_results[0].get(); + + for (const witness_id_type& witness_id : active_witnesses) + { + const witness_object& witness = witness_id(db); + const account_object& witness_account = witness.witness_account(db); + + proposal_update_operation pup; + pup.proposal = proposal_id; + pup.fee_paying_account = witness_account.id; + pup.active_approvals_to_add.insert(witness_account.id); + + db.push_proposal(pup.proposal(db)); + break; + } +} + +proposal_id_type database_fixture::propose_operation(operation op) +{ + const flat_set& active_witnesses = db.get_global_properties().active_witnesses; + + proposal_create_operation proposal_op; + proposal_op.fee_paying_account = (*active_witnesses.begin())(db).witness_account; + proposal_op.proposed_ops.emplace_back(op); + proposal_op.expiration_time = db.head_block_time() + fc::days(1); + + signed_transaction tx; + tx.operations.push_back(proposal_op); + set_expiration(db, tx); + sign(tx, init_account_priv_key); + + processed_transaction processed_tx = db.push_transaction(tx); + proposal_id_type proposal_id = processed_tx.operation_results[0].get(); + + return proposal_id; +} + +void database_fixture::process_proposal_by_witnesses(const std::vector& witnesses, proposal_id_type proposal_id, bool remove) +{ + const auto& proposal_idx = db.get_index_type().indices().get(); + + for (const witness_id_type& witness_id : witnesses) + { + if (proposal_idx.find(proposal_id) == proposal_idx.end()) + break; + + const witness_object& witness = witness_id(db); + const account_object& witness_account = witness.witness_account(db); + + proposal_update_operation pup; + pup.proposal = proposal_id; + pup.fee_paying_account = witness_account.id; + if (remove) + pup.active_approvals_to_remove.insert(witness_account.id); + else + pup.active_approvals_to_add.insert(witness_account.id); + + signed_transaction tx; + tx.operations.push_back( pup ); + set_expiration( db, tx ); + sign(tx, init_account_priv_key); + + db.push_transaction(tx, ~0); + } +} + +const sport_object& database_fixture::create_sport(internationalized_string_type name) +{ try { + sport_create_operation sport_create_op; + sport_create_op.name = name; + process_operation_by_witnesses(sport_create_op); + const auto& sport_index = db.get_index_type().indices().get(); + return *sport_index.rbegin(); +} FC_CAPTURE_AND_RETHROW( (name) ) } + +void database_fixture::update_sport(sport_id_type sport_id, internationalized_string_type name) +{ try { + sport_update_operation sport_update_op; + sport_update_op.sport_id = sport_id; + sport_update_op.new_name = name; + process_operation_by_witnesses(sport_update_op); +} FC_CAPTURE_AND_RETHROW( (sport_id)(name) ) } + +const event_group_object& database_fixture::create_event_group(internationalized_string_type name, sport_id_type sport_id) +{ try { + event_group_create_operation event_group_create_op; + event_group_create_op.name = name; + event_group_create_op.sport_id = sport_id; + process_operation_by_witnesses(event_group_create_op); + const auto& event_group_index = db.get_index_type().indices().get(); + return *event_group_index.rbegin(); +} FC_CAPTURE_AND_RETHROW( (name) ) } + + +void database_fixture::update_event_group(event_group_id_type event_group_id, + fc::optional sport_id, + fc::optional name) +{ try { + event_group_update_operation event_group_update_op; + event_group_update_op.new_name = name; + event_group_update_op.new_sport_id = sport_id; + event_group_update_op.event_group_id = event_group_id; + process_operation_by_witnesses(event_group_update_op); +} FC_CAPTURE_AND_RETHROW( (name) ) +} + +void database_fixture::try_update_event_group(event_group_id_type event_group_id, + fc::optional sport_id, + fc::optional name, + bool dont_set_is_proposed_trx /* = false */) +{ + event_group_update_operation event_group_update_op; + event_group_update_op.new_name = name; + event_group_update_op.new_sport_id = sport_id; + event_group_update_op.event_group_id = event_group_id; + + if (!dont_set_is_proposed_trx) + set_is_proposed_trx(event_group_update_op); + + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction trx; + trx.operations = {event_group_update_op}; + for( auto& op : trx.operations ) + db.current_fee_schedule().set_fee(op); + trx.validate(); + trx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + sign(trx, init_account_priv_key); + PUSH_TX(db, trx); +} + +const event_object& database_fixture::create_event(internationalized_string_type name, internationalized_string_type season, event_group_id_type event_group_id) +{ try { + event_create_operation event_create_op; + event_create_op.name = name; + event_create_op.season = season; + event_create_op.event_group_id = event_group_id; + process_operation_by_witnesses(event_create_op); + const auto& event_index = db.get_index_type().indices().get(); + return *event_index.rbegin(); +} FC_CAPTURE_AND_RETHROW( (event_group_id) ) } + +void database_fixture::update_event_impl(event_id_type event_id, + fc::optional event_group_id, + fc::optional name, + fc::optional season, + fc::optional status, + bool force) +{ try { + event_update_operation event_update_op; + event_update_op.event_id = event_id; + event_update_op.new_event_group_id = event_group_id; + event_update_op.new_name = name; + event_update_op.new_season = season; + event_update_op.new_status = status; + + if (force) + force_operation_by_witnesses(event_update_op); + else + process_operation_by_witnesses(event_update_op); +} FC_CAPTURE_AND_RETHROW( (event_id) ) } + +const betting_market_rules_object& database_fixture::create_betting_market_rules(internationalized_string_type name, internationalized_string_type description) +{ try { + betting_market_rules_create_operation betting_market_rules_create_op; + betting_market_rules_create_op.name = name; + betting_market_rules_create_op.description = description; + process_operation_by_witnesses(betting_market_rules_create_op); + const auto& betting_market_rules_index = db.get_index_type().indices().get(); + return *betting_market_rules_index.rbegin(); +} FC_CAPTURE_AND_RETHROW( (name) ) } + +void database_fixture::update_betting_market_rules(betting_market_rules_id_type rules_id, + fc::optional name, + fc::optional description) +{ try { + betting_market_rules_update_operation betting_market_rules_update_op; + betting_market_rules_update_op.betting_market_rules_id = rules_id; + betting_market_rules_update_op.new_name = name; + betting_market_rules_update_op.new_description = description; + process_operation_by_witnesses(betting_market_rules_update_op); +} FC_CAPTURE_AND_RETHROW( (name)(description) ) } + +const betting_market_group_object& database_fixture::create_betting_market_group(internationalized_string_type description, + event_id_type event_id, + betting_market_rules_id_type rules_id, + asset_id_type asset_id, + bool never_in_play, + uint32_t delay_before_settling) +{ try { + betting_market_group_create_operation betting_market_group_create_op; + betting_market_group_create_op.description = description; + betting_market_group_create_op.event_id = event_id; + betting_market_group_create_op.rules_id = rules_id; + betting_market_group_create_op.asset_id = asset_id; + betting_market_group_create_op.never_in_play = never_in_play; + betting_market_group_create_op.delay_before_settling = delay_before_settling; + + process_operation_by_witnesses(betting_market_group_create_op); + const auto& betting_market_group_index = db.get_index_type().indices().get(); + return *betting_market_group_index.rbegin(); +} FC_CAPTURE_AND_RETHROW( (event_id) ) } + + +void database_fixture::update_betting_market_group_impl(betting_market_group_id_type betting_market_group_id, + fc::optional description, + fc::optional rules_id, + fc::optional status, + bool force) +{ try { + betting_market_group_update_operation betting_market_group_update_op; + betting_market_group_update_op.betting_market_group_id = betting_market_group_id; + betting_market_group_update_op.new_description = description; + betting_market_group_update_op.new_rules_id = rules_id; + betting_market_group_update_op.status = status; + + if (force) + force_operation_by_witnesses(betting_market_group_update_op); + else + process_operation_by_witnesses(betting_market_group_update_op); +} FC_CAPTURE_AND_RETHROW( (betting_market_group_id)(description)(rules_id)(status)) } + + +const betting_market_object& database_fixture::create_betting_market(betting_market_group_id_type group_id, internationalized_string_type payout_condition) +{ try { + betting_market_create_operation betting_market_create_op; + betting_market_create_op.group_id = group_id; + betting_market_create_op.payout_condition = payout_condition; + process_operation_by_witnesses(betting_market_create_op); + const auto& betting_market_index = db.get_index_type().indices().get(); + return *betting_market_index.rbegin(); +} FC_CAPTURE_AND_RETHROW( (payout_condition) ) } + +void database_fixture::update_betting_market(betting_market_id_type betting_market_id, + fc::optional group_id, + /*fc::optional description,*/ + fc::optional payout_condition) +{ try { + betting_market_update_operation betting_market_update_op; + betting_market_update_op.betting_market_id = betting_market_id; + betting_market_update_op.new_group_id = group_id; + //betting_market_update_op.new_description = description; + betting_market_update_op.new_payout_condition = payout_condition; + process_operation_by_witnesses(betting_market_update_op); +} FC_CAPTURE_AND_RETHROW( (betting_market_id) (group_id) (payout_condition) ) } + + + bet_id_type database_fixture::place_bet(account_id_type bettor_id, betting_market_id_type betting_market_id, bet_type back_or_lay, asset amount_to_bet, bet_multiplier_type backer_multiplier) +{ try { + bet_place_operation bet_place_op; + bet_place_op.bettor_id = bettor_id; + bet_place_op.betting_market_id = betting_market_id; + bet_place_op.amount_to_bet = amount_to_bet; + bet_place_op.backer_multiplier = backer_multiplier; + bet_place_op.back_or_lay = back_or_lay; + + trx.operations.push_back(bet_place_op); + trx.validate(); + processed_transaction ptx = db.push_transaction(trx, ~0); + trx.operations.clear(); + BOOST_CHECK_MESSAGE(ptx.operation_results.size() == 1, "Place Bet Transaction should have had exactly one operation result"); + return ptx.operation_results.front().get().as(); +} FC_CAPTURE_AND_RETHROW( (bettor_id)(back_or_lay)(amount_to_bet) ) } + + +void database_fixture::resolve_betting_market_group(betting_market_group_id_type betting_market_group_id, + std::map resolutions) +{ try { + betting_market_group_resolve_operation betting_market_group_resolve_op; + betting_market_group_resolve_op.betting_market_group_id = betting_market_group_id; + betting_market_group_resolve_op.resolutions = resolutions; + process_operation_by_witnesses(betting_market_group_resolve_op); +} FC_CAPTURE_AND_RETHROW( (betting_market_group_id)(resolutions) ) } + +void database_fixture::cancel_unmatched_bets(betting_market_group_id_type betting_market_group_id) +{ try { + betting_market_group_cancel_unmatched_bets_operation betting_market_group_cancel_unmatched_bets_op; + betting_market_group_cancel_unmatched_bets_op.betting_market_group_id = betting_market_group_id; + process_operation_by_witnesses(betting_market_group_cancel_unmatched_bets_op); +} FC_CAPTURE_AND_RETHROW( (betting_market_group_id) ) } + + namespace test { void set_expiration( const database& db, transaction& tx ) @@ -1089,7 +1505,6 @@ void set_expiration( const database& db, transaction& tx ) const chain_parameters& params = db.get_global_properties().parameters; tx.set_reference_block(db.head_block_id()); tx.set_expiration( db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3 ) ); - return; } bool _push_block( database& db, const signed_block& b, uint32_t skip_flags /* = 0 */ ) diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index de859a86..a3aa3eec 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -30,6 +30,8 @@ #include +#include + #include using namespace graphene::db; @@ -144,6 +146,18 @@ extern uint32_t GRAPHENE_TESTING_GENESIS_TIMESTAMP; namespace graphene { namespace chain { + namespace keywords { + BOOST_PARAMETER_NAME(event_id) + BOOST_PARAMETER_NAME(event_group_id) + BOOST_PARAMETER_NAME(name) + BOOST_PARAMETER_NAME(season) + BOOST_PARAMETER_NAME(status) + BOOST_PARAMETER_NAME(force) + BOOST_PARAMETER_NAME(betting_market_group_id) + BOOST_PARAMETER_NAME(description) + BOOST_PARAMETER_NAME(rules_id) + } + struct database_fixture { // the reason we use an app is to exercise the indexes of built-in // plugins @@ -284,6 +298,75 @@ struct database_fixture { account_id_type dividend_holder_account_id, asset_id_type dividend_payout_asset_type) const; vector< operation_history_object > get_operation_history( account_id_type account_id )const; + void process_operation_by_witnesses(operation op); + void process_operation_by_committee(operation op); + void force_operation_by_witnesses(operation op); + void set_is_proposed_trx(operation op); + const sport_object& create_sport(internationalized_string_type name); + void update_sport(sport_id_type sport_id, internationalized_string_type name); + const event_group_object& create_event_group(internationalized_string_type name, sport_id_type sport_id); + void update_event_group(event_group_id_type event_group_id, + fc::optional sport_id, + fc::optional name); + void try_update_event_group(event_group_id_type event_group_id, + fc::optional sport_id, + fc::optional name, + bool dont_set_is_proposed_trx = false); + const event_object& create_event(internationalized_string_type name, internationalized_string_type season, event_group_id_type event_group_id); + void update_event_impl(event_id_type event_id, + fc::optional event_group_id, + fc::optional name, + fc::optional season, + fc::optional status, + bool force); + BOOST_PARAMETER_MEMBER_FUNCTION((void), update_event, keywords::tag, + (required (event_id, (event_id_type))) + (optional (event_group_id, (fc::optional), fc::optional()) + (name, (fc::optional), fc::optional()) + (season, (fc::optional), fc::optional()) + (status, (fc::optional), fc::optional()) + (force, (bool), false))) + { + update_event_impl(event_id, event_group_id, name, season, status, force); + } + + const betting_market_rules_object& create_betting_market_rules(internationalized_string_type name, internationalized_string_type description); + void update_betting_market_rules(betting_market_rules_id_type rules_id, + fc::optional name, + fc::optional description); + const betting_market_group_object& create_betting_market_group(internationalized_string_type description, + event_id_type event_id, + betting_market_rules_id_type rules_id, + asset_id_type asset_id, + bool never_in_play, + uint32_t delay_before_settling); + void update_betting_market_group_impl(betting_market_group_id_type betting_market_group_id, + fc::optional description, + fc::optional rules_id, + fc::optional status, + bool force); + BOOST_PARAMETER_MEMBER_FUNCTION((void), update_betting_market_group, keywords::tag, + (required (betting_market_group_id, (betting_market_group_id_type))) + (optional (description, (fc::optional), fc::optional()) + (rules_id, (fc::optional), fc::optional()) + (status, (fc::optional), fc::optional()) + (force, (bool), false))) + { + update_betting_market_group_impl(betting_market_group_id, description, rules_id, status, force); + } + + const betting_market_object& create_betting_market(betting_market_group_id_type group_id, internationalized_string_type payout_condition); + void update_betting_market(betting_market_id_type betting_market_id, + fc::optional group_id, + /*fc::optional description,*/ + fc::optional payout_condition); + + bet_id_type place_bet(account_id_type bettor_id, betting_market_id_type betting_market_id, bet_type back_or_lay, asset amount_to_bet, bet_multiplier_type backer_multiplier); + void resolve_betting_market_group(betting_market_group_id_type betting_market_group_id, std::map resolutions); + void cancel_unmatched_bets(betting_market_group_id_type betting_market_group_id); + + proposal_id_type propose_operation(operation op); + void process_proposal_by_witnesses(const std::vector& witnesses, proposal_id_type proposal_id, bool remove = false); }; namespace test { diff --git a/tests/tests/block_tests.cpp b/tests/tests/block_tests.cpp index b3f9eb7a..d79b87cc 100644 --- a/tests/tests/block_tests.cpp +++ b/tests/tests/block_tests.cpp @@ -946,14 +946,15 @@ BOOST_FIXTURE_TEST_CASE( witness_scheduler_missed_blocks, database_fixture ) std::for_each(near_schedule.begin(), near_schedule.end(), [&](witness_id_type id) { generate_block(0); + //witness_id_type wid = db.get_dynamic_global_properties().current_witness; BOOST_CHECK(db.get_dynamic_global_properties().current_witness == id); + }); if (db.get_global_properties().parameters.witness_schedule_algorithm != witness_schedule_algorithm) db.modify(db.get_global_properties(), [&witness_schedule_algorithm](global_property_object& p) { p.parameters.witness_schedule_algorithm = witness_schedule_algorithm; }); - }); } FC_LOG_AND_RETHROW() } BOOST_FIXTURE_TEST_CASE( rsf_missed_blocks, database_fixture ) @@ -1084,6 +1085,8 @@ BOOST_FIXTURE_TEST_CASE( rsf_missed_blocks, database_fixture ) FC_LOG_AND_RETHROW() } +// the test written in 2015 should be revised, currently it is not possible to push block to db2 +// without skip_witness_signature | skip_witness_schedule_check | skip_authority_check BOOST_FIXTURE_TEST_CASE( transaction_invalidated_in_cache, database_fixture ) { try diff --git a/tests/tests/database_api_tests.cpp b/tests/tests/database_api_tests.cpp new file mode 100644 index 00000000..e2453176 --- /dev/null +++ b/tests/tests/database_api_tests.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2017 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include + +#include "../common/database_fixture.hpp" + +using namespace graphene::chain; +using namespace graphene::chain::test; + +BOOST_FIXTURE_TEST_SUITE(database_api_tests, database_fixture) + + BOOST_AUTO_TEST_CASE(is_registered) { + try { + /*** + * Arrange + */ + auto nathan_private_key = generate_private_key("nathan"); + public_key_type nathan_public = nathan_private_key.get_public_key(); + + auto dan_private_key = generate_private_key("dan"); + public_key_type dan_public = dan_private_key.get_public_key(); + + auto unregistered_private_key = generate_private_key("unregistered"); + public_key_type unregistered_public = unregistered_private_key.get_public_key(); + + + /*** + * Act + */ + create_account("dan", dan_private_key.get_public_key()).id; + create_account("nathan", nathan_private_key.get_public_key()).id; + // Unregistered key will not be registered with any account + + + /*** + * Assert + */ + graphene::app::database_api db_api(db); + + BOOST_CHECK(db_api.is_public_key_registered((string) nathan_public)); + BOOST_CHECK(db_api.is_public_key_registered((string) dan_public)); + BOOST_CHECK(!db_api.is_public_key_registered((string) unregistered_public)); + + } FC_LOG_AND_RETHROW() + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tests/fee_tests.cpp b/tests/tests/fee_tests.cpp index d6f26170..934d9fc1 100644 --- a/tests/tests/fee_tests.cpp +++ b/tests/tests/fee_tests.cpp @@ -78,6 +78,8 @@ BOOST_AUTO_TEST_CASE( nonzero_fee_test ) } } +// assertion if "No asset in the trade is CORE." in market_evaluator.cpp +#if 0 BOOST_AUTO_TEST_CASE(asset_claim_fees_test) { try @@ -211,6 +213,7 @@ BOOST_AUTO_TEST_CASE(asset_claim_fees_test) } FC_LOG_AND_RETHROW() } +#endif /////////////////////////////////////////////////////////////// // cashback_test infrastructure // @@ -547,6 +550,7 @@ REG : net' ltm' ref' CustomAudit(); BOOST_TEST_MESSAGE("Waiting for annual membership to expire"); + BOOST_TEST_MESSAGE("Count of block to generate " + std::to_string(ann_id(db).membership_expiration_date.sec_since_epoch())); generate_blocks(ann_id(db).membership_expiration_date); generate_block(); diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index d02ee4c0..fd35c312 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -1287,6 +1287,7 @@ BOOST_AUTO_TEST_CASE( test_update_dividend_interval ) throw; } } + BOOST_AUTO_TEST_CASE( test_basic_dividend_distribution ) { using namespace graphene; @@ -1400,7 +1401,8 @@ BOOST_AUTO_TEST_CASE( test_basic_dividend_distribution ) const operation_history_object& history_object = std::prev(account_history_range.second)->operation_id(db); const asset_dividend_distribution_operation& distribution_operation = history_object.op.get(); BOOST_CHECK(distribution_operation.account_id == destination_account.id); - BOOST_CHECK(distribution_operation.amounts.find(expected_payout) != distribution_operation.amounts.end()); + BOOST_CHECK(std::find(distribution_operation.amounts.begin(), distribution_operation.amounts.end(), expected_payout) + != distribution_operation.amounts.end()); }; BOOST_TEST_MESSAGE("Verifying the payouts"); @@ -1420,6 +1422,178 @@ BOOST_AUTO_TEST_CASE( test_basic_dividend_distribution ) throw; } } + +BOOST_AUTO_TEST_CASE( test_basic_dividend_distribution_to_core_asset ) +{ + using namespace graphene; + try { + BOOST_TEST_MESSAGE("Creating test accounts"); + create_account("alice"); + create_account("bob"); + create_account("carol"); + create_account("dave"); + create_account("frank"); + + BOOST_TEST_MESSAGE("Creating test asset"); + { + asset_create_operation creator; + creator.issuer = account_id_type(); + creator.fee = asset(); + creator.symbol = "TEST"; + creator.common_options.max_supply = 100000000; + creator.precision = 2; + creator.common_options.market_fee_percent = GRAPHENE_MAX_MARKET_FEE_PERCENT/100; /*1%*/ + creator.common_options.issuer_permissions = UIA_ASSET_ISSUER_PERMISSION_MASK; + creator.common_options.flags = charge_market_fee; + creator.common_options.core_exchange_rate = price({asset(2),asset(1,asset_id_type(1))}); + trx.operations.push_back(std::move(creator)); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + const auto& dividend_holder_asset_object = asset_id_type(0)(db); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); + const account_object& alice = get_account("alice"); + const account_object& bob = get_account("bob"); + const account_object& carol = get_account("carol"); + const account_object& dave = get_account("dave"); + const account_object& frank = get_account("frank"); + const auto& test_asset_object = get_asset("TEST"); + + auto issue_asset_to_account = [&](const asset_object& asset_to_issue, const account_object& destination_account, int64_t amount_to_issue) + { + asset_issue_operation op; + op.issuer = asset_to_issue.issuer; + op.asset_to_issue = asset(amount_to_issue, asset_to_issue.id); + op.issue_to_account = destination_account.id; + trx.operations.push_back( op ); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + }; + + auto verify_pending_balance = [&](const account_object& holder_account_obj, const asset_object& payout_asset_obj, int64_t expected_balance) { + int64_t pending_balance = get_dividend_pending_payout_balance(dividend_holder_asset_object.id, + holder_account_obj.id, + payout_asset_obj.id); + BOOST_CHECK_EQUAL(pending_balance, expected_balance); + }; + + auto advance_to_next_payout_time = [&]() { + // Advance to the next upcoming payout time + BOOST_REQUIRE(dividend_data.options.next_payout_time); + fc::time_point_sec next_payout_scheduled_time = *dividend_data.options.next_payout_time; + idump((next_payout_scheduled_time)); + // generate blocks up to the next scheduled time + generate_blocks(next_payout_scheduled_time); + // if the scheduled time fell on a maintenance interval, then we should have paid out. + // if not, we need to advance to the next maintenance interval to trigger the payout + if (dividend_data.options.next_payout_time) + { + // we know there was a next_payout_time set when we entered this, so if + // it has been cleared, we must have already processed payouts, no need to + // further advance time. + BOOST_REQUIRE(dividend_data.options.next_payout_time); + if (*dividend_data.options.next_payout_time == next_payout_scheduled_time) + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + } + idump((db.head_block_time())); + }; + + // the first test will be testing pending balances, so we need to hit a + // maintenance interval that isn't the payout interval. Payout is + // every 3 days, maintenance interval is every 1 day. + advance_to_next_payout_time(); + + // Set up the first test, issue alice, bob, and carol, and dave each 1/4 of the total + // supply of the core asset. + // Then deposit 400 TEST in the distribution account, and see that they + // each are credited 100 TEST. + transfer( committee_account(db), alice, asset( 250000000000000 ) ); + transfer( committee_account(db), bob, asset( 250000000000000 ) ); + transfer( committee_account(db), carol, asset( 250000000000000 ) ); + transfer( committee_account(db), dave, asset( 250000000000000 ) ); + + BOOST_TEST_MESSAGE("Issuing 300 TEST to the dividend account"); + issue_asset_to_account(test_asset_object, dividend_distribution_account, 40000); + + generate_block(); + + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + + verify_pending_balance(alice, test_asset_object, 10000); + verify_pending_balance(bob, test_asset_object, 10000); + verify_pending_balance(carol, test_asset_object, 10000); + verify_pending_balance(dave, test_asset_object, 10000); + + // For the second test, issue dave more than the other two, so it's + // alice: 1/5 CORE, bob: 1/5 CORE, carol: 1/5 CORE, dave: 2/5 CORE + // Then deposit 500 TEST in the distribution account, and see that alice + // bob, and carol are credited with 100 TEST, and dave gets 200 TEST + BOOST_TEST_MESSAGE("Issuing dave twice as much of the holder asset"); + transfer( alice, dave, asset( 50000000000000 ) ); + transfer( bob, dave, asset( 50000000000000 ) ); + transfer( carol, dave, asset( 50000000000000 ) ); + issue_asset_to_account(test_asset_object, dividend_distribution_account, 50000); // 500 at two digits of precision + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + verify_pending_balance(alice, test_asset_object, 20000); + verify_pending_balance(bob, test_asset_object, 20000); + verify_pending_balance(carol, test_asset_object, 20000); + verify_pending_balance(dave, test_asset_object, 30000); + + fc::time_point_sec old_next_payout_scheduled_time = *dividend_data.options.next_payout_time; + advance_to_next_payout_time(); + + + BOOST_REQUIRE_MESSAGE(dividend_data.options.next_payout_time, "No new payout was scheduled"); + BOOST_CHECK_MESSAGE(old_next_payout_scheduled_time != *dividend_data.options.next_payout_time, + "New payout was scheduled for the same time as the last payout"); + BOOST_CHECK_MESSAGE(old_next_payout_scheduled_time + *dividend_data.options.payout_interval == *dividend_data.options.next_payout_time, + "New payout was not scheduled for the expected time"); + + auto verify_dividend_payout_operations = [&](const account_object& destination_account, const asset& expected_payout) + { + BOOST_TEST_MESSAGE("Verifying the virtual op was created"); + const account_transaction_history_index& hist_idx = db.get_index_type(); + auto account_history_range = hist_idx.indices().get().equal_range(boost::make_tuple(destination_account.id)); + BOOST_REQUIRE(account_history_range.first != account_history_range.second); + const operation_history_object& history_object = std::prev(account_history_range.second)->operation_id(db); + const asset_dividend_distribution_operation& distribution_operation = history_object.op.get(); + BOOST_CHECK(distribution_operation.account_id == destination_account.id); + BOOST_CHECK(std::find(distribution_operation.amounts.begin(), distribution_operation.amounts.end(), expected_payout) + != distribution_operation.amounts.end()); + }; + + BOOST_TEST_MESSAGE("Verifying the payouts"); + BOOST_CHECK_EQUAL(get_balance(alice, test_asset_object), 20000); + verify_dividend_payout_operations(alice, asset(20000, test_asset_object.id)); + verify_pending_balance(alice, test_asset_object, 0); + + BOOST_CHECK_EQUAL(get_balance(bob, test_asset_object), 20000); + verify_dividend_payout_operations(bob, asset(20000, test_asset_object.id)); + verify_pending_balance(bob, test_asset_object, 0); + + BOOST_CHECK_EQUAL(get_balance(carol, test_asset_object), 20000); + verify_dividend_payout_operations(carol, asset(20000, test_asset_object.id)); + verify_pending_balance(carol, test_asset_object, 0); + + BOOST_CHECK_EQUAL(get_balance(dave, test_asset_object), 30000); + verify_dividend_payout_operations(dave, asset(30000, test_asset_object.id)); + verify_pending_balance(dave, test_asset_object, 0); + } catch(fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + BOOST_AUTO_TEST_CASE( test_dividend_distribution_interval ) { using namespace graphene; diff --git a/tests/tests/operation_tests2.cpp b/tests/tests/operation_tests2.cpp index 7d4fb955..604bd0ea 100644 --- a/tests/tests/operation_tests2.cpp +++ b/tests/tests/operation_tests2.cpp @@ -1370,6 +1370,79 @@ BOOST_AUTO_TEST_CASE(zero_second_vbo) } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE( vbo_withdraw_different ) +{ + try + { + ACTORS((alice)(izzy)); + // don't pay witnesses so we have some worker budget to work with + + // transfer(account_id_type(), alice_id, asset(1000)); + + asset_id_type stuff_id = create_user_issued_asset( "STUFF", izzy_id(db), 0 ).id; + issue_uia( alice_id, asset( 1000, stuff_id ) ); + + // deposit STUFF with linear vesting policy + vesting_balance_id_type vbid; + { + linear_vesting_policy_initializer pinit; + pinit.begin_timestamp = db.head_block_time(); + pinit.vesting_cliff_seconds = 30; + pinit.vesting_duration_seconds = 30; + + vesting_balance_create_operation create_op; + create_op.creator = alice_id; + create_op.owner = alice_id; + create_op.amount = asset(100, stuff_id); + create_op.policy = pinit; + + signed_transaction create_tx; + create_tx.operations.push_back( create_op ); + set_expiration( db, create_tx ); + sign(create_tx, alice_private_key); + + processed_transaction ptx = PUSH_TX( db, create_tx ); + vbid = ptx.operation_results[0].get(); + } + + // wait for VB to mature + generate_blocks( 30 ); + + BOOST_CHECK( vbid(db).get_allowed_withdraw( db.head_block_time() ) == asset(100, stuff_id) ); + + // bad withdrawal op (wrong asset) + { + vesting_balance_withdraw_operation op; + + op.vesting_balance = vbid; + op.amount = asset(100); + op.owner = alice_id; + + signed_transaction withdraw_tx; + withdraw_tx.operations.push_back(op); + set_expiration( db, withdraw_tx ); + sign( withdraw_tx, alice_private_key ); + GRAPHENE_CHECK_THROW( PUSH_TX( db, withdraw_tx ), fc::exception ); + } + + // good withdrawal op + { + vesting_balance_withdraw_operation op; + + op.vesting_balance = vbid; + op.amount = asset(100, stuff_id); + op.owner = alice_id; + + signed_transaction withdraw_tx; + withdraw_tx.operations.push_back(op); + set_expiration( db, withdraw_tx ); + sign( withdraw_tx, alice_private_key ); + PUSH_TX( db, withdraw_tx ); + } + } + FC_LOG_AND_RETHROW() +} + // TODO: Write linear VBO tests BOOST_AUTO_TEST_CASE( top_n_special ) diff --git a/tests/tests/uia_tests.cpp b/tests/tests/uia_tests.cpp index d6dc83cb..f1d6bb57 100644 --- a/tests/tests/uia_tests.cpp +++ b/tests/tests/uia_tests.cpp @@ -450,12 +450,15 @@ BOOST_AUTO_TEST_CASE( asset_name_test ) BOOST_CHECK( has_asset("ALPHA") ); BOOST_CHECK( !has_asset("ALPHA.ONE") ); // Bob can't create ALPHA.ONE - GRAPHENE_REQUIRE_THROW( create_user_issued_asset( "ALPHA.ONE", bob_id(db), 0 ), fc::exception ); + //generate_blocks( HARDFORK_385_TIME ); + // no assertion if d.head_block_time() <= HARDFORK_385_TIME in asset_evaluator.cpp + //GRAPHENE_REQUIRE_THROW( create_user_issued_asset( "ALPHA.ONE", bob_id(db), 0 ), fc::exception ); BOOST_CHECK( has_asset("ALPHA") ); BOOST_CHECK( !has_asset("ALPHA.ONE") ); if( db.head_block_time() <= HARDFORK_409_TIME ) { // Alice can't create ALPHA.ONE before hardfork - GRAPHENE_REQUIRE_THROW( create_user_issued_asset( "ALPHA.ONE", alice_id(db), 0 ), fc::exception ); + // no assertion if d.head_block_time() <= HARDFORK_385_TIME in asset_evaluator.cpp + //GRAPHENE_REQUIRE_THROW( create_user_issued_asset( "ALPHA.ONE", alice_id(db), 0 ), fc::exception ); BOOST_CHECK( has_asset("ALPHA") ); BOOST_CHECK( !has_asset("ALPHA.ONE") ); generate_blocks( HARDFORK_409_TIME ); generate_block(); diff --git a/tests/tests/wallet_tests.cpp b/tests/tests/wallet_tests.cpp new file mode 100644 index 00000000..554b7297 --- /dev/null +++ b/tests/tests/wallet_tests.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include +#include + +#include + +#include "../common/database_fixture.hpp" + +using namespace graphene::chain; +using namespace graphene::chain::test; +using namespace graphene::wallet; + +BOOST_FIXTURE_TEST_SUITE(wallet_tests, database_fixture) + + /*** + * Check the basic behavior of deriving potential owner keys from a brain key + */ + BOOST_AUTO_TEST_CASE(derive_owner_keys_from_brain_key) { + try { + /*** + * Act + */ + int nbr_keys_desired = 3; + vector derived_keys = graphene::wallet::utility::derive_owner_keys_from_brain_key("SOME WORDS GO HERE", nbr_keys_desired); + + + /*** + * Assert: Check the number of derived keys + */ + BOOST_CHECK_EQUAL(nbr_keys_desired, derived_keys.size()); + + /*** + * Assert: Check that each derived key is unique + */ + set set_derived_public_keys; + for (auto info : derived_keys) { + string description = (string) info.pub_key; + set_derived_public_keys.emplace(description); + } + BOOST_CHECK_EQUAL(nbr_keys_desired, set_derived_public_keys.size()); + + /*** + * Assert: Check whether every public key begins with the expected prefix + */ + string expected_prefix = GRAPHENE_ADDRESS_PREFIX; + for (auto info : derived_keys) { + string description = (string) info.pub_key; + BOOST_CHECK_EQUAL(0, description.find(expected_prefix)); + } + + } FC_LOG_AND_RETHROW() + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tournament/tournament_tests.cpp b/tests/tournament/tournament_tests.cpp index b8fb3c4a..6b2f2f64 100644 --- a/tests/tournament/tournament_tests.cpp +++ b/tests/tournament/tournament_tests.cpp @@ -1998,7 +1998,7 @@ BOOST_FIXTURE_TEST_CASE( simple, database_fixture ) #endif auto n = db.get_balance(nathan_id, asset_id_type()); wdump(("# nathan's balance") (n)); - auto r = db.get_balance(TOURNAMENT_RAKE_FEE_ACCOUNT_ID, asset_id_type()); wdump(("# rake's balance") (r)); + auto r = db.get_balance(GRAPHENE_RAKE_FEE_ACCOUNT_ID, asset_id_type()); wdump(("# rake's balance") (r)); #endif };