a collection of flags (is_live, is_closed) to a single status field. The status changes in an event can trickle down to the market groups, and the status changes in market groups can bubble up to events.
509 lines
22 KiB
C++
509 lines
22 KiB
C++
#define DEFAULT_LOGGER "betting"
|
|
#include <graphene/chain/database.hpp>
|
|
#include <graphene/chain/account_object.hpp>
|
|
#include <graphene/chain/asset_object.hpp>
|
|
#include <graphene/chain/betting_market_object.hpp>
|
|
#include <graphene/chain/event_object.hpp>
|
|
|
|
#include <boost/range/iterator_range.hpp>
|
|
#include <boost/range/combine.hpp>
|
|
#include <boost/tuple/tuple.hpp>
|
|
|
|
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);
|
|
//idump((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<bet_object_index>().indices().get<by_odds>();
|
|
|
|
// 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<betting_market_id_type, betting_market_resolution_type>& resolutions)
|
|
{
|
|
auto& betting_market_index = get_index_type<betting_market_object_index>().indices().get<by_betting_market_group_id>();
|
|
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<betting_market_object_index>().indices().get<by_betting_market_group_id>();
|
|
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<betting_market_id_type, betting_market_resolution_type>& resolutions)
|
|
{
|
|
auto& betting_market_index = get_index_type<betting_market_object_index>().indices().get<by_betting_market_group_id>();
|
|
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)
|
|
{
|
|
ilog("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<account_id_type> 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<betting_market_id_type, betting_market_resolution_type> resolutions_by_market_id;
|
|
|
|
// collecting bettors and their positions
|
|
std::map<account_id_type, std::vector<const betting_market_position_object*> > bettor_positions_map;
|
|
|
|
auto& betting_market_index = get_index_type<betting_market_object_index>().indices().get<by_betting_market_group_id>();
|
|
// [ROL] it seems to be my mistake - wrong index used
|
|
//auto& position_index = get_index_type<betting_market_position_index>().indices().get<by_bettor_betting_market>();
|
|
auto& position_index = get_index_type<betting_market_position_index>().indices().get<by_betting_market_bettor>();
|
|
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<const betting_market_position_object*>& 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]
|
|
//idump((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;
|
|
dlog("removing betting market ${id}", ("id", betting_market.id));
|
|
remove(betting_market);
|
|
}
|
|
|
|
const event_object& event = betting_market_group.event_id(*this);
|
|
|
|
dlog("removing betting market group ${id}", ("id", betting_market_group.id));
|
|
remove(betting_market_group);
|
|
|
|
if (event.get_status() == event_status::canceled ||
|
|
event.get_status() == event_status::settled) {
|
|
dlog("removing 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<betting_market_position_index>().indices().get<by_bettor_betting_market>();
|
|
auto itr = index.find(boost::make_tuple(bettor_id, betting_market_id));
|
|
if (itr == index.end())
|
|
{
|
|
db.create<betting_market_position_object>([&](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);
|
|
//edump((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 )
|
|
{
|
|
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;
|
|
//idump((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;
|
|
//idump((back_odds_ratio)(lay_odds_ratio));
|
|
//idump((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_taker_factor = taker_bet.amount_to_bet.amount / taker_odds_ratio;
|
|
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;
|
|
//idump((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
|
|
|
|
//idump((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 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)
|
|
{
|
|
const auto& bet_odds_idx = get_index_type<bet_object_index>().indices().get<by_odds>();
|
|
|
|
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));
|
|
|
|
// ilog("");
|
|
// ilog("------------ order book ------------------");
|
|
// for (auto itr = book_itr; itr != book_end; ++itr)
|
|
// idump((*itr));
|
|
// ilog("------------ 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))
|
|
{
|
|
// if the new (taker) bet was not completely consumed, we need to put whatever remains
|
|
// of it on the books. But we only allow bets that can be exactly matched
|
|
// on the books, so round the amount down if necessary
|
|
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;
|
|
//idump((new_bet_object.amount_to_bet.amount)(rounded_bet_amount)(minimum_matchable_amount)(scale_factor));
|
|
|
|
if (rounded_bet_amount == share_type())
|
|
{
|
|
// the remainder of the bet was too small to match, 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);
|
|
// idump((bet_adjusted_op)(new_bet_object));
|
|
push_applied_operation(std::move(bet_adjusted_op));
|
|
return false;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
else
|
|
return true;
|
|
}
|
|
|
|
} }
|