diff --git a/contracts/CMakeLists.txt b/contracts/CMakeLists.txt index d069f53e88262c7d2f6411f4dcbf671ea65b310c..7035647fe4775a33e0752d6550899a16359647f9 100644 --- a/contracts/CMakeLists.txt +++ b/contracts/CMakeLists.txt @@ -29,6 +29,7 @@ add_subdirectory(test_api_multi_index) #add_subdirectory(social) add_subdirectory(eosio.bios) add_subdirectory(noop) +add_subdirectory(dice) file(GLOB SKELETONS RELATIVE ${CMAKE_SOURCE_DIR}/contracts "skeleton/*") @@ -43,6 +44,7 @@ add_custom_command(OUTPUT share/eosio/skeleton/skeleton.cpp add_custom_target(copy_skeleton_contract ALL DEPENDS share/eosio/skeleton/skeleton.cpp) install(DIRECTORY eosiolib DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}) +install(DIRECTORY eosio.system DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}) install(DIRECTORY musl DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}) install(DIRECTORY libc++ DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}) install(DIRECTORY skeleton DESTINATION ${CMAKE_INSTALL_FULL_DATAROOTDIR}/eosio) diff --git a/contracts/dice/CMakeLists.txt b/contracts/dice/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..3caf729a2d5824497c30407553f5ab58167ee670 --- /dev/null +++ b/contracts/dice/CMakeLists.txt @@ -0,0 +1,8 @@ +file(GLOB ABI_FILES "*.abi") +configure_file("${ABI_FILES}" "${CMAKE_CURRENT_BINARY_DIR}" COPYONLY) + +add_wast_executable(TARGET dice + INCLUDE_FOLDERS "${STANDARD_INCLUDE_FOLDERS}" + LIBRARIES libc++ libc eosiolib + DESTINATION_FOLDER ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/contracts/dice/dice.abi b/contracts/dice/dice.abi new file mode 100644 index 0000000000000000000000000000000000000000..9cdbbae575e83a9fc6698f1596ee574cc383ed17 --- /dev/null +++ b/contracts/dice/dice.abi @@ -0,0 +1,210 @@ +{ + "____comment": "This file was generated by eosio-abigen. DO NOT EDIT - 2018-03-29T02:09:11", + "types": [], + "structs": [{ + "name": "offer", + "base": "", + "fields": [{ + "name": "id", + "type": "uint64" + },{ + "name": "owner", + "type": "account_name" + },{ + "name": "bet", + "type": "asset" + },{ + "name": "commitment", + "type": "checksum256" + },{ + "name": "gameid", + "type": "uint64" + } + ] + },{ + "name": "player", + "base": "", + "fields": [{ + "name": "commitment", + "type": "checksum256" + },{ + "name": "reveal", + "type": "checksum256" + } + ] + },{ + "name": "game", + "base": "", + "fields": [{ + "name": "id", + "type": "uint64" + },{ + "name": "bet", + "type": "asset" + },{ + "name": "deadline", + "type": "time" + },{ + "name": "player1", + "type": "player" + },{ + "name": "player2", + "type": "player" + } + ] + },{ + "name": "global_dice", + "base": "", + "fields": [{ + "name": "id", + "type": "uint64" + },{ + "name": "nextgameid", + "type": "uint64" + } + ] + },{ + "name": "account", + "base": "", + "fields": [{ + "name": "owner", + "type": "account_name" + },{ + "name": "eos_balance", + "type": "asset" + },{ + "name": "open_offers", + "type": "uint32" + },{ + "name": "open_games", + "type": "uint32" + } + ] + },{ + "name": "offerbet", + "base": "", + "fields": [{ + "name": "bet", + "type": "asset" + },{ + "name": "player", + "type": "account_name" + },{ + "name": "commitment", + "type": "checksum256" + } + ] + },{ + "name": "canceloffer", + "base": "", + "fields": [{ + "name": "commitment", + "type": "checksum256" + } + ] + },{ + "name": "reveal", + "base": "", + "fields": [{ + "name": "commitment", + "type": "checksum256" + },{ + "name": "source", + "type": "checksum256" + } + ] + },{ + "name": "claimexpired", + "base": "", + "fields": [{ + "name": "gameid", + "type": "uint64" + } + ] + },{ + "name": "deposit", + "base": "", + "fields": [{ + "name": "from", + "type": "account_name" + },{ + "name": "a", + "type": "asset" + } + ] + },{ + "name": "withdraw", + "base": "", + "fields": [{ + "name": "to", + "type": "account_name" + },{ + "name": "a", + "type": "asset" + } + ] + } + ], + "actions": [{ + "name": "offerbet", + "type": "offerbet" + },{ + "name": "canceloffer", + "type": "canceloffer" + },{ + "name": "reveal", + "type": "reveal" + },{ + "name": "claimexpired", + "type": "claimexpired" + },{ + "name": "deposit", + "type": "deposit" + },{ + "name": "withdraw", + "type": "withdraw" + } + ], + "tables": [{ + "name": "offer", + "index_type": "i64", + "key_names": [ + "id" + ], + "key_types": [ + "uint64" + ], + "type": "offer" + },{ + "name": "game", + "index_type": "i64", + "key_names": [ + "id" + ], + "key_types": [ + "uint64" + ], + "type": "game" + },{ + "name": "global", + "index_type": "i64", + "key_names": [ + "id" + ], + "key_types": [ + "uint64" + ], + "type": "global_dice" + },{ + "name": "account", + "index_type": "i64", + "key_names": [ + "owner" + ], + "key_types": [ + "account_name" + ], + "type": "account" + } + ] +} \ No newline at end of file diff --git a/contracts/dice/dice.cpp b/contracts/dice/dice.cpp index 7e75ce7651ecf3b1c8a9a45ff93a928e315a2326..6665d939bd57614f0ccef08b0f39ac39eabd44d5 100644 --- a/contracts/dice/dice.cpp +++ b/contracts/dice/dice.cpp @@ -3,80 +3,375 @@ * @copyright defined in eos/LICENSE.txt */ -namespace dice { - -void apply_offer( const offer_bet& offer ) { - eosio_assert( offer.amount > 0, "insufficient bet" ); - eosio_assert( !hasOffer( offer.commitment ), "offer with this commitment already exist" ); - require_auth( offer.player ); - - auto acnt = get_account( offer.player ); - acnt.balance -= offer.amount; - acnt.open_offers++; - save( acnt ); - - /** - * 1. Lookup lowerbound on offer's by offer_primary_key - * if lowerbound.primary.bet == offer.bet - * global_dice.nextgameid++ (load and save to DB) - * create new game and set game.bet == offer.bet - * set player1 == lowerbound player - * set player2 == offer.player - * update lowerbound.primary.bet = 0 and lwoerbound.gameid = global_dice.nextgameid - * Create offer entry in offers table with bet = 0 and gameid == --^ - * else - * Create offer entry in offers table with bet = offer.bet and gameid = 0 - */ -} - -void apply_cancel( const cancel_offer& offer ) { - /** - * Lookup offer in offer's table and assert that gameid == 0 - * Lookup account and increment balance by bet - * Deelte offer from offers table - */ -} +#include +#include +#include +#include +#include +#include +#include +#include +#include -/** - * No authority required on this message so long as it is properly revealed - */ -void apply_reveal( const reveal& offer ) { - assert_sha256( &offer.source, sizeof(offer.source), &offer.commitment ); - - /** - * assert offer already exists - * assert offer has a gameid > 0 - * - * Lookup game by gameid - * assert game.player[x].reveal is 0 hasn't already been revealed - * set game.player[x].reveal to revealed - * - * if( game.player[!x].reveal != 0 ) { - * uint256_t result; - * sha256( &game.player1, sizeof(player)*2, result); - * if( result.words[1] < result.words[0] ) { - * pay_player1 by incrementing account by bet and decrement open games - * } - * else - * pay_player2 by incrmenting account by bet and decrement open games - * delete game, and both offers - * } - * else { - * set game.deadline = now() + timeout (5 minutes) - * } - * - */ -} +#include -/** - * No authority required by this so long as - */ -void apply_claim( const claim_expired& claim ) { - /// lookup game by id, assert now() > deadline and deadline != 0 - /// pay the player that revealed - /// delete game and open offers -} +using eos_currency = eosiosystem::contract::currency; + +using eosio::key256; +using eosio::indexed_by; +using eosio::const_mem_fun; +using eosio::asset; + +class dice : public eosio::contract { + public: + const uint32_t FIVE_MINUTES = 5*60; + + dice(account_name self) + :eosio::contract(self), + offers(_self, _self), + games(_self, _self), + global_dices(_self, _self), + accounts(_self, _self) + {} + + //@abi action + void offerbet(const asset& bet, const account_name player, const checksum256& commitment) { + + auto amount = eos_currency::token_type(bet); + + eosio_assert( amount.quantity > 0, "invalid bet" ); + eosio_assert( !has_offer( commitment ), "offer with this commitment already exist" ); + require_auth( player ); + + auto cur_player_itr = accounts.find( player ); + eosio_assert(cur_player_itr != accounts.end(), "unknown account"); + + // Store new offer + auto new_offer_itr = offers.emplace(_self, [&](auto& offer){ + offer.id = offers.available_primary_key(); + offer.bet = bet; + offer.owner = player; + offer.commitment = commitment; + offer.gameid = 0; + }); + + // Try to find a matching bet + auto idx = offers.template get_index(); + auto matched_offer_itr = idx.lower_bound( (uint64_t)new_offer_itr->bet.amount ); + + if( matched_offer_itr == idx.end() + || matched_offer_itr->bet.amount != new_offer_itr->bet.amount + || matched_offer_itr->owner == new_offer_itr->owner ) { + + // No matching bet found, update player's account + accounts.modify( cur_player_itr, 0, [&](auto& acnt) { + acnt.eos_balance = (asset)(eos_currency::token_type(acnt.eos_balance) - amount); + acnt.open_offers++; + }); + + } else { + // Create global game counter if not exists + auto gdice_itr = global_dices.begin(); + if( gdice_itr == global_dices.end() ) { + gdice_itr = global_dices.emplace(_self, [&](auto& gdice){ + gdice.nextgameid=0; + }); + } + + // Increment global game counter + global_dices.modify(gdice_itr, 0, [&](auto& gdice){ + gdice.nextgameid++; + }); + + // Create a new game + auto game_itr = games.emplace(_self, [&](auto& new_game){ + new_game.id = gdice_itr->nextgameid; + new_game.bet = new_offer_itr->bet; + new_game.deadline = 0; + + new_game.player1.commitment = matched_offer_itr->commitment; + memset(&new_game.player1.reveal, 0, sizeof(checksum256)); + + new_game.player2.commitment = new_offer_itr->commitment; + memset(&new_game.player2.reveal, 0, sizeof(checksum256)); + }); + + // Update player's offers + idx.modify(matched_offer_itr, 0, [&](auto& offer){ + offer.bet.amount = 0; + offer.gameid = game_itr->id; + }); + + offers.modify(new_offer_itr, 0, [&](auto& offer){ + offer.bet.amount = 0; + offer.gameid = game_itr->id; + }); + + // Update player's accounts + accounts.modify( accounts.find( matched_offer_itr->owner ), 0, [&](auto& acnt) { + acnt.open_offers--; + acnt.open_games++; + }); + + accounts.modify( cur_player_itr, 0, [&](auto& acnt) { + acnt.eos_balance = (asset)(eos_currency::token_type(acnt.eos_balance) - amount); + acnt.open_games++; + }); + } + } + + //@abi action + void canceloffer( const checksum256& commitment ) { + + auto idx = offers.template get_index(); + auto offer_itr = idx.find( offer::get_commitment(commitment) ); + + eosio_assert( offer_itr != idx.end(), "offer does not exists" ); + eosio_assert( offer_itr->gameid == 0, "unable to cancel offer" ); + require_auth( offer_itr->owner ); + + auto acnt_itr = accounts.find(offer_itr->owner); + accounts.modify(acnt_itr, 0, [&](auto& acnt){ + acnt.open_offers--; + acnt.eos_balance.amount += offer_itr->bet.amount; + }); + + idx.erase(offer_itr); + } + + //@abi action + void reveal( const checksum256& commitment, const checksum256& source ) { + + assert_sha256( (char *)&source, sizeof(source), (const checksum256 *)&commitment ); + + auto idx = offers.template get_index(); + auto curr_revealer_offer = idx.find( offer::get_commitment(commitment) ); + + eosio_assert(curr_revealer_offer != idx.end(), "offer not found"); + eosio_assert(curr_revealer_offer->gameid > 0, "unable to reveal"); + + auto game_itr = games.find( curr_revealer_offer->gameid ); + + player curr_reveal = game_itr->player1; + player prev_reveal = game_itr->player2; + + if( !is_equal(curr_reveal.commitment, commitment) ) { + std::swap(curr_reveal, prev_reveal); + } + + eosio_assert( is_zero(curr_reveal.reveal) == true, "player already revealed"); + + if( !is_zero(prev_reveal.reveal) ) { + + checksum256 result; + sha256( (char *)&game_itr->player1, sizeof(player)*2, &result); + + auto prev_revealer_offer = idx.find( offer::get_commitment(prev_reveal.commitment) ); + + int winner = result.hash[1] < result.hash[0] ? 0 : 1; + + if( winner ) { + pay_and_clean(*game_itr, *curr_revealer_offer, *prev_revealer_offer); + } else { + pay_and_clean(*game_itr, *prev_revealer_offer, *curr_revealer_offer); + } + + } else { + games.modify(game_itr, 0, [&](auto& game){ + + if( is_equal(curr_reveal.commitment, game.player1.commitment) ) + game.player1.reveal = source; + else + game.player2.reveal = source; + + game.deadline = now() + FIVE_MINUTES; + }); + } + } + + //@abi action + void claimexpired( const uint64_t gameid ) { + + auto game_itr = games.find(gameid); + + eosio_assert(game_itr != games.end(), "game not found"); + eosio_assert(game_itr->deadline != 0 && now() > game_itr->deadline, "game not expired"); + + auto idx = offers.template get_index(); + auto player1_offer = idx.find( offer::get_commitment(game_itr->player1.commitment) ); + auto player2_offer = idx.find( offer::get_commitment(game_itr->player2.commitment) ); + + if( !is_zero(game_itr->player1.reveal) ) { + eosio_assert( is_zero(game_itr->player2.reveal), "game error"); + pay_and_clean(*game_itr, *player1_offer, *player2_offer); + } else { + eosio_assert( is_zero(game_itr->player1.reveal), "game error"); + pay_and_clean(*game_itr, *player2_offer, *player1_offer); + } + + } + + //@abi action + void deposit( const account_name from, const asset& a ) { + + auto itr = accounts.find(from); + if( itr == accounts.end() ) { + itr = accounts.emplace(_self, [&](auto& acnt){ + acnt.owner = from; + }); + } + + auto amount = eos_currency::token_type(a); + + eos_currency::inline_transfer( from, _self, amount ); + accounts.modify( itr, 0, [&]( auto& acnt ) { + acnt.eos_balance = (asset)(eos_currency::token_type(acnt.eos_balance) + amount); + }); + } + + //@abi action + void withdraw( const account_name to, const asset& a ) { + require_auth( to ); + + auto itr = accounts.find( to ); + eosio_assert(itr != accounts.end(), "unknown account"); + + auto amount = eos_currency::token_type(a); + accounts.modify( itr, 0, [&]( auto& acnt ) { + acnt.eos_balance = (asset)(eos_currency::token_type(acnt.eos_balance) - amount); + }); + + eos_currency::inline_transfer( _self, to, amount ); + + if( itr->is_empty() ) { + accounts.erase(itr); + } + } + + private: + //@abi table offer i64 + struct offer { + uint64_t id; + account_name owner; + asset bet; + checksum256 commitment; + uint64_t gameid = 0; + + uint64_t primary_key()const { return id; } + + uint64_t by_bet()const { return (uint64_t)bet.amount; } + + key256 by_commitment()const { return get_commitment(commitment); } + + static key256 get_commitment(const checksum256& commitment) { + const uint64_t *p64 = reinterpret_cast(&commitment); + return key256::make_from_word_sequence(p64[0], p64[1], p64[2], p64[3]); + } + + EOSLIB_SERIALIZE( offer, (id)(owner)(bet)(commitment)(gameid) ) + }; + + typedef eosio::multi_index< N(offer), offer, + indexed_by< N(bet), const_mem_fun >, + indexed_by< N(commitment), const_mem_fun > + > offer_index; + + struct player { + checksum256 commitment; + checksum256 reveal; + + EOSLIB_SERIALIZE( player, (commitment)(reveal) ) + }; + + //@abi table game i64 + struct game { + uint64_t id; + asset bet; + time deadline; + player player1; + player player2; + + uint64_t primary_key()const { return id; } + + EOSLIB_SERIALIZE( game, (id)(bet)(deadline)(player1)(player2) ) + }; + + typedef eosio::multi_index< N(game), game> game_index; + + //@abi table global i64 + struct global_dice { + uint64_t id = 0; + uint64_t nextgameid = 0; + + uint64_t primary_key()const { return id; } + + EOSLIB_SERIALIZE( global_dice, (id)(nextgameid) ) + }; + + typedef eosio::multi_index< N(global), global_dice> global_dice_index; + + //@abi table account i64 + struct account { + account( account_name o = account_name() ):owner(o){} + + account_name owner; + asset eos_balance; + uint32_t open_offers = 0; + uint32_t open_games = 0; + + bool is_empty()const { return !( eos_balance.amount | open_offers | open_games ); } + + uint64_t primary_key()const { return owner; } + + EOSLIB_SERIALIZE( account, (owner)(eos_balance)(open_offers)(open_games) ) + }; + + typedef eosio::multi_index< N(account), account> account_index; + + offer_index offers; + game_index games; + global_dice_index global_dices; + account_index accounts; + + bool has_offer( const checksum256& commitment )const { + auto idx = offers.template get_index(); + auto itr = idx.find( offer::get_commitment(commitment) ); + return itr != idx.end(); + } + + bool is_equal(const checksum256& a, const checksum256& b)const { + return memcmp((void *)&a, (const void *)&b, sizeof(checksum256)) == 0; + } + + bool is_zero(const checksum256& a)const { + const uint64_t *p64 = reinterpret_cast(&a); + return p64[0] == 0 && p64[1] == 0 && p64[2] == 0 && p64[3] == 0; + } + + void pay_and_clean(const game& g, const offer& winner_offer, + const offer& loser_offer) { + + // Update winner account balance and game count + auto winner_account = accounts.find(winner_offer.owner); + accounts.modify( winner_account, 0, [&]( auto& acnt ) { + acnt.eos_balance.amount += 2*g.bet.amount; + acnt.open_games--; + }); + + // Update losser account game count + auto loser_account = accounts.find(loser_offer.owner); + accounts.modify( loser_account, 0, [&]( auto& acnt ) { + acnt.open_games--; + }); + if( loser_account->is_empty() ) { + accounts.erase(loser_account); + } -} + games.erase(g); + offers.erase(winner_offer); + offers.erase(loser_offer); + } +}; +EOSIO_ABI( dice, (offerbet)(canceloffer)(reveal)(claimexpired)(deposit)(withdraw) ) diff --git a/contracts/dice/dice.hpp b/contracts/dice/dice.hpp deleted file mode 100644 index 8527a10e5f297454b996cf163be3db27345b9266..0000000000000000000000000000000000000000 --- a/contracts/dice/dice.hpp +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @file - * @copyright defined in eos/LICENSE.txt - */ - -namespace dice { - - struct offer_bet { - eosio::tokens amount; - account_name player; - uint256 commitment; - }; - - struct cancel_offer { - uint256 commitment; - }; - - struct reveal { - uint256 commitment; - uint256 source; - }; - - struct claim_expired { - uint64_t gameid = 0; - }; - - using eos_tokens = eosio::tokens; - - struct offer_primary_key { - eos_tokens bet; - account_name owner; - }; - - /** - * This struct is used with i128xi128 index which allows us to lookup and - * iterate over offers by either {bet,owner,commitment} or {commitment,bet,owner} - * - * Only the first 128 bits of commitment ID are used in the index, but there will - * be a requirement that these first 128 bits be unique and any collisions will - * be rejected. - * - * If there is no existing offer of equal bet size then gameid is set as 0, if it is - * matched then a new game record is created and the offer is modified so that bet - * goes to 0 (and therefore moves to end of queue and not matched with other bets) and - * the gameid of both offers are updated. - */ - struct PACKED( offer ) { - offer_primary_key primary; - uint256 commitment; - uint32_t gameid = 0; - }; - - struct player { - uint256 commitment; - uint256 reveal; - }; - - /** - * - */ - struct PACKED( game ) { - uint32_t gameid; - eos_tokens bet; - time deadline; - player player1; - player player2; - }; - - struct Packed( global_dice ) { - uint32_t nextgameid = 0; - }; - - - struct PACKED( account ) { - account( account_name o = account_name() ):owner(o){} - - account_name owner; - eos_tokens eos_balance; - uint32_t open_offers = 0; - - bool is_empty()const { return ! ( bool(eos_balance) | open_offers); } - }; - - using accounts = eosio::table; - using global_dices = eosio::table; - using offers = eosio::table; - - inline account get_account( account_name owner ) { - account owned_account(owner); - accounts::get( owned_account ); - return owned_account; - } - -}; diff --git a/contracts/eosio.token/eosio.token.abi b/contracts/eosio.token/eosio.token.abi index 60d5f88d851f743a5b20ccb322eb18c50298070f..13c3e0102cd4343ac0f807e7c9e8150298cdf7ad 100644 --- a/contracts/eosio.token/eosio.token.abi +++ b/contracts/eosio.token/eosio.token.abi @@ -1,9 +1,5 @@ { - "types": [{ - "new_type_name": "account_name", - "type": "name" - } - ], + "types": [], "structs": [{ "name": "transfer", "base": "", diff --git a/contracts/eosiolib/dispatcher.hpp b/contracts/eosiolib/dispatcher.hpp index 7e8c6d0d686fef108804aac432383cc598d499d5..fb4cf7891bffb577e5583fb5ab1adc67759cecb3 100644 --- a/contracts/eosiolib/dispatcher.hpp +++ b/contracts/eosiolib/dispatcher.hpp @@ -37,18 +37,16 @@ namespace eosio { return eosio::dispatch( code, act ); } - - template bool execute_action( T* obj, void (T::*func)(Args...) ) { char buffer[action_data_size()]; read_action_data( buffer, sizeof(buffer) ); - auto args = unpack>( buffer, sizeof(buffer) ); + + auto args = unpack...>>( buffer, sizeof(buffer) ); auto f2 = [&]( auto... a ){ (obj->*func)( a... ); }; -// apply( obj, func, args ); boost::mp11::tuple_apply( f2, args ); return true; diff --git a/libraries/abi_generator/abi_generator.cpp b/libraries/abi_generator/abi_generator.cpp index 149ed505fe35a1ac26d229ab26eeed372c1fa6f2..35466a8fff1e5259fcd4082168ac9f10286dbe83 100644 --- a/libraries/abi_generator/abi_generator.cpp +++ b/libraries/abi_generator/abi_generator.cpp @@ -68,7 +68,7 @@ string abi_generator::translate_type(const string& type_name) { else if (type_name == "unsigned long" || type_name == "uint32_t") built_in_type = "uint32"; else if (type_name == "unsigned short" || type_name == "uint16_t") built_in_type = "uint16"; else if (type_name == "unsigned char" || type_name == "uint8_t") built_in_type = "uint8"; - + else if (type_name == "long long" || type_name == "int64_t") built_in_type = "int64"; else if (type_name == "long" || type_name == "int32_t") built_in_type = "int32"; else if (type_name == "short" || type_name == "int16_t") built_in_type = "int16"; @@ -77,23 +77,89 @@ string abi_generator::translate_type(const string& type_name) { return built_in_type; } +bool abi_generator::inspect_type_methods_for_actions(const Decl* decl) { try { + + const auto* rec_decl = dyn_cast(decl); + if(rec_decl == nullptr) return false; + + const auto* type = rec_decl->getTypeForDecl(); + ABI_ASSERT(type != nullptr); + if( !type->isStructureOrClassType() ) { + return false; + } + + bool at_least_one_action = false; + for(const auto* method : rec_decl->methods()) { + + const RawComment* raw_comment = ast_context->getRawCommentForDeclNoCache(method); + if( raw_comment == nullptr) continue; + + SourceManager& source_manager = ast_context->getSourceManager(); + string raw_text = raw_comment->getRawText(source_manager); + regex r(R"(@abi (action))"); + + smatch smatch; + if(regex_search(raw_text, smatch, r)){ + + auto method_name = method->getNameAsString(); + ABI_ASSERT(find_struct(method_name) == nullptr, "action already exists ${method_name}", ("method_name",method_name)); + + struct_def abi_struct; + for(const auto* p : method->parameters() ) { + clang::QualType qt = p->getOriginalType().getNonReferenceType(); + qt.setLocalFastQualifiers(0); + + string field_name = p->getNameAsString(); + string field_type_name = add_type(qt); + + field_def struct_field{field_name, field_type_name}; + ABI_ASSERT(is_builtin_type(get_vector_element_type(struct_field.type)) + || find_struct(get_vector_element_type(struct_field.type)) + || find_type(get_vector_element_type(struct_field.type)) + , "Unknown type ${type} [${abi}]",("type",struct_field.type)("abi",*output)); + + type_size[string(struct_field.type)] = is_vector(struct_field.type) ? 0 : ast_context->getTypeSize(qt); + abi_struct.fields.push_back(struct_field); + } + + abi_struct.name = method_name; + abi_struct.base = ""; + + output->structs.push_back(abi_struct); + + full_types[method_name] = method_name; + + output->actions.push_back({method_name, method_name}); + at_least_one_action = true; + } + + } + + return at_least_one_action; + +} FC_CAPTURE_AND_RETHROW() } + void abi_generator::handle_decl(const Decl* decl) { try { ABI_ASSERT(decl != nullptr); ABI_ASSERT(output != nullptr); ABI_ASSERT(ast_context != nullptr); - //ASTContext& ctx = decl->getASTContext(); - const RawComment* raw_comment = ast_context->getRawCommentForDeclNoCache(decl); - - if(!raw_comment) return; - SourceManager& source_manager = ast_context->getSourceManager(); - auto file_name = source_manager.getFilename(raw_comment->getLocStart()); + auto file_name = source_manager.getFilename(decl->getLocStart()); if ( !abi_context.empty() && !file_name.startswith(abi_context) ) { return; } + if(inspect_type_methods_for_actions(decl)) { + return; + } + + const RawComment* raw_comment = ast_context->getRawCommentForDeclNoCache(decl); + if(raw_comment == nullptr) { + return; + } + string raw_text = raw_comment->getRawText(source_manager); regex r(R"(@abi (action|table)((?: [a-z0-9]+)*))"); @@ -103,7 +169,7 @@ void abi_generator::handle_decl(const Decl* decl) { try { if(smatch.size() == 3) { auto type = smatch[1].str(); - + vector params; auto string_params = smatch[2].str(); boost::trim(string_params); @@ -116,7 +182,7 @@ void abi_generator::handle_decl(const Decl* decl) { try { ABI_ASSERT(action_decl != nullptr); auto qt = action_decl->getTypeForDecl()->getCanonicalTypeInternal(); - + auto type_name = add_struct(qt); ABI_ASSERT(!is_builtin_type(type_name), "A built-in type with the same name exists, try using another name: ${type_name}", ("type_name",type_name)); @@ -151,7 +217,7 @@ void abi_generator::handle_decl(const Decl* decl) { try { table_def table; table.name = boost::algorithm::to_lower_copy(boost::erase_all_copy(type_name, "_")); table.type = type_name; - + if(params.size() >= 1) { table.name = params[0]; } @@ -173,9 +239,9 @@ void abi_generator::handle_decl(const Decl* decl) { try { } } } - + raw_text = smatch.suffix(); - } + } } FC_CAPTURE_AND_RETHROW() } @@ -193,11 +259,11 @@ bool abi_generator::is_string(const string& type) { void abi_generator::get_all_fields(const struct_def& s, vector& fields) { abi_serializer abis(*output); - + for(const auto& field : s.fields) { fields.push_back(field); } - + if(s.base.size()) { const auto* base = find_struct(s.base); ABI_ASSERT(base, "Unable to find base type ${type}",("type",s.base)); @@ -246,7 +312,7 @@ void abi_generator::guess_key_names(table_def& table, const struct_def s) { if( table.index_type == "i64i64i64" || table.index_type == "i128i128" || table.index_type == "i64") { - + table.key_names.clear(); table.key_types.clear(); @@ -342,7 +408,7 @@ bool abi_generator::is_elaborated(const clang::QualType& qt) { } bool abi_generator::is_vector(const clang::QualType& vqt) { - + QualType qt(vqt); if ( is_elaborated(qt) ) @@ -404,7 +470,7 @@ clang::QualType abi_generator::add_typedef(const clang::QualType& tqt) { abi_typedef.new_type_name = new_type_name; abi_typedef.type = translate_type(underlying_type_name); const auto* td = find_type(abi_typedef.new_type_name); - + if(!td && !is_struct_specialization(underlying_type) ) { output->types.push_back(abi_typedef); } else { @@ -424,7 +490,7 @@ clang::CXXRecordDecl::base_class_range abi_generator::get_struct_bases(const cla const auto* td_decl = qt->getAs()->getDecl(); qt = td_decl->getUnderlyingType().getUnqualifiedType(); } - + const auto* record_type = qt->getAs(); ABI_ASSERT(record_type != nullptr); auto cxxrecord_decl = clang::dyn_cast(record_type->getDecl()); @@ -455,7 +521,7 @@ string abi_generator::add_vector(const clang::QualType& vqt) { auto vector_element_type = get_vector_element_type(qt); ABI_ASSERT(!is_vector(vector_element_type), "Only one-dimensional arrays are supported"); - + add_type(vector_element_type); auto vector_element_type_str = translate_type(get_type_name(vector_element_type)); @@ -492,7 +558,7 @@ string abi_generator::add_type(const clang::QualType& tqt) { if( is_struct(qt) ) { return add_struct(qt, full_type_name); } - + ABI_ASSERT(false, "types can only be: vector, struct, class or a built-in type. (${type}) ", ("type",get_type_name(qt))); return type_name; } @@ -513,7 +579,7 @@ string abi_generator::add_struct(const clang::QualType& sqt, string full_name) { } auto name = remove_namespace(full_name); - + ABI_ASSERT(is_struct(qt), "Only struct and class are supported. ${full_name}",("full_name",full_name)); if( find_struct(name) ) { @@ -543,7 +609,7 @@ string abi_generator::add_struct(const clang::QualType& sqt, string full_name) { struct_def abi_struct; for (const clang::FieldDecl* field : get_struct_fields(qt) ) { clang::QualType qt = field->getType(); - + string field_name = field->getNameAsString(); string field_type_name = add_type(qt); @@ -561,7 +627,7 @@ string abi_generator::add_struct(const clang::QualType& sqt, string full_name) { abi_struct.base = base_name; output->structs.push_back(abi_struct); - + full_types[name] = full_name; return name; } diff --git a/libraries/abi_generator/include/eosio/abi_generator/abi_generator.hpp b/libraries/abi_generator/include/eosio/abi_generator/abi_generator.hpp index 1fd4d1868dab10dad548db2d8ec2dd4190a05031..4949c8cc506cb4854946d08d11acfb376b72e6b0 100644 --- a/libraries/abi_generator/include/eosio/abi_generator/abi_generator.hpp +++ b/libraries/abi_generator/include/eosio/abi_generator/abi_generator.hpp @@ -124,6 +124,7 @@ namespace eosio { void handle_tagdecl_definition(TagDecl* tag_decl); private: + bool inspect_type_methods_for_actions(const Decl* decl); string remove_namespace(const string& full_name); diff --git a/programs/eosio-abigen/main.cpp b/programs/eosio-abigen/main.cpp index f45747fc1947ce46bb96fb99071a38b32df6ba96..02116bb054249ba05351f8cef4a6233f9820042b 100644 --- a/programs/eosio-abigen/main.cpp +++ b/programs/eosio-abigen/main.cpp @@ -1,10 +1,13 @@ #include #include #include +#include using namespace eosio; using namespace eosio::chain::contracts; +using mvo = fc::mutable_variant_object; + std::unique_ptr create_factory(bool verbose, bool opt_sfs, string abi_context, abi_def& output) { class abi_frontend_action_factory : public FrontendActionFactory { bool verbose; @@ -49,7 +52,17 @@ int main(int argc, const char **argv) { abi_def output; try { int result = Tool.run(create_factory(abi_verbose, abi_opt_sfs, abi_context, output).get()); if(!result) { abi_serializer(output).validate(); - fc::json::save_to_file(output, abi_destination, true); + + fc::variant vabi; + to_variant(output, vabi); + + auto comment = fc::format_string( + "This file was generated by eosio-abigen. DO NOT EDIT - ${ts}", + mvo("ts",fc::time_point_sec(fc::time_point::now()).to_iso_string())); + + auto abi_with_comment = mvo("____comment", comment)(mvo(vabi)); + + fc::json::save_to_file(abi_with_comment, abi_destination, true); } return result; } FC_CAPTURE_AND_LOG((output)); return -1; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ac33497eccfe65f71a57dcff36b1baa949ed5961..ac468165878ce4a5cdf468e56453b9bf12a81493 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -28,7 +28,7 @@ target_include_directories( chain_test PUBLIC ${CMAKE_BINARY_DIR}/contracts ${CM target_include_directories( chain_test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/wasm_tests ) target_include_directories( chain_test PUBLIC ${CMAKE_SOURCE_DIR}/plugins/net_plugin/include ) target_include_directories( chain_test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) -add_dependencies(chain_test asserter test_api test_api_mem test_api_db test_api_multi_index exchange currency proxy identity identity_test stltest infinite eosio.system eosio.token eosio.bios test.inline multi_index_test noop ) +add_dependencies(chain_test asserter test_api test_api_mem test_api_db test_api_multi_index exchange currency proxy identity identity_test stltest infinite eosio.system eosio.token eosio.bios test.inline multi_index_test noop dice) # configure_file(${CMAKE_CURRENT_SOURCE_DIR}/p2p_tests/sync/test.sh ${CMAKE_CURRENT_BINARY_DIR}/p2p_tests/sync/test.sh COPYONLY) diff --git a/tests/wasm_tests/dice_tests.cpp b/tests/wasm_tests/dice_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3464fe624b650dc6183ca5ea6940f913372b6fd4 --- /dev/null +++ b/tests/wasm_tests/dice_tests.cpp @@ -0,0 +1,429 @@ +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +using namespace eosio; +using namespace eosio::chain; +using namespace eosio::chain::contracts; +using namespace eosio::testing; +using namespace fc; +using namespace std; +using mvo = fc::mutable_variant_object; + +struct offer_bet_t { + asset bet; + account_name player; + checksum256_type commitment; + + static account_name get_account() { return N(dice); } + static action_name get_name() {return N(offerbet); } +}; +FC_REFLECT(offer_bet_t, (bet)(player)(commitment)); + +struct cancel_offer_t { + checksum256_type commitment; + + static account_name get_account() { return N(dice); } + static action_name get_name() {return N(canceloffer); } +}; +FC_REFLECT(cancel_offer_t, (commitment)); + +struct reveal_t { + checksum256_type commitment; + checksum256_type source; + + static account_name get_account() { return N(dice); } + static action_name get_name() {return N(reveal); } +}; +FC_REFLECT(reveal_t, (commitment)(source)); + +struct deposit_t { + account_name from; + asset amount; + + static account_name get_account() { return N(dice); } + static action_name get_name() {return N(deposit); } +}; +FC_REFLECT( deposit_t, (from)(amount) ); + +struct withdraw_t { + account_name to; + asset amount; + + static account_name get_account() { return N(dice); } + static action_name get_name() {return N(withdraw); } +}; +FC_REFLECT( withdraw_t, (to)(amount) ); + +struct __attribute((packed)) account_t { + account_name owner; + asset eos_balance; + uint32_t open_offers; + uint32_t open_games; +}; +FC_REFLECT(account_t, (owner)(eos_balance)(open_offers)(open_games)); + +struct player_t { + checksum_type commitment; + checksum_type reveal; +}; +FC_REFLECT(player_t, (commitment)(reveal)); + +struct __attribute((packed)) game_t { + uint64_t gameid; + asset bet; + fc::time_point_sec deadline; + player_t player1; + player_t player2; +}; +FC_REFLECT(game_t, (gameid)(bet)(deadline)(player1)(player2)); + +struct dice_tester : tester { + + const contracts::table_id_object* find_table( name code, name scope, name table ) { + auto tid = control->get_database().find(boost::make_tuple(code, scope, table)); + return tid; + } + + template + const auto& get_index() { + return control->get_database().get_index(); + } + + void offer_bet(account_name account, asset amount, const checksum_type& commitment) { + signed_transaction trx; + action act( {{account, config::active_name}}, + offer_bet_t{amount, account, commitment} ); + trx.actions.push_back(act); + set_transaction_headers(trx); + trx.sign(get_private_key( account, "active" ), chain_id_type()); + control->push_transaction(packed_transaction(trx,packed_transaction::none)); + } + + void cancel_offer(account_name account, const checksum_type& commitment) { + signed_transaction trx; + action act( {{account, config::active_name}}, + cancel_offer_t{commitment} ); + trx.actions.push_back(act); + set_transaction_headers(trx); + trx.sign(get_private_key( account, "active" ), chain_id_type()); + control->push_transaction(packed_transaction(trx,packed_transaction::none)); + } + + void deposit(account_name account, asset amount) { + signed_transaction trx; + action act( {{account, config::active_name}}, + deposit_t{account, amount} ); + trx.actions.push_back(act); + set_transaction_headers(trx); + trx.sign(get_private_key( account, "active" ), chain_id_type()); + control->push_transaction(packed_transaction(trx,packed_transaction::none)); + } + + void withdraw(account_name account, asset amount) { + signed_transaction trx; + action act( {{account, config::active_name}}, + withdraw_t{account, amount} ); + trx.actions.push_back(act); + set_transaction_headers(trx); + trx.sign(get_private_key( account, "active" ), chain_id_type()); + control->push_transaction(packed_transaction(trx,packed_transaction::none)); + } + + void reveal(account_name account, const checksum_type& commitment, const checksum_type& source ) { + signed_transaction trx; + action act( {{account, config::active_name}}, + reveal_t{commitment, source} ); + trx.actions.push_back(act); + set_transaction_headers(trx); + trx.sign(get_private_key( account, "active" ), chain_id_type()); + control->push_transaction(packed_transaction(trx,packed_transaction::none)); + } + + bool dice_account(account_name account, account_t& acnt) { + auto* maybe_tid = find_table(N(dice), N(dice), N(account)); + if(maybe_tid == nullptr) return false; + + auto* o = control->get_database().find(boost::make_tuple(maybe_tid->id, account)); + if(o == nullptr) { + return false; + } + + fc::raw::unpack(o->value.data(), o->value.size(), acnt); + return true; + } + + bool dice_game(uint64_t game_id, game_t& game) { + auto* maybe_tid = find_table(N(dice), N(dice), N(game)); + if(maybe_tid == nullptr) return false; + + auto* o = control->get_database().find(boost::make_tuple(maybe_tid->id, game_id)); + if(o == nullptr) return false; + + fc::raw::unpack(o->value.data(), o->value.size(), game); + return true; + } + + uint32_t open_games(account_name account) { + account_t acnt; + if(!dice_account(account, acnt)) return 0; + return acnt.open_games; + } + + asset game_bet(uint64_t game_id) { + game_t game; + if(!dice_game(game_id, game)) return asset(); + return game.bet; + } + + uint32_t open_offers(account_name account) { + account_t acnt; + if(!dice_account(account, acnt)) return 0; + return acnt.open_offers; + } + + asset balance_of(account_name account) { + account_t acnt; + if(!dice_account(account, acnt)) return asset(); + return acnt.eos_balance; + } + + checksum_type commitment_for( const char* secret ) { + return commitment_for(checksum_type(secret)); + } + + checksum_type commitment_for( const checksum_type& secret ) { + return fc::sha256::hash( secret.data(), sizeof(secret) ); + } + + void add_dice_authority(account_name account) { + auto auth = authority{ + 1, + { + {.key = get_public_key(account,"active"), .weight = 1} + }, + { + {.permission = {N(dice),N(active)}, .weight = 1} + } + }; + set_authority(account, N(active), auth, N(owner) ); + } +}; + +BOOST_AUTO_TEST_SUITE(dice_tests) + +BOOST_FIXTURE_TEST_CASE( dice_test, dice_tester ) try { + + set_code(config::system_account_name, eosio_token_wast); + set_abi(config::system_account_name, eosio_token_abi); + + create_accounts( {N(dice),N(alice),N(bob),N(carol),N(david)}, false); + produce_block(); + + add_dice_authority(N(alice)); + add_dice_authority(N(bob)); + add_dice_authority(N(carol)); + + push_action(N(eosio), N(create), N(eosio), mvo() + ("issuer", "eosio") + ("maximum_supply", "1000000000000.0000 EOS") + ("can_freeze", "0") + ("can_recall", "0") + ("can_whitelist", "0") + ); + + push_action(N(eosio), N(issue), N(eosio), mvo() + ("to", "eosio") + ("quantity", "1000000.0000 EOS") + ("memo", "") + ); + + transfer( N(eosio), N(alice), "10000.0000 EOS", "", N(eosio) ); + transfer( N(eosio), N(bob), "10000.0000 EOS", "", N(eosio) ); + transfer( N(eosio), N(carol), "10000.0000 EOS", "", N(eosio) ); + + produce_block(); + + set_code(N(dice), dice_wast); + set_abi(N(dice), dice_abi); + + produce_block(); + + // Alice deposits 1000 EOS + deposit( N(alice), asset::from_string("1000.0000 EOS")); + produce_block(); + + BOOST_REQUIRE_EQUAL( balance_of(N(alice)), asset::from_string("1000.0000 EOS")); + BOOST_REQUIRE_EQUAL( open_games(N(alice)), 0); + + // Alice tries to bet 0 EOS (fail) + // secret : 9b886346e1351d4144d0b8392a975612eb0f8b6de7eae1cc9bcc55eb52be343c + BOOST_CHECK_THROW( offer_bet( N(alice), asset::from_string("0.0000 EOS"), + commitment_for("9b886346e1351d4144d0b8392a975612eb0f8b6de7eae1cc9bcc55eb52be343c") + ), fc::exception); + + // Alice bets 10 EOS (success) + // secret : 0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46 + offer_bet( N(alice), asset::from_string("10.0000 EOS"), + commitment_for("0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46") + ); + produce_block(); + + // Bob tries to bet using a secret previously used by Alice (fail) + // secret : 00000000000000000000000000000002c334abe6ce13219a4cf3b862abb03c46 + BOOST_CHECK_THROW( offer_bet( N(bob), asset::from_string("10.0000 EOS"), + commitment_for("0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46") + ), fc::exception); + produce_block(); + + // Alice tries to bet 1000 EOS (fail) + // secret : a512f6b1b589a8906d574e9de74a529e504a5c53a760f0991a3e00256c027971 + BOOST_CHECK_THROW( offer_bet( N(alice), asset::from_string("10000.0000 EOS"), + commitment_for("a512f6b1b589a8906d574e9de74a529e504a5c53a760f0991a3e00256c027971") + ), fc::exception); + produce_block(); + + // Bob tries to bet 90 EOS without deposit + // secret : 4facfc98932dde46fdc4403125a16337f6879a842a7ff8b0dc8e1ecddd59f3c8 + BOOST_CHECK_THROW( offer_bet( N(bob), asset::from_string("90.0000 EOS"), + commitment_for("4facfc98932dde46fdc4403125a16337f6879a842a7ff8b0dc8e1ecddd59f3c8") + ), fc::exception); + produce_block(); + + // Bob deposits 500 EOS + deposit( N(bob), asset::from_string("500.0000 EOS")); + BOOST_REQUIRE_EQUAL( balance_of(N(bob)), asset::from_string("500.0000 EOS")); + + // Bob bets 11 EOS (success) + // secret : eec3272712d974c474a3e7b4028b53081344a5f50008e9ccf918ba0725a8d784 + offer_bet( N(bob), asset::from_string("11.0000 EOS"), + commitment_for("eec3272712d974c474a3e7b4028b53081344a5f50008e9ccf918ba0725a8d784") + ); + produce_block(); + + // Bob cancels (success) + BOOST_REQUIRE_EQUAL( open_offers(N(bob)), 1); + cancel_offer( N(bob), commitment_for("eec3272712d974c474a3e7b4028b53081344a5f50008e9ccf918ba0725a8d784") ); + BOOST_REQUIRE_EQUAL( open_offers(N(bob)), 0); + + // Carol deposits 300 EOS + deposit( N(carol), asset::from_string("300.0000 EOS")); + + // Carol bets 10 EOS (success) + // secret : 3efb4bd5e19b780f4980c919330c0306f8157f93db1fc72c7cefec63e0e7f37a + offer_bet( N(carol), asset::from_string("10.0000 EOS"), + commitment_for("3efb4bd5e19b780f4980c919330c0306f8157f93db1fc72c7cefec63e0e7f37a") + ); + produce_block(); + + BOOST_REQUIRE_EQUAL( open_games(N(alice)), 1); + BOOST_REQUIRE_EQUAL( open_offers(N(alice)), 0); + + BOOST_REQUIRE_EQUAL( open_games(N(carol)), 1); + BOOST_REQUIRE_EQUAL( open_offers(N(carol)), 0); + + BOOST_REQUIRE_EQUAL( game_bet(1), asset::from_string("10.0000 EOS")); + + + // Alice tries to cancel a nonexistent bet (fail) + BOOST_CHECK_THROW( cancel_offer( N(alice), + commitment_for("00000000000000000000000000000000000000000000000000000000abb03c46") + ), fc::exception); + + // Alice tries to cancel an in-game bet (fail) + BOOST_CHECK_THROW( cancel_offer( N(alice), + commitment_for("0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46") + ), fc::exception); + + // Alice reveals secret (success) + reveal( N(alice), + commitment_for("0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46"), + checksum_type("0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46") + ); + produce_block(); + + // Alice tries to reveal again (fail) + BOOST_CHECK_THROW( reveal( N(alice), + commitment_for("0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46"), + checksum_type("0ba044d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46") + ), fc::exception); + + // Bob tries to reveal an invalid (secret,commitment) pair (fail) + BOOST_CHECK_THROW( reveal( N(bob), + commitment_for("121344d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46"), + checksum_type("141544d2833758ee2c8f24d8a3f70c82c334abe6ce13219a4cf3b862abb03c46") + ), fc::exception); + + // Bob tries to reveal a valid (secret,commitment) pair that has no offer/game (fail) + BOOST_CHECK_THROW( reveal( N(bob), + commitment_for("e48c6884bb97ac5f5951df6012ce79f63bb8549ad0111315ad9ecbaf4c9b1eb8"), + checksum_type("e48c6884bb97ac5f5951df6012ce79f63bb8549ad0111315ad9ecbaf4c9b1eb8") + ), fc::exception); + + // Bob reveals Carol's secret (success) + reveal( N(bob), + commitment_for("3efb4bd5e19b780f4980c919330c0306f8157f93db1fc72c7cefec63e0e7f37a"), + checksum_type("3efb4bd5e19b780f4980c919330c0306f8157f93db1fc72c7cefec63e0e7f37a") + ); + + BOOST_REQUIRE_EQUAL( open_games(N(alice)), 0); + BOOST_REQUIRE_EQUAL( open_offers(N(alice)), 0); + BOOST_REQUIRE_EQUAL( balance_of(N(alice)), asset::from_string("1010.0000 EOS")); + + BOOST_REQUIRE_EQUAL( open_games(N(carol)), 0); + BOOST_REQUIRE_EQUAL( open_offers(N(carol)), 0); + BOOST_REQUIRE_EQUAL( balance_of(N(carol)), asset::from_string("290.0000 EOS")); + + // Alice withdraw 1009 EOS (success) + withdraw( N(alice), asset::from_string("1009.0000 EOS")); + BOOST_REQUIRE_EQUAL( balance_of(N(alice)), asset::from_string("1.0000 EOS")); + + BOOST_REQUIRE_EQUAL( + get_currency_balance(N(eosio), EOS_SYMBOL, N(alice)), + asset::from_string("10009.0000 EOS") + ); + + // Alice withdraw 2 EOS (fail) + BOOST_CHECK_THROW( withdraw( N(alice), asset::from_string("2.0000 EOS")), + fc::exception); + + // Alice withdraw 1 EOS (success) + withdraw( N(alice), asset::from_string("1.0000 EOS")); + + BOOST_REQUIRE_EQUAL( + get_currency_balance(N(eosio), EOS_SYMBOL, N(alice)), + asset::from_string("10010.0000 EOS") + ); + + // Verify alice account was deleted + account_t alice_account; + BOOST_CHECK(dice_account(N(alice), alice_account) == false); + + // No games in table + auto* game_tid = find_table(N(dice), N(dice), N(game)); + BOOST_CHECK(game_tid != nullptr); + BOOST_CHECK(game_tid->count == 0); + + // No offers in table + auto* offer_tid = find_table(N(dice), N(dice), N(offer)); + BOOST_CHECK(offer_tid != nullptr); + BOOST_CHECK(offer_tid->count == 0); + + // 2 records in account table (Bob & Carol) + auto* account_tid = find_table(N(dice), N(dice), N(account)); + BOOST_CHECK(account_tid != nullptr); + BOOST_CHECK(account_tid->count == 2); + +} FC_LOG_AND_RETHROW() /// basic_test + +BOOST_AUTO_TEST_SUITE_END()