diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd611b909..30cc914e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,7 +126,7 @@ jobs: cp eden-micro-chain.wasm ../product_cache cp eden.abi ../product_cache cp eden.wasm ../product_cache - cp run-full-election.wasm ../product_cache + cp run-elections.wasm ../product_cache cp run-genesis.wasm ../product_cache cp token.abi ../product_cache cp token.wasm ../product_cache @@ -181,7 +181,7 @@ jobs: product_cache/token.abi product_cache/token.wasm product_cache/run-genesis.wasm - product_cache/run-full-election.wasm + product_cache/run-elections.wasm build-micro-chain: name: Build Micro Chain @@ -353,6 +353,7 @@ jobs: - "tsconfig.build.json" - "tsconfig.json" - "yarn.lock" + - "scripts/eden_chain_runner.sh" - "packages/**" - "contracts/**" @@ -364,7 +365,7 @@ jobs: name: Eden Microchain path: build - - name: Download Ephemeral Chain Runners + - name: Download Ephemeral Eden Chain Runners if: steps.filter.outputs.src == 'true' uses: actions/download-artifact@v2 with: diff --git a/README.md b/README.md index 4a20985d4..1fee9a04f 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,64 @@ wget https://nodejs.org/dist/v14.16.0/node-v14.16.0-linux-x64.tar.xz tar xf node-v14.16.0-linux-x64.tar.xz npm i -g yarn ``` + +### Running Eden with Ephemeral Chains Locally + +Ephemeral chains are instances of the EOS blockchain spawned by `nodeos` locally, with manipulated data from our chain runners, eg: [Basic Genesis Runner](contracts/eden/tests/run-genesis.cpp) or [Full Election Runner](contracts/eden/tests/run-full-election.cpp). By running a ephemeral chain you are in full control of the blockchain, giving you more flexibility to test the Eden contracts. + +#### Get the executables + +You will need to build the repo locally by following the below **build** steps. If you don't have a proper C++ environment setup you can download it from our current [main branch artifacts](https://github.com/eoscommunity/Eden/actions/workflows/build.yml?query=branch%3Amain). + +If you built locally, you can skip these steps. + +**Downloading the executables** + +- Open our [main branch builds](https://github.com/eoscommunity/Eden/actions/workflows/build.yml?query=branch%3Amain). +- Click in the most recent successful one +- Scroll down to the artifacts section +- Download the following files: + - Eden Microchain + - Ephemeral Eden Chains Runners + - clsdk +- From the root of this repo, run the following commands: + +```sh +mkdir build +# unzip all of the above artifact files in this build folder +cd build +tar -xvf clsdk-ubuntu-20-04.tar.gz clsdk/bin +cp ../scripts/eden_chain_runner.sh ./ +``` + +Now you have all the files needed for running the ephemeral chain inside the `build` folder. + +If you are on a Linux machine compatible with Ubuntu arch you can spin it up by just running: `./eden_chain_runner.sh run-genesis.wasm 1` + +Otherwise you can spin it up with the following docker command: + +```sh +docker run --name eden-genesis \ + -v "$(pwd)":/app \ + -w /app \ + -p 8080:8080 -p 8888:8888 \ + -d -it ghcr.io/eoscommunity/eden-builder:sub-chain \ + bash ./eden_chain_runner.sh run-genesis.wasm 1 +``` + +To see if the chain is running successfully you can execute `cleos get info` or watch the nodeos logs: `tail -fn +1 eden-runner.log` + +With the ephemeral chain running you can just spin up our local environment with: + +```sh +yarn +NODE_ENV=test yarn build --stream +NODE_ENV=test yarn start --stream +open http://localhost:3000 +``` + +**Re-running ephemeral chains** + +Running the above commands again will just setup a brand new chain! Just watch out to kill nodeos and unlock your keos wallet if built locally or remove your docker container. Also don't forget to restart the `yarn` environment because the blocks state needs to be refreshed. + +In the above instructions we ran a simple genesis case with 3 inducted members, but you can also try `run-full-election.wasm` to see a community with more than 100 members with chief delegates already elected. diff --git a/contracts/eden/CMakeLists.txt b/contracts/eden/CMakeLists.txt index ec019e00d..b33292ead 100644 --- a/contracts/eden/CMakeLists.txt +++ b/contracts/eden/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(eden src/actions/migrate.cpp src/actions/encrypt.cpp src/actions/tables.cpp + src/actions/sessions.cpp src/eden.cpp src/events.cpp src/accounts.cpp @@ -32,6 +33,7 @@ add_executable(eden src/encrypt.cpp ) target_include_directories(eden PUBLIC include ../token/include PRIVATE ../../external/atomicassets-contract/include) +target_compile_options(eden PUBLIC -flto) target_link_libraries(eden eosio-contract) set_target_properties(eden PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${ROOT_BINARY_DIR}) @@ -62,7 +64,7 @@ eden_tester_test(test-eden) # Chain Runners add_test_eden("run-genesis" "") -add_test_eden("run-full-election" "") +add_test_eden("run-elections" "") file(CREATE_LINK ${CMAKE_CURRENT_SOURCE_DIR}/tests/data ${ROOT_BINARY_DIR}/eden-test-data SYMBOLIC) diff --git a/contracts/eden/include/eden.hpp b/contracts/eden/include/eden.hpp index 456d524a0..8d5166371 100644 --- a/contracts/eden/include/eden.hpp +++ b/contracts/eden/include/eden.hpp @@ -2,11 +2,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include @@ -40,6 +42,9 @@ namespace eden extern const char* peacetreaty_clause; extern const char* bylaws_clause; + // Placeholder; the ABI generator redefines this + using verb = std::variant; + #ifdef ENABLE_SET_TABLE_ROWS using table_variant = boost::mp11::mp_append& current_session, + eosio::name eden_account, + const eosio::public_key& key); + + void run(eosio::ignore auth, eosio::ignore> verbs); + void withdraw(eosio::name owner, const eosio::asset& quantity); void donate(eosio::name payer, const eosio::asset& quantity); @@ -95,21 +111,33 @@ namespace eden void clearall(); - void inductinit(uint64_t id, + void inductinit(const eosio::not_in_abi& current_session, + uint64_t id, eosio::name inviter, eosio::name invitee, std::vector witnesses); - void inductprofil(uint64_t id, new_member_profile new_member_profile); + void inductprofil(const eosio::not_in_abi& current_session, + uint64_t id, + new_member_profile new_member_profile); - void inductvideo(eosio::name account, uint64_t id, std::string video); + void inductvideo(const eosio::not_in_abi& current_session, + eosio::name account, + uint64_t id, + std::string video); - void inductendors(eosio::name account, uint64_t id, eosio::checksum256 induction_data_hash); + void inductendors(const eosio::not_in_abi& current_session, + eosio::name account, + uint64_t id, + eosio::checksum256 induction_data_hash); void inductdonate(eosio::name payer, uint64_t id, const eosio::asset& quantity); - void inductcancel(eosio::name account, uint64_t id); - void inductmeetin(eosio::name account, + void inductcancel(const eosio::not_in_abi& current_session, + eosio::name account, + uint64_t id); + void inductmeetin(const eosio::not_in_abi& current_session, + eosio::name account, uint64_t id, const std::vector& keys, const eosio::bytes& data, @@ -127,16 +155,25 @@ namespace eden const std::string& election_time, uint32_t round_duration_sec); - void electopt(eosio::name member, bool participating); + void electopt(const eosio::not_in_abi& current_session, + eosio::name member, + bool participating); void electseed(const eosio::bytes& btc_header); - void electmeeting(eosio::name account, + void electmeeting(const eosio::not_in_abi& current_session, + eosio::name account, uint8_t round, const std::vector& keys, const eosio::bytes& data, const std::optional& old_data); - void electvote(uint8_t round, eosio::name voter, eosio::name candidate); - void electvideo(uint8_t round, eosio::name voter, const std::string& video); + void electvote(const eosio::not_in_abi& current_session, + uint8_t round, + eosio::name voter, + eosio::name candidate); + void electvideo(const eosio::not_in_abi& current_session, + uint8_t round, + eosio::name voter, + const std::string& video); void electprocess(uint32_t max_steps); void distribute(uint32_t max_steps); @@ -188,9 +225,12 @@ namespace eden eosio::ignore>); }; - EOSIO_ACTIONS( + EDEN_ACTIONS( eden, "eden.gm"_n, + action(newsession, eden_account, key, expiration, description), + eden_verb(delsession, 0, eden_account, key), + action(run, auth, verbs), action(withdraw, owner, quantity, ricardian_contract(withdraw_ricardian)), action(donate, owner, quantity), action(fundtransfer, from, distribution_time, rank, to, amount, memo), @@ -211,35 +251,41 @@ namespace eden action(addtogenesis, account, expiration), action(gensetexpire, id, new_expiration), action(clearall, ricardian_contract(clearall_ricardian)), - action(inductinit, - id, - inviter, - invitee, - witnesses, - ricardian_contract(inductinit_ricardian)), - action(inductmeetin, account, id, keys, data, old_data), - action(inductprofil, id, new_member_profile, ricardian_contract(inductprofil_ricardian)), - action(inductvideo, account, id, video, ricardian_contract(inductvideo_ricardian)), - action(inductendors, - account, - id, - induction_data_hash, - ricardian_contract(inductendors_ricardian)), + eden_verb(inductinit, + 10, + id, + inviter, + invitee, + witnesses, + ricardian_contract(inductinit_ricardian)), + eden_verb(inductmeetin, 1, account, id, keys, data, old_data), + eden_verb(inductprofil, + 2, + id, + new_member_profile, + ricardian_contract(inductprofil_ricardian)), + eden_verb(inductvideo, 3, account, id, video, ricardian_contract(inductvideo_ricardian)), + eden_verb(inductendors, + 4, + account, + id, + induction_data_hash, + ricardian_contract(inductendors_ricardian)), action(setencpubkey, account, key), action(electsettime, election_time), action(electconfig, day, time, round_duration), - action(electopt, member, participating), + eden_verb(electopt, 5, member, participating), action(electseed, btc_header), - action(electmeeting, account, round, keys, data, old_data), - action(electvote, round, voter, candidate), - action(electvideo, round, voter, video), + eden_verb(electmeeting, 6, account, round, keys, data, old_data), + eden_verb(electvote, 7, round, voter, candidate), + eden_verb(electvideo, 8, round, voter, video), action(electprocess, max_steps), action(bylawspropose, proposer, bylaws), action(bylawsapprove, approver, bylaws_hash), action(bylawsratify, approver, bylaws_hash), action(distribute, max_steps), action(inductdonate, payer, id, quantity, ricardian_contract(inductdonate_ricardian)), - action(inductcancel, account, id, ricardian_contract(inductcancel_ricardian)), + eden_verb(inductcancel, 9, account, id, ricardian_contract(inductcancel_ricardian)), action(inducted, inductee, ricardian_contract(inducted_ricardian)), action(resign, account), action(gc, limit, ricardian_contract(gc_ricardian)), diff --git a/contracts/eden/include/eden_abi_generator.hpp b/contracts/eden/include/eden_abi_generator.hpp new file mode 100644 index 000000000..91dcdcf5b --- /dev/null +++ b/contracts/eden/include/eden_abi_generator.hpp @@ -0,0 +1,20 @@ +#pragma once + +#define EOSIO_ABIGEN_ITEMeden_verbs(ns, variant_name, missing_struct_name) \ + ([&] { \ + gen.def.structs.push_back(eosio::struct_def{missing_struct_name}); \ + eosio::variant_def vdef{variant_name}; \ + ns::for_each_verb([&](uint32_t index, const char* name, const auto&) { \ + if (index >= vdef.types.size()) \ + vdef.types.resize(index + 1, missing_struct_name); \ + vdef.types[index] = name; \ + }); \ + auto& variants = gen.def.variants.value; \ + auto it = std::find_if(variants.begin(), variants.end(), \ + [&](auto& d) { return d.name == variant_name; }); \ + if (it != variants.end()) \ + *it = std::move(vdef); \ + else \ + variants.push_back(std::move(vdef)); \ + })(); \ + , 1 diff --git a/contracts/eden/include/eden_dispatcher.hpp b/contracts/eden/include/eden_dispatcher.hpp new file mode 100644 index 000000000..25bb76215 --- /dev/null +++ b/contracts/eden/include/eden_dispatcher.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include + +namespace eden +{ + template + void execute_session_action(eosio::name contract, + R (T::*func)(const eosio::not_in_abi& current_session, + Args...), + const eosio::not_in_abi& current_session, + eosio::datastream& ds) + { + std::tuple...> t; + ds >> t; + T inst(contract, contract, ds); + std::apply([&](auto&... args) { (inst.*func)(current_session, std::move(args)...); }, t); + } +} // namespace eden + +#define EOSIO_MATCH_ACTIONeden_verb EOSIO_MATCH_YES +#define EOSIO_EXTRACT_ACTION_NAMEeden_verb(name, index, ...) name +#define EOSIO_EXTRACT_ACTION_ARGSeden_verb(name, index, ...) __VA_ARGS__ + +#define EDEN_MATCH_SESSION_ACTION(x) EOSIO_MATCH(EDEN_MATCH_SESSION_ACTION, x) +#define EDEN_MATCH_SESSION_ACTIONeden_verb EOSIO_MATCH_YES + +#define EDEN_EXTRACT_SESSION_ACTION_INDEX(x) BOOST_PP_CAT(EDEN_EXTRACT_SESSION_ACTION_INDEX, x) +#define EDEN_EXTRACT_SESSION_ACTION_INDEXeden_verb(name, index, ...) index + +#define EDEN_DISPATCH_SESSION_ACTION_INTERNAL_1(r, type, member) \ + case EDEN_EXTRACT_SESSION_ACTION_INDEX(member): \ + ::eden::execute_session_action(contract, &type::EOSIO_EXTRACT_ACTION_NAME(member), \ + current_session, ds); \ + return true; +#define EDEN_DISPATCH_SESSION_ACTION_INTERNAL(r, type, member) \ + BOOST_PP_IIF(EDEN_MATCH_SESSION_ACTION(member), EDEN_DISPATCH_SESSION_ACTION_INTERNAL_1, \ + EOSIO_EMPTY) \ + (r, type, member) +#define EDEN_DISPATCH_SESSION_ACTION(type, MEMBERS) \ + BOOST_PP_SEQ_FOR_EACH(EDEN_DISPATCH_SESSION_ACTION_INTERNAL, type, MEMBERS) + +#define EDEN_GET_SESSION_ACTION_INTERNAL_1(r, type, member) \ + f(EDEN_EXTRACT_SESSION_ACTION_INDEX(member), \ + BOOST_PP_STRINGIZE(EOSIO_EXTRACT_ACTION_NAME(member)), \ + &type::EOSIO_EXTRACT_ACTION_NAME(member)); +#define EDEN_GET_SESSION_ACTION_INTERNAL(r, type, member) \ + BOOST_PP_IIF(EDEN_MATCH_SESSION_ACTION(member), EDEN_GET_SESSION_ACTION_INTERNAL_1, \ + EOSIO_EMPTY) \ + (r, type, member) +#define EDEN_GET_SESSION_ACTION(type, MEMBERS) \ + BOOST_PP_SEQ_FOR_EACH(EDEN_GET_SESSION_ACTION_INTERNAL, type, MEMBERS) + +#define EDEN_NAME_FOR_SESSION_ACTION_INTERNAL_1(r, type, member) \ + case EDEN_EXTRACT_SESSION_ACTION_INDEX(member): \ + return BOOST_PP_CAT(BOOST_PP_STRINGIZE(EOSIO_EXTRACT_ACTION_NAME(member)), _n); +#define EDEN_NAME_FOR_SESSION_ACTION_INTERNAL(r, type, member) \ + BOOST_PP_IIF(EDEN_MATCH_SESSION_ACTION(member), EDEN_NAME_FOR_SESSION_ACTION_INTERNAL_1, \ + EOSIO_EMPTY) \ + (r, type, member) +#define EDEN_NAME_FOR_SESSION_ACTION(type, MEMBERS) \ + BOOST_PP_SEQ_FOR_EACH(EDEN_NAME_FOR_SESSION_ACTION_INTERNAL, type, MEMBERS) + +#define EDEN_INDEX_FOR_SESSION_ACTION_INTERNAL_1(r, type, member) \ + if (name == BOOST_PP_CAT(BOOST_PP_STRINGIZE(EOSIO_EXTRACT_ACTION_NAME(member)), _n)) \ + return EDEN_EXTRACT_SESSION_ACTION_INDEX(member); +#define EDEN_INDEX_FOR_SESSION_ACTION_INTERNAL(r, type, member) \ + BOOST_PP_IIF(EDEN_MATCH_SESSION_ACTION(member), EDEN_INDEX_FOR_SESSION_ACTION_INTERNAL_1, \ + EOSIO_EMPTY) \ + (r, type, member) +#define EDEN_INDEX_FOR_SESSION_ACTION(type, MEMBERS) \ + BOOST_PP_SEQ_FOR_EACH(EDEN_INDEX_FOR_SESSION_ACTION_INTERNAL, type, MEMBERS) + +#define EDEN_ACTIONS(CONTRACT_CLASS, CONTRACT_ACCOUNT, ...) \ + EOSIO_ACTIONS(CONTRACT_CLASS, CONTRACT_ACCOUNT, __VA_ARGS__) \ + namespace actions \ + { \ + inline bool session_dispatch(eosio::name contract, \ + uint32_t index, \ + const eosio::not_in_abi& current_session, \ + eosio::datastream& ds) \ + { \ + switch (index) \ + { \ + EDEN_DISPATCH_SESSION_ACTION(CONTRACT_CLASS, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \ + } \ + return false; \ + } \ + template \ + void for_each_verb(F f) \ + { \ + EDEN_GET_SESSION_ACTION(CONTRACT_CLASS, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \ + } \ + inline eosio::name get_name_for_session_action(uint32_t index) \ + { \ + switch (index) \ + { \ + EDEN_NAME_FOR_SESSION_ACTION(CONTRACT_CLASS, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \ + } \ + return {}; \ + } \ + inline std::optional get_index_for_session_action(eosio::name name) \ + { \ + EDEN_INDEX_FOR_SESSION_ACTION(CONTRACT_CLASS, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \ + return {}; \ + } \ + } diff --git a/contracts/eden/include/elections.hpp b/contracts/eden/include/elections.hpp index ca5f29eec..f4d8074aa 100644 --- a/contracts/eden/include/elections.hpp +++ b/contracts/eden/include/elections.hpp @@ -109,14 +109,25 @@ namespace eden EOSIO_REFLECT(current_election_state_pending_date); EOSIO_COMPARE(current_election_state_pending_date); - struct current_election_state_registration + struct current_election_state_registration_v0 { eosio::block_timestamp start_time; - uint16_t - election_threshold; // The election may be moved forward if active membership reached this + // The election may be moved forward if active membership reached this + uint16_t election_threshold; + // The number of times that the election schedule has been updated + // always > 0 + uint8_t election_schedule_version = 1; }; - EOSIO_REFLECT(current_election_state_registration, start_time, election_threshold); - EOSIO_COMPARE(current_election_state_registration); + EOSIO_REFLECT(current_election_state_registration_v0, start_time, election_threshold); + EOSIO_COMPARE(current_election_state_registration_v0); + + struct current_election_state_registration_v1 : current_election_state_registration_v0 + { + }; + EOSIO_REFLECT(current_election_state_registration_v1, + base current_election_state_registration_v0, + election_schedule_version); + EOSIO_COMPARE(current_election_state_registration_v1); struct election_seeder { @@ -130,27 +141,44 @@ namespace eden EOSIO_REFLECT(election_seeder, current, start_time, end_time); EOSIO_COMPARE(election_seeder); - struct current_election_state_seeding + struct current_election_state_seeding_v0 { election_seeder seed; + std::uint8_t election_schedule_version = 1; + }; + EOSIO_REFLECT(current_election_state_seeding_v0, seed); + EOSIO_COMPARE(current_election_state_seeding_v0); + + struct current_election_state_seeding_v1 : current_election_state_seeding_v0 + { }; - EOSIO_REFLECT(current_election_state_seeding, seed); - EOSIO_COMPARE(current_election_state_seeding); + EOSIO_REFLECT(current_election_state_seeding_v1, + base current_election_state_seeding_v0, + election_schedule_version) // In this phase, every voter is assigned a unique random integer id in [0,N) - struct current_election_state_init_voters + struct current_election_state_init_voters_v0 { uint16_t next_member_idx; election_rng rng; eosio::name last_processed = {}; uint16_t next_report_index = 0; + uint8_t election_schedule_version = 1; }; - EOSIO_REFLECT(current_election_state_init_voters, + EOSIO_REFLECT(current_election_state_init_voters_v0, next_member_idx, rng, last_processed, next_report_index) - EOSIO_COMPARE(current_election_state_init_voters); + EOSIO_COMPARE(current_election_state_init_voters_v0); + + struct current_election_state_init_voters_v1 : current_election_state_init_voters_v0 + { + }; + EOSIO_REFLECT(current_election_state_init_voters_v1, + base current_election_state_init_voters_v0, + election_schedule_version) + EOSIO_COMPARE(current_election_state_init_voters_v1); struct current_election_state_active { @@ -188,15 +216,35 @@ namespace eden EOSIO_COMPARE(current_election_state_final); using current_election_state = std::variant; + current_election_state_final, + current_election_state_registration_v1, + current_election_state_seeding_v1, + current_election_state_init_voters_v1>; using current_election_state_singleton = eosio::singleton<"elect.curr"_n, current_election_state>; + template + T* get_if_derived(current_election_state* state) + { + return std::visit( + [](auto& s) -> T* { + if constexpr (std::is_base_of_v>) + { + return &s; + } + else + { + return nullptr; + } + }, + *state); + } + // Requirements: // - Except for the last round, the group size shall be in [4,6] // - The last round has a minimum group size of 3 @@ -219,7 +267,7 @@ namespace eden void set_state_sing(const current_election_state& new_value); void add_voter(election_rng& rng, uint8_t round, uint16_t& next_index, eosio::name member); - uint32_t randomize_voters(current_election_state_init_voters& state, uint32_t max_steps); + uint32_t randomize_voters(current_election_state_init_voters_v0& state, uint32_t max_steps); std::vector extract_board(); void finish_election(std::vector&& board, eosio::name winner); void set_board_permission(const std::vector& board); @@ -234,6 +282,7 @@ namespace eden { } std::optional get_next_election_time(); + std::uint8_t election_schedule_version(); void set_time(uint8_t day, const std::string& time); void set_default_election(eosio::time_point_sec origin_time); void trigger_election(); diff --git a/contracts/eden/include/events.hpp b/contracts/eden/include/events.hpp index b317acb1c..dfc56448c 100644 --- a/contracts/eden/include/events.hpp +++ b/contracts/eden/include/events.hpp @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include #include #include +#include #include #include @@ -30,6 +32,12 @@ namespace eden // // election_event_end + struct migration_event + { + eosio::varuint32 index; + }; + EOSIO_REFLECT(migration_event, index) + struct election_event_schedule { eosio::block_timestamp election_time; @@ -206,6 +214,24 @@ namespace eden }; EOSIO_REFLECT(distribution_event_return, owner, distribution_time, rank, amount, pool) + // Session events + + struct session_new_event + { + eosio::name eden_account; + eosio::public_key key; + eosio::block_timestamp expiration; + std::string description; + }; + EOSIO_REFLECT(session_new_event, eden_account, key, expiration, description) + + struct session_del_event + { + eosio::name eden_account; + eosio::public_key key; + }; + EOSIO_REFLECT(session_del_event, eden_account, key) + using event = std::variant; + distribution_event_return, + migration_event, + session_new_event, + session_del_event>; void push_event(const event& e, eosio::name self); void send_events(eosio::name self); diff --git a/contracts/eden/include/inductions.hpp b/contracts/eden/include/inductions.hpp index 18499f2e2..c1c6c9f91 100644 --- a/contracts/eden/include/inductions.hpp +++ b/contracts/eden/include/inductions.hpp @@ -144,7 +144,6 @@ namespace eden void check_new_induction(eosio::name invitee, eosio::name inviter) const; bool is_valid_induction(const induction& induction) const; - void check_valid_induction(const induction& induction) const; void validate_profile(const new_member_profile& new_member_profile) const; void validate_video(const std::string& video) const; void check_valid_endorsers(eosio::name inviter, @@ -165,6 +164,7 @@ namespace eden const induction& get_induction(uint64_t id) const; const induction& get_endorsed_induction(eosio::name invitee) const; bool has_induction(eosio::name invitee) const; + void check_valid_induction(const induction& induction) const; void initialize_induction(uint64_t id, eosio::name inviter, diff --git a/contracts/eden/include/members.hpp b/contracts/eden/include/members.hpp index 911d95a03..72fb72f39 100644 --- a/contracts/eden/include/members.hpp +++ b/contracts/eden/include/members.hpp @@ -17,12 +17,7 @@ namespace eden active_member = 1 }; - using election_participation_status_type = uint8_t; - enum election_participation_status : election_participation_status_type - { - not_in_election, - in_election - }; + inline constexpr std::uint8_t not_in_election = 0; struct migrate_member_v0 { @@ -38,7 +33,7 @@ namespace eden member_status_type status; uint64_t nft_template_id; // Only reflected in v1 - election_participation_status_type election_participation_status = not_in_election; + uint8_t election_participation_status = 0; uint8_t election_rank = 0; eosio::name representative{uint64_t(-1)}; std::optional encryption_key; diff --git a/contracts/eden/include/migrations.hpp b/contracts/eden/include/migrations.hpp index 1e3d779cb..89834f224 100644 --- a/contracts/eden/include/migrations.hpp +++ b/contracts/eden/include/migrations.hpp @@ -18,11 +18,17 @@ namespace eden }; EOSIO_REFLECT(no_migration<0>); EOSIO_REFLECT(no_migration<1>); + EOSIO_REFLECT(no_migration<2>); + + // No-op migration to notify history the fix for checking expired inductions on inductdonate + using fix_inductdonate_expiration_check = no_migration<2>; + using migration_variant = std::variant, migrate_member_v0, - no_migration<1>>; + no_migration<1>, + fix_inductdonate_expiration_check>; using migration_singleton = eosio::singleton<"migration"_n, migration_variant>; diff --git a/contracts/eden/include/sessions.hpp b/contracts/eden/include/sessions.hpp new file mode 100644 index 000000000..91a8aa962 --- /dev/null +++ b/contracts/eden/include/sessions.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include + +namespace eden +{ + struct session_v0 + { + eosio::public_key key; + eosio::block_timestamp expiration; + std::string description; + std::vector sequences; + }; + EOSIO_REFLECT(session_v0, key, expiration, description, sequences) + + struct session_container_v0 + { + eosio::name eden_account; + eosio::block_timestamp earliest_expiration; + std::vector sessions; + + uint64_t primary_key() const { return eden_account.value; } + uint64_t by_expiration() const { return earliest_expiration.slot; } + }; + EOSIO_REFLECT(session_container_v0, eden_account, earliest_expiration, sessions) + + using session_container_variant = std::variant; + + struct session_container + { + session_container_variant value; + EDEN_FORWARD_MEMBERS(value, eden_account, earliest_expiration, sessions); + EDEN_FORWARD_FUNCTIONS(value, primary_key, by_expiration) + }; + EOSIO_REFLECT(session_container, value) + + using sessions_table_type = eosio::multi_index< + "sessions"_n, + session_container, + eosio::indexed_by< + "byexpiration"_n, + eosio::const_mem_fun>>; + + uint32_t gc_sessions(eosio::name contract, uint32_t remaining); + void clearall_sessions(eosio::name contract); + void remove_sessions(eosio::name contract, eosio::name eden_account); + + // No authorization provided + struct no_auth + { + }; + EOSIO_REFLECT(no_auth) + + // Case 1: eosio account. run() does a require_auth(eosio_account). + // * contract: "" + // * contract_account: "" + // * eosio_account: the account + // + // Case 2: contract-defined account. run() does a require_auth(eosio_account), verifies + // that eosio_account is associated with contract_account, verifies the recovered + // public key is registered for the account, and checks the sequence number. run() + // may delegate everything except the require_auth to another contract. + // * contract: the contract defining the account space + // * contract_account: the account + // * eosio_account: eosio account associated with contract_account. + // + // The Eden contract only supports Case 2 and requires contract == the Eden contract. + struct account_auth + { + eosio::name contract; + eosio::name contract_account; + eosio::name eosio_account; + }; + EOSIO_REFLECT(account_auth, contract, contract_account, eosio_account) + + // * signature: covers sha256(contract, account, sequence, verbs) + // * contract: the contract defining the account space, or "" if an eosio account + // * account: the contract account or eosio account + // * sequence: replay prevention + // + // The Eden contract requires contract == the Eden contract. + struct signature_auth + { + eosio::signature signature; + eosio::name contract; + eosio::name account; + eosio::varuint32 sequence; + }; + EOSIO_REFLECT(signature_auth, signature, contract, account, sequence) + + using run_auth = std::variant; + + enum class run_auth_type + { + no_auth, + account_auth, + signature_auth + }; + + struct session_info + { + std::optional authorized_eden_account; + + void require_auth(eosio::name eden_account) const + { + if (!authorized_eden_account) + eosio::require_auth(eden_account); + else if (eden_account != *authorized_eden_account) + eosio::check(false, "need authorization of " + eden_account.to_string() + + " but have authorization of " + + authorized_eden_account->to_string()); + } + }; +} // namespace eden diff --git a/contracts/eden/src/actions/elect.cpp b/contracts/eden/src/actions/elect.cpp index e73192224..61dad28a4 100644 --- a/contracts/eden/src/actions/elect.cpp +++ b/contracts/eden/src/actions/elect.cpp @@ -26,22 +26,14 @@ namespace eden globals.set_election_round_duration(round_duration); } - void eden::electopt(eosio::name voter, bool participating) + void eden::electopt(const eosio::not_in_abi& current_session, + eosio::name voter, + bool participating) { - eosio::require_auth(voter); + current_session.value.require_auth(voter); members members{get_self()}; const auto& member = members.get_member(voter); - if (participating) - { - eosio::check(member.election_participation_status() == not_in_election, - "Not currently opted out"); - } - else - { - eosio::check(member.election_participation_status() == in_election, - "Not currently opted in"); - } members.election_opt(member, participating); } @@ -51,13 +43,14 @@ namespace eden elections.seed(btc_header); } - void eden::electmeeting(eosio::name account, + void eden::electmeeting(const eosio::not_in_abi& current_session, + eosio::name account, uint8_t round, const std::vector& keys, const eosio::bytes& data, const std::optional& old_data) { - eosio::require_auth(account); + current_session.value.require_auth(account); members members{get_self()}; elections elections{get_self()}; auto group_id = elections.get_group_id(account, round); @@ -66,16 +59,22 @@ namespace eden encrypt.set(group_id, keys, data, old_data); } - void eden::electvote(uint8_t round, eosio::name voter, eosio::name candidate) + void eden::electvote(const eosio::not_in_abi& current_session, + uint8_t round, + eosio::name voter, + eosio::name candidate) { - eosio::require_auth(voter); + current_session.value.require_auth(voter); elections elections(get_self()); elections.vote(round, voter, candidate); } - void eden::electvideo(uint8_t round, eosio::name voter, const std::string& video) + void eden::electvideo(const eosio::not_in_abi& current_session, + uint8_t round, + eosio::name voter, + const std::string& video) { - eosio::require_auth(voter); + current_session.value.require_auth(voter); elections elections{get_self()}; members members{get_self()}; if (auto check = elections.can_upload_video(round, voter); boost::logic::indeterminate(check)) diff --git a/contracts/eden/src/actions/genesis.cpp b/contracts/eden/src/actions/genesis.cpp index c8f6bde3c..41fc48112 100644 --- a/contracts/eden/src/actions/genesis.cpp +++ b/contracts/eden/src/actions/genesis.cpp @@ -27,6 +27,7 @@ namespace eden bylaws{get_self()}.clear_all(); encrypt{get_self(), "induction"_n}.clear_all(); encrypt{get_self(), "election"_n}.clear_all(); + clearall_sessions(get_self()); } void eden::gensetexpire(uint64_t induction_id, eosio::time_point new_expiration) diff --git a/contracts/eden/src/actions/induct.cpp b/contracts/eden/src/actions/induct.cpp index 93b1d04c2..1eff90827 100644 --- a/contracts/eden/src/actions/induct.cpp +++ b/contracts/eden/src/actions/induct.cpp @@ -10,12 +10,13 @@ namespace eden { - void eden::inductinit(uint64_t id, + void eden::inductinit(const eosio::not_in_abi& current_session, + uint64_t id, eosio::name inviter, eosio::name invitee, std::vector witnesses) { - require_auth(inviter); + current_session.value.require_auth(inviter); globals{get_self()}.check_active(); @@ -33,13 +34,14 @@ namespace eden inductions{get_self()}.initialize_induction(id, inviter, invitee, witnesses); } - void eden::inductmeetin(eosio::name account, + void eden::inductmeetin(const eosio::not_in_abi& current_session, + eosio::name account, uint64_t id, const std::vector& keys, const eosio::bytes& data, const std::optional& old_data) { - require_auth(account); + current_session.value.require_auth(account); globals{get_self()}.check_active(); members members{get_self()}; @@ -52,11 +54,13 @@ namespace eden encrypt.set(id, keys, data, old_data); } - void eden::inductprofil(uint64_t id, new_member_profile new_member_profile) + void eden::inductprofil(const eosio::not_in_abi& current_session, + uint64_t id, + new_member_profile new_member_profile) { inductions inductions{get_self()}; const auto& induction = inductions.get_induction(id); - require_auth(induction.invitee()); + current_session.value.require_auth(induction.invitee()); members{get_self()}.check_pending_member(induction.invitee()); @@ -69,9 +73,12 @@ namespace eden } } - void eden::inductvideo(eosio::name account, uint64_t id, std::string video) + void eden::inductvideo(const eosio::not_in_abi& current_session, + eosio::name account, + uint64_t id, + std::string video) { - require_auth(account); + current_session.value.require_auth(account); inductions inductions{get_self()}; const auto& induction = inductions.get_induction(id); @@ -83,9 +90,12 @@ namespace eden inductions.update_video(induction, video); } - void eden::inductendors(eosio::name account, uint64_t id, eosio::checksum256 induction_data_hash) + void eden::inductendors(const eosio::not_in_abi& current_session, + eosio::name account, + uint64_t id, + eosio::checksum256 induction_data_hash) { - require_auth(account); + current_session.value.require_auth(account); inductions inductions{get_self()}; const auto& induction = inductions.get_induction(id); @@ -108,6 +118,7 @@ namespace eden accounts user_accounts{get_self()}; const auto& induction = inductions.get_induction(id); + inductions.check_valid_induction(induction); eosio::check(payer == induction.invitee(), "only inductee may donate using this action"); eosio::check(quantity == globals.get().minimum_donation, "incorrect donation"); user_accounts.sub_balance(payer, quantity); @@ -147,7 +158,7 @@ namespace eden if (elect_state.exists()) { auto state = elect_state.get(); - if (auto* reg = std::get_if(&state); + if (auto* reg = get_if_derived(&state); reg && members.stats().active_members >= reg->election_threshold) { elections elections{get_self()}; @@ -166,6 +177,7 @@ namespace eden auctions auctions{get_self()}; remaining = auctions.finish_auctions(remaining); } + remaining = gc_sessions(get_self(), remaining); eosio::check(remaining != limit, "Nothing to do."); if (!removed_members.empty()) { @@ -178,9 +190,11 @@ namespace eden } } - void eden::inductcancel(eosio::name account, uint64_t id) + void eden::inductcancel(const eosio::not_in_abi& current_session, + eosio::name account, + uint64_t id) { - eosio::require_auth(account); + current_session.value.require_auth(account); inductions inductions{get_self()}; bool is_genesis = globals{get_self()}.get().stage == contract_stage::genesis; diff --git a/contracts/eden/src/actions/sessions.cpp b/contracts/eden/src/actions/sessions.cpp new file mode 100644 index 000000000..e338a0685 --- /dev/null +++ b/contracts/eden/src/actions/sessions.cpp @@ -0,0 +1,226 @@ +#include +#include +#include +#include + +namespace eden +{ + void set_expiration(session_container& sc) + { + auto expiration = eosio::block_timestamp::max(); + for (auto& session : sc.sessions()) + expiration = std::min(expiration, session.expiration); + sc.earliest_expiration() = expiration; + } + + void expire(eosio::name contract, session_container& sc) + { + auto now = eosio::current_block_time(); + auto& sessions = sc.sessions(); + for (auto& session : sessions) + if (session.expiration <= now) + push_event(session_del_event{sc.eden_account(), session.key}, contract); + auto new_end = std::remove_if(sessions.begin(), sessions.end(), + [&](auto& session) { return session.expiration <= now; }); + sessions.erase(new_end, sessions.end()); + } + + uint32_t gc_sessions(eosio::name contract, uint32_t remaining) + { + auto now = eosio::current_block_time(); + sessions_table_type table(contract, default_scope); + auto idx = table.get_index<"byexpiration"_n>(); + while (remaining && idx.begin() != idx.end() && idx.begin()->earliest_expiration() <= now) + { + auto& sc = *idx.begin(); + table.modify(sc, contract, [&](auto& sc) { + expire(contract, sc); + set_expiration(sc); + }); + if (sc.sessions().empty()) + table.erase(sc); + --remaining; + } + return remaining; + } + + void clearall_sessions(eosio::name contract) + { + sessions_table_type table(contract, default_scope); + while (table.begin() != table.end()) + table.erase(table.begin()); + } + + void remove_sessions(eosio::name contract, eosio::name eden_account) + { + sessions_table_type table(contract, default_scope); + auto it = table.find(eden_account.value); + if (it == table.end()) + return; + for (auto& session : it->sessions()) + push_event(session_del_event{eden_account, session.key}, contract); + table.erase(it); + } + + void eden::newsession(eosio::name eden_account, + const eosio::public_key& key, + eosio::block_timestamp expiration, + const std::string& description) + { + eosio::require_auth(eden_account); + eosio::check(key.index() < 2, "unsupported key type"); + eosio::check(expiration > eosio::current_block_time(), "session is expired"); + eosio::check(expiration <= eosio::current_block_time().to_time_point() + eosio::days(90), + "expiration is too far in the future"); + eosio::check(description.size() <= 20, "description is too long"); + members(get_self()).get_member(eden_account); + + sessions_table_type table(get_self(), default_scope); + auto sc = table.find(eden_account.value); + if (sc == table.end()) + { + table.emplace(get_self(), [&](auto& sc) { + sc.eden_account() = eden_account; + sc.earliest_expiration() = expiration; + sc.sessions().push_back(session_v0{ + .key = key, + .expiration = expiration, + .description = description, + }); + }); + } + else + { + table.modify(sc, get_self(), [&](auto& sc) { + expire(get_self(), sc); + auto& sessions = sc.sessions(); + auto session = std::find_if(sessions.begin(), sessions.end(), + [&](auto& session) { return session.key == key; }); + eosio::check(session == sessions.end(), "session key already exists"); + sessions.push_back(session_v0{ + .key = key, + .expiration = expiration, + .description = description, + }); + if (sessions.size() > 4) + sessions.erase(sessions.begin()); + set_expiration(sc); + }); + } + push_event(session_new_event{eden_account, key, expiration, description}, get_self()); + } // eden::newsession + + void eden::delsession(const eosio::not_in_abi& current_session, + eosio::name eden_account, + const eosio::public_key& key) + { + current_session.value.require_auth(eden_account); + sessions_table_type table(get_self(), default_scope); + auto sc = table.find(eden_account.value); + eosio::check(sc != table.end(), "Session key is either expired or not found"); + bool empty = false; + table.modify(sc, get_self(), [&](auto& sc) { + auto& sessions = sc.sessions(); + auto session = std::find_if(sessions.begin(), sessions.end(), + [&](auto& session) { return session.key == key; }); + eosio::check(session != sessions.end(), "Session key is either expired or not found"); + push_event(session_del_event{sc.eden_account(), session->key}, get_self()); + sessions.erase(session); + expire(get_self(), sc); + set_expiration(sc); + empty = sessions.empty(); + }); + if (empty) + table.erase(sc); + } // eden::delsession + + void eden::run(eosio::ignore auth, eosio::ignore> verbs) + { + auto& ds = get_datastream(); + eosio::name eden_account; + eosio::varuint32 auth_type; + ds >> auth_type; + if (auth_type.value == (int)run_auth_type::no_auth) + { + } + else if (auth_type.value == (int)run_auth_type::account_auth) + { + account_auth a; + ds >> a; + eosio::check(a.contract == get_self(), "unsupported contract for auth"); + eosio::check(a.contract_account == a.eosio_account, + "account recovery not yet implemented"); + eosio::require_auth(a.eosio_account); + eden_account = a.contract_account; + } + else if (auth_type.value == (int)run_auth_type::signature_auth) + { + eosio::signature signature; + eosio::name contract; + eosio::varuint32 sequence; + ds >> signature; + auto digest = eosio::sha256(ds.pos(), ds.remaining()); + auto recovered = eosio::recover_key(digest, signature); + ds >> contract; + ds >> eden_account; + ds >> sequence; + eosio::check(contract == get_self(), "unsupported contract for auth"); + + sessions_table_type table(get_self(), default_scope); + auto sc = table.find(eden_account.value); + if (sc == table.end()) + eosio::check(false, "Recovered session key " + public_key_to_string(recovered) + + " is either expired or not found"); + table.modify(sc, get_self(), [&](auto& sc) { + expire(get_self(), sc); + set_expiration(sc); + auto& sessions = sc.sessions(); + auto session = std::find_if(sessions.begin(), sessions.end(), + [&](auto& session) { return session.key == recovered; }); + if (session == sessions.end()) + eosio::check(false, "Recovered session key " + public_key_to_string(recovered) + + " is either expired or not found"); + + auto& sequences = session->sequences; + if (sequences.begin() != sequences.end()) + { + if (sequence.value < *sequences.begin() && sequences.size() >= 20) + eosio::check(false, + "received duplicate sequence " + std::to_string(sequence.value)); + else if (sequence.value > sequences.end()[-1].value + 10) + eosio::check(false, + "sequence " + std::to_string(sequence.value) + " skips too many"); + } + else if (sequence.value > 10) + eosio::check(false, + "sequence " + std::to_string(sequence.value) + " skips too many"); + auto it = std::lower_bound(sequences.begin(), sequences.end(), sequence); + if (it != sequences.end() && *it == sequence) + eosio::check(false, "received duplicate sequence " + std::to_string(sequence.value)); + sequences.insert(it, sequence); + if (sequences.size() > 20) + sequences.erase(sequences.begin()); + }); + } + else + { + eosio::check(false, "unsupported auth type"); + } + + eosio::not_in_abi current_session; + current_session.value.authorized_eden_account = eden_account; + + eosio::varuint32 num_verbs; + ds >> num_verbs; + eosio::check(num_verbs.value > 0, "verbs is empty"); + for (uint32_t i = 0; i < num_verbs.value; ++i) + { + eosio::varuint32 index; + ds >> index; + eosio::check(actions::session_dispatch(get_self(), index.value, current_session, ds), + "unsupported verb index"); + } + + eosio::check(!ds.remaining(), "detected extra verb data"); + } // eden::run +} // namespace eden diff --git a/contracts/eden/src/eden-micro-chain.cpp b/contracts/eden/src/eden-micro-chain.cpp index 294b32456..95b986a22 100644 --- a/contracts/eden/src/eden-micro-chain.cpp +++ b/contracts/eden/src/eden-micro-chain.cpp @@ -12,6 +12,7 @@ #include #include #include +#include using namespace eosio::literals; @@ -32,6 +33,17 @@ const eosio::name account_max = eosio::name{~uint64_t(0)}; const eosio::block_timestamp block_timestamp_min = eosio::block_timestamp{0}; const eosio::block_timestamp block_timestamp_max = eosio::block_timestamp{~uint32_t(0)}; +const eosio::ecc_public_key ecc_public_key_min = {}; + +const eosio::ecc_public_key ecc_public_key_max = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +const eosio::public_key public_key_min_k1{std::in_place_index_t<0>{}, ecc_public_key_min}; +const eosio::public_key public_key_max_r1{std::in_place_index_t<1>{}, ecc_public_key_max}; + // TODO: switch to uint64_t (js BigInt) after we upgrade to nodejs >= 15 extern "C" void __wasm_call_ctors(); [[clang::export_name("initialize")]] void initialize(uint32_t eden_account_low, @@ -158,6 +170,7 @@ enum tables balance_history_table, induction_table, member_table, + session_table, election_table, election_round_table, election_group_table, @@ -165,6 +178,7 @@ enum tables distribution_table, distribution_fund_table, nft_table, + encryption_key_table, }; struct Induction; @@ -217,6 +231,13 @@ struct status eosio::block_timestamp nextElection; uint16_t electionThreshold = 0; uint16_t numElectionParticipants = 0; + uint16_t migrationIndex = 0; + + template + bool isMigrationCompleted() const + { + return migrationIndex >= boost::mp11::mp_find::value; + } }; struct status_object : public chainbase::object @@ -307,6 +328,21 @@ using balance_history_index = mic; +struct encryption_key_object : public chainbase::object +{ + CHAINBASE_DEFAULT_CONSTRUCTOR(encryption_key_object) + + id_type id; + eosio::name account; + eosio::public_key encryptionKey; + + eosio::name by_pk() const { return account; } +}; + +using encryption_key_index = mic, + ordered_by_pk>; + struct induction { uint64_t id = 0; @@ -366,6 +402,23 @@ using member_index = mic, ordered_by_createdAt>; +using SessionKey = std::tuple; + +struct session_object : public chainbase::object +{ + CHAINBASE_DEFAULT_CONSTRUCTOR(session_object) + + id_type id; + eosio::name eden_account; + eosio::public_key key; + eosio::block_timestamp expiration; + std::string description; + + SessionKey by_pk() const { return {eden_account, key}; } +}; +using session_index = + mic, ordered_by_pk>; + struct election_object : public chainbase::object { CHAINBASE_DEFAULT_CONSTRUCTOR(election_object) @@ -518,8 +571,10 @@ struct database chainbase::generic_index status; chainbase::generic_index balances; chainbase::generic_index balance_history; + chainbase::generic_index encryption_keys; chainbase::generic_index inductions; chainbase::generic_index members; + chainbase::generic_index sessions; chainbase::generic_index elections; chainbase::generic_index election_rounds; chainbase::generic_index election_groups; @@ -533,8 +588,10 @@ struct database db.add_index(status); db.add_index(balances); db.add_index(balance_history); + db.add_index(encryption_keys); db.add_index(inductions); db.add_index(members); + db.add_index(sessions); db.add_index(elections); db.add_index(election_rounds); db.add_index(election_groups); @@ -700,6 +757,32 @@ BalanceHistoryConnection Balance::history(std::optional [](auto& balance_history, auto key) { return balance_history.upper_bound(key); }); } +struct EncryptionKey +{ + eosio::name _account; + const encryption_key_object* obj; + + Member account() const; + std::optional encryptionKey() const + { + return obj ? std::optional{eosio::public_key_to_string(obj->encryptionKey)} : std::nullopt; + } +}; +EOSIO_REFLECT2(EncryptionKey, account, encryptionKey) + +constexpr const char EncryptionKeyConnection_name[] = "EncryptionKeyConnection"; +constexpr const char EncryptionKeyEdge_name[] = "EncryptionKeyEdge"; +using EncryptionKeyConnection = clchain::Connection< + clchain::ConnectionConfig>; + +EncryptionKey get_encryption_key(eosio::name account) +{ + if (auto* obj = get_ptr(db.encryption_keys, account)) + return EncryptionKey{account, obj}; + else + return EncryptionKey{account, nullptr}; +} + struct Member { eosio::name account; @@ -716,6 +799,11 @@ struct Member bool participating() const { return member && member->participating; } eosio::block_timestamp createdAt() const { return member->createdAt; } + std::optional encryptionKey() const + { + return get_encryption_key(account).encryptionKey(); + } + NftConnection nfts(std::optional gt, std::optional ge, std::optional lt, @@ -761,6 +849,7 @@ EOSIO_REFLECT2( inductionVideo, participating, createdAt, + encryptionKey, method(nfts, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(collectedNfts, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(elections, "gt", "ge", "lt", "le", "first", "last", "before", "after"), @@ -794,6 +883,22 @@ Member Balance::account() const return *get_member(_account, true); } +Member EncryptionKey::account() const +{ + return *get_member(_account, true); +} + +struct Session +{ + const session_object* session; + + auto member() const { return get_member(session->eden_account); } + const auto& key() const { return session->key; } + const auto& expiration() const { return session->expiration; } + const auto& description() const { return session->description; } +}; +EOSIO_REFLECT2(Session, member, key, expiration, description) + struct InductionEndorsingMemberStatus { eosio::name endorserAccount; @@ -1245,6 +1350,7 @@ void clearall() clear_table(db.balance_history); clear_table(db.inductions); clear_table(db.members); + clear_table(db.sessions); clear_table(db.elections); clear_table(db.election_rounds); clear_table(db.election_groups); @@ -1252,6 +1358,12 @@ void clearall() clear_table(db.distributions); clear_table(db.distribution_funds); clear_table(db.nfts); + clear_table(db.encryption_keys); +} + +void delsession(eosio::name eden_account, const eosio::public_key& key) +{ + // ignored; events handle session creation and deletion } eosio::asset add_balance(eosio::name account, const eosio::asset& delta) @@ -1454,6 +1566,14 @@ void inductprofil(uint64_t id, eden::new_member_profile profile) }); } +void inductmeetin(eosio::name account, + uint64_t id, + const std::vector& keys, + const eosio::bytes& data, + const std::optional& old_data) +{ +} + void inductvideo(eosio::name account, uint64_t id, std::string video) { modify(db.inductions, id, [&](auto& obj) { @@ -1568,6 +1688,14 @@ void electvote(uint8_t round, eosio::name voter, eosio::name candidate) db.votes.modify(vote, [&](auto& vote) { vote.candidate = candidate; }); } +void electmeeting(eosio::name account, + uint8_t round, + const std::vector& keys, + const eosio::bytes& data, + const std::optional& old_data) +{ +} + void electvideo(uint8_t round, eosio::name voter, const std::string& video) { auto& election_idx = db.elections.get(); @@ -1579,6 +1707,14 @@ void electvideo(uint8_t round, eosio::name voter, const std::string& video) db.votes.modify(*vote, [&](auto& vote) { vote.video = video; }); } +void setencpubkey(eosio::name member, eosio::public_key key) +{ + add_or_modify(db.encryption_keys, member, [&](bool is_new, auto& row) { + row.account = member; + row.encryptionKey = key; + }); +} + void logmint(const action_context& context, uint64_t asset_id, eosio::name authorized_minter, @@ -1644,12 +1780,22 @@ void logtransfer(const action_context& context, } } +void handle_event(const eden::migration_event& event) +{ + db.status.modify(get_status(), + [&](auto& status) { status.status.migrationIndex = event.index; }); +} + void handle_event(const eden::election_event_schedule& event) { db.status.modify(get_status(), [&](auto& status) { status.status.nextElection = event.election_time; status.status.electionThreshold = event.election_threshold; }); + for (auto& member : db.members) + { + db.members.modify(member, [&](auto& member) { member.member.participating = false; }); + } } void handle_event(const eden::election_event_begin& event) @@ -1819,6 +1965,21 @@ void handle_event(const eden::distribution_event_fund& event) }); } +void handle_event(const eden::session_new_event& event) +{ + db.sessions.emplace([&](auto& session) { + session.eden_account = event.eden_account; + session.key = event.key; + session.expiration = event.expiration; + session.description = event.description; + }); +} + +void handle_event(const eden::session_del_event& event) +{ + remove_if_exists(db.sessions, SessionKey{event.eden_account, event.key}); +} + void handle_event(const auto& event) {} void handle_event(const action_context& context, const auto& event) @@ -1832,10 +1993,9 @@ void handle_event(const action_context& context, const eden::event& event) } template -void call(void (*f)(Args...), const action_context& context, const std::vector& data) +void call(void (*f)(Args...), const action_context& context, eosio::input_stream& s) { std::tuple...> t; - eosio::input_stream s(data); // TODO: prevent abort, indicate what failed eosio::from_bin(t, s); std::apply([f](auto&&... args) { f(std::move(args)...); }, t); @@ -1844,28 +2004,20 @@ void call(void (*f)(Args...), const action_context& context, const std::vector void call(void (*f)(const action_context&, Args...), const action_context& context, - const std::vector& data) + eosio::input_stream& s) { std::tuple...> t; - eosio::input_stream s(data); // TODO: prevent abort, indicate what failed eosio::from_bin(t, s); std::apply([&](auto&&... args) { f(context, std::move(args)...); }, t); } -void remove_expired_inductions(const subchain::eosio_block& block) +void remove_expired_inductions(const eosio::time_point& block_time, const status& status) { - auto& idx = db.status.get(); - if (idx.size() < 1) - return; // skip if genesis is not complete + if (status.isMigrationCompleted()) + return; // skip if migration is not ready to collect expired records - const auto& status = get_status(); - if (!status.status.active) - return; // skip if not active - - auto expiration_time = - eosio::block_timestamp(block.timestamp).to_time_point().sec_since_epoch() - - eden::induction_expiration_secs; + auto expiration_time = block_time.sec_since_epoch() - eden::induction_expiration_secs; auto& index = db.inductions.get(); auto it = index.begin(); @@ -1880,6 +2032,92 @@ void remove_expired_inductions(const subchain::eosio_block& block) } } +void clean_data(const subchain::eosio_block& block) +{ + auto& idx = db.status.get(); + if (idx.size() < 1) + return; // skip if genesis is not complete + + const auto& status = get_status(); + if (!status.status.active) + return; // skip if contract is not active + + remove_expired_inductions(block.timestamp, status.status); +} + +bool dispatch(eosio::name action_name, const action_context& context, eosio::input_stream& s); + +void run(const action_context& context, eosio::input_stream& s) +{ + eden::run_auth auth; + eosio::varuint32 num_verbs; + from_bin(auth, s); + from_bin(num_verbs, s); + for (uint32_t i = 0; i < num_verbs.value; ++i) + { + auto index = eosio::varuint32_from_bin(s); + auto name = eden::actions::get_name_for_session_action(index); + if (!dispatch(name, context, s)) + // fatal because this throws off the rest of the stream + eosio::check(false, + "run: verb not found: " + std::to_string(index) + " " + name.to_string()); + } + eosio::check(!s.remaining(), "unpack error (extra data) within run"); +} + +bool dispatch(eosio::name action_name, const action_context& context, eosio::input_stream& s) +{ + if (action_name == "run"_n) + run(context, s); + else if (action_name == "clearall"_n) + call(clearall, context, s); + else if (action_name == "delsession"_n) + call(delsession, context, s); + else if (action_name == "withdraw"_n) + call(withdraw, context, s); + else if (action_name == "donate"_n) + call(donate, context, s); + else if (action_name == "transfer"_n) + call(transfer, context, s); + else if (action_name == "fundtransfer"_n) + call(fundtransfer, context, s); + else if (action_name == "usertransfer"_n) + call(usertransfer, context, s); + else if (action_name == "genesis"_n) + call(genesis, context, s); + else if (action_name == "addtogenesis"_n) + call(addtogenesis, context, s); + else if (action_name == "inductinit"_n) + call(inductinit, context, s); + else if (action_name == "inductprofil"_n) + call(inductprofil, context, s); + else if (action_name == "inductmeetin"_n) + call(inductmeetin, context, s); + else if (action_name == "inductvideo"_n) + call(inductvideo, context, s); + else if (action_name == "inductcancel"_n) + call(inductcancel, context, s); + else if (action_name == "inductdonate"_n) + call(inductdonate, context, s); + else if (action_name == "inductendors"_n) + call(inductendors, context, s); + else if (action_name == "resign"_n) + call(resign, context, s); + else if (action_name == "electopt"_n) + call(electopt, context, s); + else if (action_name == "electvote"_n) + call(electvote, context, s); + else if (action_name == "electmeeting"_n) + call(electmeeting, context, s); + else if (action_name == "electvideo"_n) + call(electvideo, context, s); + else if (action_name == "setencpubkey"_n) + call(setencpubkey, context, s); + else + return false; + return true; +} + void filter_block(const subchain::eosio_block& block) { block_state block_state{}; @@ -1890,46 +2128,15 @@ void filter_block(const subchain::eosio_block& block) action_context context{block, block_state, trx, action}; if (action.firstReceiver == eden_account) { - if (action.name == "clearall"_n) - call(clearall, context, action.hexData.data); - else if (action.name == "withdraw"_n) - call(withdraw, context, action.hexData.data); - else if (action.name == "donate"_n) - call(donate, context, action.hexData.data); - else if (action.name == "transfer"_n) - call(transfer, context, action.hexData.data); - else if (action.name == "fundtransfer"_n) - call(fundtransfer, context, action.hexData.data); - else if (action.name == "usertransfer"_n) - call(usertransfer, context, action.hexData.data); - else if (action.name == "genesis"_n) - call(genesis, context, action.hexData.data); - else if (action.name == "addtogenesis"_n) - call(addtogenesis, context, action.hexData.data); - else if (action.name == "inductinit"_n) - call(inductinit, context, action.hexData.data); - else if (action.name == "inductprofil"_n) - call(inductprofil, context, action.hexData.data); - else if (action.name == "inductvideo"_n) - call(inductvideo, context, action.hexData.data); - else if (action.name == "inductcancel"_n) - call(inductcancel, context, action.hexData.data); - else if (action.name == "inductdonate"_n) - call(inductdonate, context, action.hexData.data); - else if (action.name == "inductendors"_n) - call(inductendors, context, action.hexData.data); - else if (action.name == "resign"_n) - call(resign, context, action.hexData.data); - else if (action.name == "electopt"_n) - call(electopt, context, action.hexData.data); - else if (action.name == "electvote"_n) - call(electvote, context, action.hexData.data); - else if (action.name == "electvideo"_n) - call(electvideo, context, action.hexData.data); + eosio::input_stream s(action.hexData.data); + dispatch(action.name, context, s); } else if (action.firstReceiver == token_account && action.receiver == eden_account && action.name == "transfer"_n) - call(notify_transfer, context, action.hexData.data); + { + eosio::input_stream s(action.hexData.data); + call(notify_transfer, context, s); + } else if (action.firstReceiver == "eosio.null"_n && action.name == "eden.events"_n && action.creatorAction && action.creatorAction->receiver == eden_account) { @@ -1940,15 +2147,16 @@ void filter_block(const subchain::eosio_block& block) } else if (action.firstReceiver == atomic_account && action.receiver == eden_account) { + eosio::input_stream s(action.hexData.data); if (action.name == "logmint"_n) - call(logmint, context, action.hexData.data); + call(logmint, context, s); else if (action.name == "logtransfer"_n) - call(logtransfer, context, action.hexData.data); + call(logtransfer, context, s); } } // for(action) // garbage collection housekeeping - remove_expired_inductions(block); + clean_data(block); eosio::check(!block_state.in_withdraw && !block_state.in_manual_transfer, "missing transfer notification"); @@ -2200,6 +2408,11 @@ constexpr const char MemberEdge_name[] = "MemberEdge"; using MemberConnection = clchain::Connection>; +constexpr const char SessionConnection_name[] = "SessionConnection"; +constexpr const char SessionEdge_name[] = "SessionEdge"; +using SessionConnection = clchain::Connection< + clchain::ConnectionConfig>; + constexpr const char ElectionConnection_name[] = "ElectionConnection"; constexpr const char ElectionEdge_name[] = "ElectionEdge"; using ElectionConnection = clchain::Connection< @@ -2245,6 +2458,26 @@ struct Query Balance masterPool() const { return get_balance(master_pool); } Balance distributionFund() const { return get_balance(distribution_fund); } + EncryptionKeyConnection encryptionKeys(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const + { + return clchain::make_connection( + gt, ge, lt, le, first, last, before, after, // + db.encryption_keys.get(), // + [](auto& obj) { return obj.account; }, // + [](auto& obj) { + return EncryptionKey{obj.account, &obj}; + }, + [](auto& encryption_keys, auto key) { return encryption_keys.lower_bound(key); }, + [](auto& encryption_keys, auto key) { return encryption_keys.upper_bound(key); }); + } + MemberConnection members(std::optional gt, std::optional ge, std::optional lt, @@ -2293,6 +2526,32 @@ struct Query [](auto& members, auto key) { return members.upper_bound(key); }); } + SessionConnection sessions(std::optional gt, + std::optional ge, + std::optional lt, + std::optional le, + std::optional first, + std::optional last, + std::optional before, + std::optional after) const + { + return clchain::make_connection( + gt ? std::optional{SessionKey{*gt, public_key_max_r1}} // + : std::nullopt, // + ge ? std::optional{SessionKey{*ge, public_key_min_k1}} // + : std::nullopt, // + lt ? std::optional{SessionKey{*lt, public_key_min_k1}} // + : std::nullopt, // + le ? std::optional{SessionKey{*le, public_key_max_r1}} // + : std::nullopt, // + first, last, before, after, // + db.sessions.get(), // + [](auto& obj) { return obj.by_pk(); }, // + [](auto& obj) { return Session{&obj}; }, + [](auto& sessions, auto key) { return sessions.lower_bound(key); }, + [](auto& sessions, auto key) { return sessions.upper_bound(key); }); + } + InductionConnection inductions(std::optional gt, std::optional ge, std::optional lt, @@ -2384,8 +2643,10 @@ EOSIO_REFLECT2( masterPool, distributionFund, method(balances, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(encryptionKeys, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(members, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(membersByCreatedAt, "gt", "ge", "lt", "le", "first", "last", "before", "after"), + method(sessions, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(inductions, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(inductionsByCreatedAt, "gt", "ge", "lt", "le", "first", "last", "before", "after"), method(elections, "gt", "ge", "lt", "le", "first", "last", "before", "after"), diff --git a/contracts/eden/src/eden.cpp b/contracts/eden/src/eden.cpp index 5b1ae2172..dddaee508 100644 --- a/contracts/eden/src/eden.cpp +++ b/contracts/eden/src/eden.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -48,7 +49,10 @@ EOSIO_ABIGEN( "FLOAT_VEC", "DOUBLE_VEC", "STRING_VEC"), + variant("run_auth", eden::run_auth), + variant("verb", eden::verb), actions(eden::actions), + eden_verbs(eden::actions, "verb", "unsupported_verb"), table("account"_n, eden::account_variant), table("auction"_n, eden::auction_variant), table("bylaws"_n, eden::bylaws_variant), @@ -64,6 +68,7 @@ EOSIO_ABIGEN( table("memberstats"_n, eden::member_stats_variant), table("migration"_n, eden::migration_variant), table("pools"_n, eden::pool_variant), + table("sessions"_n, eden::session_container_variant), table("votes"_n, eden::vote), ricardian_clause("peacetreaty", eden::peacetreaty_clause), ricardian_clause("bylaws", eden::bylaws_clause)) diff --git a/contracts/eden/src/elections.cpp b/contracts/eden/src/elections.cpp index 0fac12fd5..71597ad26 100644 --- a/contracts/eden/src/elections.cpp +++ b/contracts/eden/src/elections.cpp @@ -201,13 +201,13 @@ namespace eden const auto* old_value = state_sing.get_or_null(); if (old_value && *old_value == new_value) return; - if (auto n = std::get_if(&new_value)) + if (auto n = std::get_if(&new_value)) { push_event(election_event_schedule{.election_time = n->start_time, .election_threshold = n->election_threshold}, contract); } - else if (auto n = std::get_if(&new_value)) + else if (auto n = std::get_if(&new_value)) { push_event(election_event_seeding{.election_time = n->seed.end_time, .start_time = n->seed.start_time, @@ -280,30 +280,58 @@ namespace eden return {}; } auto state = state_sing.get(); - if (auto* r = std::get_if(&state)) + if (auto* r = get_if_derived(&state)) { return r->start_time; } - else if (auto* s = std::get_if(&state)) + else if (auto* s = get_if_derived(&state)) { return s->seed.end_time.to_time_point(); } return {}; } + std::uint8_t elections::election_schedule_version() + { + if (!state_sing.exists()) + { + return 1; + } + auto state = state_sing.get(); + if (auto* r = get_if_derived(&state)) + { + return r->election_schedule_version; + } + else if (auto* s = get_if_derived(&state)) + { + return s->election_schedule_version; + } + else if (auto* i = get_if_derived(&state)) + { + return i->election_schedule_version; + } + return 1; + } + void elections::set_next_election_time(eosio::time_point election_time) { auto lock_time = eosio::current_time_point() + eosio::days(30); eosio::check(election_time >= lock_time, "New election time is too close"); + uint8_t sequence = 1; if (state_sing.exists()) { auto state = state_sing.get(); - eosio::check( - std::holds_alternative(state) && - std::get(state).start_time >= lock_time, - "Election cannot be rescheduled"); + bool okay = false; + if (auto r = get_if_derived(&state)) + { + sequence = r->election_schedule_version + 1; + eosio::check(sequence != 0, "Integer overflow: election rescheduled too many times"); + okay = r->start_time >= lock_time; + } + eosio::check(okay, "Election cannot be rescheduled"); } - set_state_sing(current_election_state_registration{election_time, max_active_members + 1}); + set_state_sing( + current_election_state_registration_v1{election_time, max_active_members + 1, sequence}); } void elections::set_time(uint8_t day, const std::string& time) @@ -338,7 +366,7 @@ namespace eden : 0; uint16_t new_threshold = active_members + (active_members + 9) / 10; new_threshold = std::clamp(new_threshold, min_election_threshold, max_active_members); - set_state_sing(current_election_state_registration{ + set_state_sing(current_election_state_registration_v1{ get_election_time(state.election_start_time, origin_time + eosio::days(180)), new_threshold}); } @@ -363,14 +391,16 @@ namespace eden // Ignore events that would trigger an election unless they move // the next election closer. auto current_state = state_sing.get(); - if (auto* current = std::get_if(¤t_state)) + if (auto* current = get_if_derived(¤t_state)) { auto new_start_time = eosio::block_timestamp{ get_election_time(state.election_start_time, now + eosio::days(30))}; if (new_start_time < current->start_time) { - set_state_sing( - current_election_state_registration{new_start_time, max_active_members + 1}); + uint8_t sequence = current->election_schedule_version + 1; + eosio::check(sequence != 0, "Integer overflow: election rescheduled too many times"); + set_state_sing(current_election_state_registration_v1{ + new_start_time, max_active_members + 1, sequence}); } } } @@ -380,7 +410,7 @@ namespace eden { eosio::check(btc_header.data.size() == 80, "Wrong size for BTC block header"); auto state = state_sing.get(); - if (auto* registration = std::get_if(&state)) + if (auto* registration = get_if_derived(&state)) { auto now = eosio::current_block_time(); eosio::block_timestamp seeding_start = @@ -391,10 +421,11 @@ namespace eden .election_time = registration->start_time, }, contract); - state = current_election_state_seeding{ - {.start_time = seeding_start, .end_time = registration->start_time.to_time_point()}}; + state = current_election_state_seeding_v1{ + {{.start_time = seeding_start, .end_time = registration->start_time.to_time_point()}, + registration->election_schedule_version}}; } - if (auto* seeding = std::get_if(&state)) + if (auto* seeding = get_if_derived(&state)) { eosio::input_stream stream{btc_header.data}; seeding->seed.update(stream); @@ -413,13 +444,15 @@ namespace eden void elections::start_election() { - eosio::check(std::holds_alternative(state_sing.get()), - "Election seed not set"); - auto old_state = std::get(state_sing.get()); + auto old_state_variant = state_sing.get(); + auto old_state_ptr = get_if_derived(&old_state_variant); + eosio::check(old_state_ptr != nullptr, "Election seed not set"); + auto& old_state = *old_state_ptr; auto election_start_time = old_state.seed.end_time.to_time_point(); eosio::check(eosio::current_block_time() >= old_state.seed.end_time, "Seeding window is still open"); - set_state_sing(current_election_state_init_voters{0, election_rng{old_state.seed.current}}); + set_state_sing(current_election_state_init_voters_v1{ + 0, election_rng{old_state.seed.current}, {}, 0, old_state.election_schedule_version}); push_event(election_event_end_seeding{.election_time = election_start_time}, contract); // Must happen after the election is started @@ -439,7 +472,7 @@ namespace eden bylaws.new_board(); } - uint32_t elections::randomize_voters(current_election_state_init_voters& state, + uint32_t elections::randomize_voters(current_election_state_init_voters_v0& state, uint32_t max_steps) { members members(contract); @@ -450,18 +483,13 @@ namespace eden { if (iter->status() == member_status::active_member) { - switch (iter->election_participation_status()) + if (iter->election_participation_status() == state.election_schedule_version) { - case in_election: - { - add_voter(state.rng, 0, state.next_member_idx, iter->account()); - break; - } - case not_in_election: - { - members.set_rank(iter->account(), 0, eosio::name(-1)); - break; - } + add_voter(state.rng, 0, state.next_member_idx, iter->account()); + } + else + { + members.set_rank(iter->account(), 0, eosio::name(-1)); } } state.last_processed = iter->account(); @@ -495,7 +523,7 @@ namespace eden uint32_t elections::prepare_election(uint32_t max_steps) { auto state_variant = state_sing.get(); - if (auto* state = std::get_if(&state_variant)) + if (auto* state = get_if_derived(&state_variant)) { if (max_steps == 0) { @@ -505,7 +533,7 @@ namespace eden state_variant = state_sing.get(); --max_steps; } - if (auto* state = std::get_if(&state_variant)) + if (auto* state = get_if_derived(&state_variant)) { // This needs to happen before any members have their ranks adjusted max_steps = distribute_monthly(contract, max_steps); @@ -919,7 +947,6 @@ namespace eden void elections::vote(uint8_t round, eosio::name voter, eosio::name candidate) { - eosio::require_auth(voter); const auto& state = check_active(); eosio::check(state.round == round, "Round " + std::to_string(static_cast(round)) + " is not running (in round " + @@ -966,15 +993,27 @@ namespace eden return false; } + static bool is_election_running(current_election_state_singleton& state_sing) + { + if (!state_sing.exists()) + { + return false; + } + auto state = state_sing.get(); + if (std::holds_alternative(state_sing.get())) + { + return false; + } + if (auto* r = get_if_derived(&state)) + { + return eosio::current_block_time() >= r->start_time; + } + return true; + } + void elections::on_resign(eosio::name member) { - eosio::check( - !state_sing.exists() || - std::holds_alternative(state_sing.get()) || - (std::holds_alternative(state_sing.get()) && - std::get(state_sing.get()).start_time > - eosio::current_block_time()), - "Cannot resign during an election"); + eosio::check(!is_election_running(state_sing), "Cannot resign during an election"); if (remove_from_board(member)) { trigger_election(); @@ -987,8 +1026,7 @@ namespace eden if (iter == vote_tb.end()) { auto current_state = state_sing.get(); - bool valid_state = - !std::holds_alternative(current_state); + bool valid_state = !get_if_derived(¤t_state); election_state_singleton state{contract, default_scope}; auto end_time = std::get(state.get()).last_election_time.to_time_point() + diff --git a/contracts/eden/src/members.cpp b/contracts/eden/src/members.cpp index a7dde1e57..bd32617eb 100644 --- a/contracts/eden/src/members.cpp +++ b/contracts/eden/src/members.cpp @@ -1,5 +1,6 @@ #include #include +#include namespace eden { @@ -102,6 +103,7 @@ namespace eden break; } member_stats.set(stats, contract); + remove_sessions(contract, iter->account()); return member_tb.erase(iter); } @@ -117,6 +119,7 @@ namespace eden const auto& member = member_tb.get(account.value); if (member.status() == member_status::pending_membership) { + remove_sessions(contract, account); member_tb.erase(member); auto stats = this->stats(); eosio::check(stats.pending_members != 0, "Integer overflow"); @@ -150,7 +153,7 @@ namespace eden .name = name, .status = member_status::active_member, .nft_template_id = row.nft_template_id(), - .election_participation_status = not_in_election}}; + .election_participation_status = 0}}; }); } @@ -160,7 +163,7 @@ namespace eden row.value = std::visit([](auto& v) { return member_v1{v}; }, row.value); row.election_rank() = rank; row.representative() = representative; - row.election_participation_status() = not_in_election; + row.election_participation_status() = 0; }); auto stats = this->stats(); if (representative != eosio::name(-1)) @@ -192,9 +195,24 @@ namespace eden election_time->to_time_point(), "Registration has closed"); + uint8_t new_participation_status; + if (participating) + { + new_participation_status = elections.election_schedule_version(); + eosio::check(member.election_participation_status() != new_participation_status, + "Not currently opted out"); + } + else + { + new_participation_status = not_in_election; + eosio::check( + member.election_participation_status() == elections.election_schedule_version(), + "Not currently opted in"); + } + member_tb.modify(member, eosio::same_payer, [&](auto& row) { row.value = std::visit([](auto& v) { return member_v1{v}; }, row.value); - row.election_participation_status() = participating ? in_election : not_in_election; + row.election_participation_status() = new_participation_status; }); } diff --git a/contracts/eden/src/migrations.cpp b/contracts/eden/src/migrations.cpp index 2d19f85d5..7b8185656 100644 --- a/contracts/eden/src/migrations.cpp +++ b/contracts/eden/src/migrations.cpp @@ -1,3 +1,4 @@ +#include #include namespace eden @@ -13,9 +14,9 @@ namespace eden void migrations::init() { - migration_sing.set(std::variant_alternative_t - 1, - migration_variant>(), - contract); + constexpr size_t index = std::variant_size_v - 1; + migration_sing.set(std::variant_alternative_t(), contract); + push_event(migration_event{static_cast(index)}, contract); } uint32_t migrations::migrate_some(uint32_t max_steps) @@ -28,6 +29,8 @@ namespace eden max_steps = current_state.migrate_some(contract, max_steps); if (max_steps) { + push_event(migration_event{static_cast(state.index())}, + contract); constexpr std::size_t next_index = boost::mp11::mp_find>::value + diff --git a/contracts/eden/tests/data/contract-auth-elect.expected b/contracts/eden/tests/data/contract-auth-elect.expected new file mode 100644 index 000000000..0ab06ac27 --- /dev/null +++ b/contracts/eden/tests/data/contract-auth-elect.expected @@ -0,0 +1,491 @@ +[ + "migration_event", + { + "index": 5 + } +] +[ + "set_pool_event", + { + "pool": "master", + "monthly_distribution_pct": 5 + } +] +[ + "election_event_schedule", + { + "election_time": "2020-07-04T15:30:00.000", + "election_threshold": 1000 + } +] +[ + "session_new_event", + { + "eden_account": "alice", + "key": "PUB_K1_665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vAtJaV", + "expiration": "2020-03-31T00:01:40.500", + "description": "" + } +] +[ + "session_new_event", + { + "eden_account": "pip", + "key": "PUB_K1_8YQhKe3x1xTA1KHmkBPznWqa3UGQsaHTUMkJJtcds9giKNsHGv", + "expiration": "2020-03-31T00:01:40.500", + "description": "" + } +] +[ + "session_new_event", + { + "eden_account": "egeon", + "key": "PUB_K1_8kBx4XYj3zZ3Z1Sb8vdq43ursVTebfcShKMDUymiA2cteLVLM1", + "expiration": "2020-03-31T00:01:40.500", + "description": "" + } +] +[ + "election_event_begin", + { + "election_time": "2020-07-04T15:30:00.000" + } +] +[ + "election_event_seeding", + { + "election_time": "2020-07-04T15:30:00.000", + "start_time": "2020-07-03T15:30:00.000", + "end_time": "2020-07-04T15:30:00.000", + "seed": "79BC69E1EF2D91BDFD54CF74E8B3784EEDC110F7A0BA571297FAD90DEC3B97C7" + } +] +[ + "election_event_end_seeding", + { + "election_time": "2020-07-04T15:30:00.000" + } +] +[ + "distribution_event_schedule", + { + "distribution_time": "2020-07-04T15:30:00.000" + } +] +[ + "distribution_event_reserve", + { + "distribution_time": "2020-07-04T15:30:00.000", + "pool": "master", + "target_amount": "51.5000 EOS" + } +] +[ + "distribution_event_schedule", + { + "distribution_time": "2020-08-03T15:30:00.000" + } +] +[ + "election_event_config_summary", + { + "election_time": "2020-07-04T15:30:00.000", + "num_rounds": 3, + "num_participants": 103 + } +] +[ + "election_event_create_round", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "requires_voting": true, + "num_participants": 103, + "num_groups": 25 + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember2p", + "edenmember43", + "edenmember2l", + "edenmember1m", + "edenmember3q" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember44", + "edenmember3t", + "edenmember12", + "edenmember22", + "edenmember1n" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember21", + "edenmember3d", + "edenmember13", + "edenmember3w", + "edenmember2q" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember2n", + "edenmember3g", + "edenmember1o", + "edenmember2t" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember3p", + "edenmember1k", + "edenmember2b", + "edenmember3z" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember3a", + "edenmember1v", + "edenmember3s", + "edenmember15" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember1a", + "edenmember3j", + "edenmember1t", + "edenmember1z" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember3b", + "edenmember23", + "edenmember2m", + "edenmember2i" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember3x", + "edenmember3h", + "edenmember24", + "edenmember1q" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember1j", + "alice", + "edenmember1s", + "edenmember1b" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember1h", + "edenmember4b", + "edenmember41", + "edenmember3u" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember1x", + "edenmember3l", + "edenmember35", + "edenmember1f" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "egeon", + "edenmember2u", + "edenmember2s", + "edenmember25" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember2d", + "edenmember2k", + "edenmember3m", + "edenmember3v" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember3o", + "edenmember33", + "edenmember3n", + "edenmember45" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember4a", + "edenmember2v", + "edenmember2o", + "edenmember1y" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember2f", + "edenmember2e", + "edenmember1i", + "edenmember2y" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember1c", + "edenmember32", + "edenmember3e", + "edenmember3k" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember3y", + "edenmember2w", + "edenmember2r", + "edenmember2x" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember3c", + "edenmember34", + "edenmember1e", + "edenmember1g" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember2j", + "edenmember2h", + "edenmember2a", + "edenmember3f" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember1p", + "edenmember31", + "edenmember3i", + "edenmember1d" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember11", + "edenmember42", + "edenmember3r", + "edenmember1r" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember1l", + "edenmember14", + "edenmember1w", + "pip" + ] + } +] +[ + "election_event_create_group", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voters": [ + "edenmember2c", + "edenmember2z", + "edenmember1u", + "edenmember2g" + ] + } +] +[ + "election_event_begin_round_voting", + { + "election_time": "2020-07-04T15:30:00.000", + "round": 0, + "voting_begin": "2020-07-04T15:40:00.000", + "voting_end": "2020-07-04T16:40:00.000" + } +] +[ + "session_del_event", + { + "eden_account": "alice", + "key": "PUB_K1_665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vAtJaV" + } +] +[ + "session_del_event", + { + "eden_account": "egeon", + "key": "PUB_K1_8kBx4XYj3zZ3Z1Sb8vdq43ursVTebfcShKMDUymiA2cteLVLM1" + } +] +[ + "session_del_event", + { + "eden_account": "pip", + "key": "PUB_K1_8YQhKe3x1xTA1KHmkBPznWqa3UGQsaHTUMkJJtcds9giKNsHGv" + } +] +[ + "session_new_event", + { + "eden_account": "alice", + "key": "PUB_K1_665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vAtJaV", + "expiration": "2020-10-02T15:40:00.000", + "description": "" + } +] +[ + "session_new_event", + { + "eden_account": "pip", + "key": "PUB_K1_8YQhKe3x1xTA1KHmkBPznWqa3UGQsaHTUMkJJtcds9giKNsHGv", + "expiration": "2020-10-02T15:40:00.000", + "description": "" + } +] +[ + "session_new_event", + { + "eden_account": "egeon", + "key": "PUB_K1_8kBx4XYj3zZ3Z1Sb8vdq43ursVTebfcShKMDUymiA2cteLVLM1", + "expiration": "2020-10-02T15:40:00.000", + "description": "" + } +] diff --git a/contracts/eden/tests/data/contract-auth-induct.expected b/contracts/eden/tests/data/contract-auth-induct.expected new file mode 100644 index 000000000..c4a01a7a0 --- /dev/null +++ b/contracts/eden/tests/data/contract-auth-induct.expected @@ -0,0 +1,63 @@ +[ + "migration_event", + { + "index": 5 + } +] +[ + "set_pool_event", + { + "pool": "master", + "monthly_distribution_pct": 5 + } +] +[ + "election_event_schedule", + { + "election_time": "2020-07-04T15:30:00.000", + "election_threshold": 1000 + } +] +[ + "session_new_event", + { + "eden_account": "alice", + "key": "PUB_K1_665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vAtJaV", + "expiration": "2020-03-31T00:00:00.500", + "description": "" + } +] +[ + "session_new_event", + { + "eden_account": "pip", + "key": "PUB_K1_8YQhKe3x1xTA1KHmkBPznWqa3UGQsaHTUMkJJtcds9giKNsHGv", + "expiration": "2020-03-31T00:00:00.500", + "description": "" + } +] +[ + "session_new_event", + { + "eden_account": "egeon", + "key": "PUB_K1_8kBx4XYj3zZ3Z1Sb8vdq43ursVTebfcShKMDUymiA2cteLVLM1", + "expiration": "2020-03-31T00:00:00.500", + "description": "" + } +] +[ + "session_new_event", + { + "eden_account": "bertie", + "key": "PUB_K1_5iALbhfqEZvqkUifUGbfMQSFnd1ui8ZsXVHT23XWh1HM4B1jNS", + "expiration": "2020-03-31T00:00:00.500", + "description": "" + } +] +[ + "session_del_event", + { + "eden_account": "bertie", + "key": "PUB_K1_5iALbhfqEZvqkUifUGbfMQSFnd1ui8ZsXVHT23XWh1HM4B1jNS" + } +] diff --git a/contracts/eden/tests/data/contract-auth.expected b/contracts/eden/tests/data/contract-auth.expected new file mode 100644 index 000000000..2305a73ad --- /dev/null +++ b/contracts/eden/tests/data/contract-auth.expected @@ -0,0 +1,52 @@ +[ + "migration_event", + { + "index": 5 + } +] +[ + "set_pool_event", + { + "pool": "master", + "monthly_distribution_pct": 5 + } +] +[ + "election_event_schedule", + { + "election_time": "2020-07-04T15:30:00.000", + "election_threshold": 1000 + } +] +[ + "session_new_event", + { + "eden_account": "alice", + "key": "PUB_K1_665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vAtJaV", + "expiration": "2020-03-31T00:00:00.500", + "description": "four score and seven" + } +] +[ + "session_new_event", + { + "eden_account": "alice", + "key": "PUB_K1_8VWTR1mogYHEd9HJxgG2Tj3GbPghrnJqMfWfdHbTE11BLxqvo3", + "expiration": "2020-03-31T00:00:00.500", + "description": "another session" + } +] +[ + "session_del_event", + { + "eden_account": "alice", + "key": "PUB_K1_665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vAtJaV" + } +] +[ + "session_del_event", + { + "eden_account": "alice", + "key": "PUB_K1_8VWTR1mogYHEd9HJxgG2Tj3GbPghrnJqMfWfdHbTE11BLxqvo3" + } +] diff --git a/contracts/eden/tests/data/test-election.expected b/contracts/eden/tests/data/test-election.expected index 0c71953b5..7a1229c9d 100644 --- a/contracts/eden/tests/data/test-election.expected +++ b/contracts/eden/tests/data/test-election.expected @@ -1,3 +1,9 @@ +[ + "migration_event", + { + "index": 5 + } +] [ "set_pool_event", { diff --git a/contracts/eden/tests/include/tester-base.hpp b/contracts/eden/tests/include/tester-base.hpp index 621b30cb2..4ca0814fc 100644 --- a/contracts/eden/tests/include/tester-base.hpp +++ b/contracts/eden/tests/include/tester-base.hpp @@ -202,6 +202,7 @@ struct eden_tester test_chain chain; user_context eosio_token = chain.as("eosio.token"_n); user_context eden_gm = chain.as("eden.gm"_n); + user_context payer = chain.as("payer"_n); user_context alice = chain.as("alice"_n); user_context pip = chain.as("pip"_n); user_context egeon = chain.as("egeon"_n); @@ -215,6 +216,7 @@ struct eden_tester chain.create_code_account("eden.gm"_n); f(); eden_setup(chain); + chain.create_account("payer"_n); for (auto account : {"alice"_n, "pip"_n, "egeon"_n, "bertie"_n, "ahab"_n}) { chain.create_account(account); @@ -257,6 +259,12 @@ struct eden_tester } } + auto hash_induction(const std::string& video, const eden::new_member_profile& profile) + { + auto hash_data = eosio::convert_to_bin(std::tuple(video, profile)); + return eosio::sha256(hash_data.data(), hash_data.size()); + } + void finish_induction(uint64_t induction_id, eosio::name inviter, eosio::name invitee, @@ -403,7 +411,7 @@ struct eden_tester } } - void run_election(bool auto_donate = true, uint32_t batch_size = 10000, bool add_video = false) + void start_election(bool auto_donate = true, uint32_t batch_size = 10000) { if (auto_donate) { @@ -414,6 +422,11 @@ struct eden_tester skip_to(next_election_time().to_time_point()); setup_election(batch_size); + } + + void run_election(bool auto_donate = true, uint32_t batch_size = 10000, bool add_video = false) + { + start_election(auto_donate, batch_size); uint8_t round = 0; @@ -507,6 +520,60 @@ struct eden_tester return result; }; + void newsession(eosio::name authorizer, + eosio::name eden_account, + const eosio::public_key& key, + eosio::block_timestamp expiration, + const std::string& description, + const char* expected = nullptr) + { + expect(chain.as(authorizer) + .trace(eden_account, key, expiration, description), + expected); + } + + void delsession(eosio::name authorizer, + eosio::name eden_account, + const eosio::public_key& key, + const char* expected = nullptr) + { + expect(chain.as(authorizer).trace(eden_account, key), expected); + } + + template + void run(const private_key& key, + eosio::name eden_account, + eosio::varuint32 sequence, + const char* expected, + const Ts&... verbs) + { + std::vector data; + vector_stream s{data}; + to_bin("eden.gm"_n, s); + to_bin(eden_account, s); + to_bin(sequence, s); + to_bin(eosio::varuint32(sizeof...(verbs)), s); + for (const auto& a : {verbs...}) + data.insert(data.end(), a.begin(), a.end()); + + auto digest = eosio::sha256(data.data(), data.size()); + auto signature = eosio::sign(key, digest); + auto sig_bin = eosio::convert_to_bin(signature); + data.insert(data.begin(), (uint8_t)eden::run_auth_type::signature_auth); + data.insert(data.begin() + 1, sig_bin.begin(), sig_bin.end()); + + eosio::action act; + act.account = "eden.gm"_n; + act.name = "run"_n; + act.authorization.push_back({"payer"_n, "active"_n}); + act.data = std::move(data); + + // printf("created run with data: %s\n", + // eosio::hex(act.data.begin(), act.data.end()).c_str()); + + expect(chain.push_transaction(chain.make_transaction({act})), expected); + } + void write_dfuse_history(const char* filename) { chain.start_block(); @@ -514,3 +581,16 @@ struct eden_tester dfuse_subchain::write_history(filename, chain); } }; + +// session action +template +std::vector sact(Ts&&... args) +{ + auto index = actions::get_index_for_session_action(ActionWrapper::action_name); + eosio::check(index.has_value(), "action index not found"); + auto data = eosio::convert_to_bin(eosio::varuint32(*index)); + auto act = ActionWrapper{""_n}.to_action(std::forward(args)...); + data.insert(data.end(), act.data.begin(), act.data.end()); + // eosio::print("sact: ", eosio::hex(data.begin(), data.end()), "\n"); + return data; +} diff --git a/contracts/eden/tests/run-full-election.cpp b/contracts/eden/tests/run-elections.cpp similarity index 57% rename from contracts/eden/tests/run-full-election.cpp rename to contracts/eden/tests/run-elections.cpp index d2ad76af5..cdddedd13 100644 --- a/contracts/eden/tests/run-full-election.cpp +++ b/contracts/eden/tests/run-elections.cpp @@ -2,7 +2,7 @@ TEST_CASE("Setup Eden chain with full election") { - nodeos_runner r("chain-full-election"); + nodeos_runner r("chain-run-elections"); r.tester.genesis(); r.tester.run_election(true, 10000, true); @@ -10,6 +10,10 @@ TEST_CASE("Setup Eden chain with full election") r.tester.induct_n(100); r.checkpoint("inductions"); r.tester.run_election(true, 10000, true); + r.checkpoint("full_election"); + r.tester.eden_gm.act( + time_point_sec{static_cast(time(nullptr))}); + r.tester.start_election(true, 10000); r.start_nodeos(); } diff --git a/contracts/eden/tests/test-eden.cpp b/contracts/eden/tests/test-eden.cpp index f74c7f4f7..84b77eef0 100644 --- a/contracts/eden/tests/test-eden.cpp +++ b/contracts/eden/tests/test-eden.cpp @@ -4,6 +4,31 @@ bool write_expected = false; +const eosio::private_key alice_session_priv_key = + private_key_from_string("5KdMjZ6vrbQWromznw5v7WLt4q92abv8sKgRKzagpj8SHacnozX"); +const eosio::public_key alice_session_pub_key = + public_key_from_string("EOS665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vyzqsQ"); + +const eosio::private_key alice_session_2_priv_key = + private_key_from_string("5KKnoRi3WfLL82sS4WdP8YXmezVR24Y8jxy5JXzwC2SouqoHgu2"); +const eosio::public_key alice_session_2_pub_key = + public_key_from_string("EOS8VWTR1mogYHEd9HJxgG2Tj3GbPghrnJqMfWfdHbTE11BJmyLRR"); + +const eosio::private_key pip_session_priv_key = + private_key_from_string("5KZLNGfDrqPM1yVL5zPXMhbAHBSi6ZtU2seqeUdEfudPgv9n93h"); +const eosio::public_key pip_session_pub_key = + public_key_from_string("EOS8YQhKe3x1xTA1KHmkBPznWqa3UGQsaHTUMkJJtcds9giK4Erft"); + +const eosio::private_key egeon_session_priv_key = + private_key_from_string("5Jk9RLHvhSgN8h7VjRGdY91GpeoXs5qP7JnizReg4DXBqtbGM8y"); +const eosio::public_key egeon_session_pub_key = + public_key_from_string("EOS8kBx4XYj3zZ3Z1Sb8vdq43ursVTebfcShKMDUymiA2ctcznX71"); + +const eosio::private_key bertie_session_priv_key = + private_key_from_string("5Jr4bSzJWhtr3bxY83xRDhUTgir9Mhn6YwVt4Y9SRgu1GopZ5vA"); +const eosio::public_key bertie_session_pub_key = + public_key_from_string("EOS5iALbhfqEZvqkUifUGbfMQSFnd1ui8ZsXVHT23XWh1HLyyPrJE"); + int main(int argc, char* argv[]) { Catch::Session session; @@ -718,7 +743,7 @@ TEST_CASE("election") t.electdonate_all(); { eden::current_election_state_singleton state("eden.gm"_n, eden::default_scope); - auto current = std::get(state.get()); + auto current = std::get(state.get()); CHECK(eosio::convert_to_json(current.start_time) == "\"2020-07-04T15:30:00.000\""); } t.skip_to("2020-07-03T15:29:59.500"); @@ -738,6 +763,18 @@ TEST_CASE("election") CHECK(result.board == std::vector{"alice"_n, "egeon"_n, "pip"_n}); } +TEST_CASE("election reschedule") +{ + eden_tester t; + t.genesis(); + t.electdonate_all(); + t.eden_gm.act(s2t("2020-03-02T15:30:01.000")); + t.skip_to(t.next_election_time().to_time_point() - eosio::days(1)); + t.electseed(t.next_election_time().to_time_point() - eosio::days(1)); + t.skip_to(t.next_election_time().to_time_point()); + expect(t.alice.trace(100), "No voters"); +} + TEST_CASE("mid-election induction") { eden_tester t; @@ -951,9 +988,9 @@ TEST_CASE("budget distribution minimum period") t.genesis(); t.set_balance(s2a("36.0000 EOS")); t.run_election(); - t.electdonate_all(); t.set_balance(s2a("100000.0000 EOS")); t.eden_gm.act(s2t("2020-09-02T15:30:01.000")); + t.electdonate_all(); t.run_election(); std::map expected{ {s2t("2020-07-04T15:30:00.000"), s2a("1.8000 EOS")}, @@ -969,7 +1006,6 @@ TEST_CASE("budget distribution exact") t.genesis(); t.set_balance(s2a("36.0000 EOS")); t.run_election(); - t.electdonate_all(); t.set_balance(s2a("1000.0000 EOS")); t.eden_gm.act(s2t("2020-09-02T15:30:00.000")); t.run_election(); @@ -986,7 +1022,6 @@ TEST_CASE("budget distribution underflow") t.genesis(); t.set_balance(s2a("36.0000 EOS")); t.run_election(); - t.electdonate_all(); t.set_balance(s2a("1000.0000 EOS")); t.eden_gm.act(s2t("2020-09-02T15:30:01.000")); t.run_election(); @@ -1210,9 +1245,9 @@ TEST_CASE("settablerows") t.eden_gm.act( eosio::name(eden::default_scope), std::vector{ - eden::current_election_state_registration{s2t("2020-01-02T00:00:00.0000")}}); + eden::current_election_state_registration_v1{s2t("2020-01-02T00:00:00.0000")}}); eden::current_election_state_singleton state{"eden.gm"_n, eden::default_scope}; - auto value = std::get(state.get()); + auto value = std::get(state.get()); CHECK(value.start_time.to_time_point() == s2t("2020-01-02T00:00:00.0000")); } @@ -1230,3 +1265,183 @@ TEST_CASE("election-events") t.write_dfuse_history("dfuse-test-election.json"); CompareFile{"test-election"}.write_events(t.chain).compare(); } + +TEST_CASE("contract-auth") +{ + eden_tester t; + t.genesis(); + + t.newsession("pip"_n, "alice"_n, alice_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), + "no, pip, no", "missing authority of alice"); + t.newsession("alice"_n, "alice"_n, alice_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point(), "my first session", + "session is expired"); + t.newsession("alice"_n, "alice"_n, alice_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(91), + "my first session", "expiration is too far in the future"); + t.newsession("alice"_n, "alice"_n, alice_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), + "four score and twenty", "description is too long"); + + t.newsession("alice"_n, "alice"_n, alice_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), + "four score and seven"); + t.newsession("alice"_n, "alice"_n, alice_session_2_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), + "another session"); + t.newsession("alice"_n, "alice"_n, alice_session_2_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(60), + "another session", "session key already exists"); + + t.delsession("pip"_n, "alice"_n, alice_session_pub_key, "missing authority of alice"); + t.delsession("alice"_n, "alice"_n, alice_session_pub_key); + t.chain.start_block(); + t.delsession("alice"_n, "alice"_n, alice_session_pub_key, + "Session key is either expired or not found"); + + t.run(alice_session_priv_key, "alice"_n, 1, + "Recovered session key PUB_K1_665ajq1JUMwWH3bHcRxxTqiZBZBc6CakwUfLkZJxRqp4vAtJaV " + "is either expired or not found", + sact("alice"_n, pip_session_pub_key)); + t.run(alice_session_2_priv_key, "alice"_n, 1, "Session key is either expired or not found", + sact("alice"_n, alice_session_pub_key)); + t.run(alice_session_2_priv_key, "alice"_n, 1, nullptr, + sact("alice"_n, alice_session_2_pub_key)); + t.chain.start_block(); + t.run(alice_session_2_priv_key, "alice"_n, 1, + "Recovered session key PUB_K1_8VWTR1mogYHEd9HJxgG2Tj3GbPghrnJqMfWfdHbTE11BLxqvo3 " + "is either expired or not found", + sact("alice"_n, alice_session_2_pub_key)); + + t.write_dfuse_history("dfuse-contract-auth.json"); + CompareFile{"contract-auth"}.write_events(t.chain).compare(); +} // TEST_CASE("contract-auth") + +TEST_CASE("contract-auth-induct") +{ + eden_tester t; + t.genesis(); + + t.newsession("alice"_n, "alice"_n, alice_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), ""); + t.newsession("pip"_n, "pip"_n, pip_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), ""); + t.newsession("egeon"_n, "egeon"_n, egeon_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), ""); + + t.newsession("bertie"_n, "bertie"_n, bertie_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), "", + "member bertie not found"); + + t.run(pip_session_priv_key, "pip"_n, 1, + "need authorization of alice but have authorization of pip", + sact(1234, "alice"_n, "bertie"_n, std::vector{"pip"_n, "egeon"_n})); + t.run(alice_session_priv_key, "alice"_n, 1, nullptr, + sact(1234, "alice"_n, "bertie"_n, std::vector{"pip"_n, "egeon"_n})); + t.newsession("bertie"_n, "bertie"_n, bertie_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), ""); + t.run(alice_session_priv_key, "alice"_n, 2, + "need authorization of bertie but have authorization of alice", + sact(1234, bertie_profile)); + t.run(bertie_session_priv_key, "bertie"_n, 1, "Video can only be set by inviter or a witness", + sact(1234, bertie_profile), + sact("bertie"_n, 1234, "vid"s)); + t.run(bertie_session_priv_key, "bertie"_n, 1, + "need authorization of pip but have authorization of bertie", + sact(1234, bertie_profile), + sact("pip"_n, 1234, "vid"s)); + t.run(bertie_session_priv_key, "bertie"_n, 1, + "Induction can only be endorsed by inviter or a witness", + sact(1234, bertie_profile), + sact("bertie"_n, 1234, t.hash_induction("vid"s, bertie_profile))); + t.run(bertie_session_priv_key, "bertie"_n, 1, + "need authorization of pip but have authorization of bertie", + sact(1234, bertie_profile), + sact("pip"_n, 1234, t.hash_induction("vid"s, bertie_profile))); + t.run(bertie_session_priv_key, "bertie"_n, 1, nullptr, + sact(1234, bertie_profile)); + + t.run(pip_session_priv_key, "pip"_n, 2, + "need authorization of alice but have authorization of pip", + sact("alice"_n, 1234, std::vector(4), + eosio::bytes{}, std::nullopt)); + t.run(pip_session_priv_key, "pip"_n, 2, nullptr, + sact("pip"_n, 1234, std::vector(0), + eosio::bytes{}, std::nullopt)); + + t.run(pip_session_priv_key, "pip"_n, 3, nullptr, + sact("pip"_n, 1234, "vid"s), + sact("pip"_n, 1234, t.hash_induction("vid"s, bertie_profile))); + t.run(alice_session_priv_key, "alice"_n, 3, nullptr, + sact("alice"_n, 1234, t.hash_induction("vid"s, bertie_profile))); + + t.run(pip_session_priv_key, "pip"_n, 4, + "need authorization of alice but have authorization of pip", + sact("alice"_n, 1234)); + t.run(pip_session_priv_key, "pip"_n, 4, nullptr, sact("pip"_n, 1234)); + + t.write_dfuse_history("dfuse-contract-auth-induct.json"); + CompareFile{"contract-auth-induct"}.write_events(t.chain).compare(); +} // TEST_CASE("contract-auth-induct") + +TEST_CASE("contract-auth-elect") +{ + eden_tester t; + t.genesis(); + t.induct_n(100); + + auto create_sessions = [&] { + t.alice.trace(1000); + t.newsession("alice"_n, "alice"_n, alice_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), ""); + t.newsession("pip"_n, "pip"_n, pip_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), ""); + t.newsession("egeon"_n, "egeon"_n, egeon_session_pub_key, + t.chain.get_head_block_info().timestamp.to_time_point() + eosio::days(90), ""); + }; + + create_sessions(); + t.run(pip_session_priv_key, "pip"_n, 1, + "need authorization of alice but have authorization of pip", + sact("alice"_n, true)); + t.run(alice_session_priv_key, "alice"_n, 1, nullptr, sact("alice"_n, true)); + t.chain.finish_block(); + t.run(alice_session_priv_key, "alice"_n, 2, "Not currently opted out", + sact("alice"_n, true)); + + t.electdonate_all(); + t.skip_to(t.next_election_time().to_time_point() - eosio::days(1)); + t.electseed(t.next_election_time().to_time_point() - eosio::days(1)); + t.skip_to(t.next_election_time().to_time_point() + eosio::minutes(10)); + t.setup_election(); + + t.run(pip_session_priv_key, "pip"_n, 2, + "Recovered session key PUB_K1_8YQhKe3x1xTA1KHmkBPznWqa3UGQsaHTUMkJJtcds9giKNsHGv " + "is either expired or not found", + sact("pip"_n, 0, std::vector(0), + eosio::bytes{}, std::nullopt)); + create_sessions(); + t.run(pip_session_priv_key, "pip"_n, 2, + "need authorization of alice but have authorization of pip", + sact("alice"_n, 0, std::vector(0), + eosio::bytes{}, std::nullopt)); + t.run(pip_session_priv_key, "pip"_n, 2, nullptr, + sact("pip"_n, 0, std::vector(0), + eosio::bytes{}, std::nullopt)); + + t.run(pip_session_priv_key, "pip"_n, 3, + "need authorization of alice but have authorization of pip", + sact(0, "alice"_n, "pip"_n)); + t.run(alice_session_priv_key, "alice"_n, 0, "alice and pip are not in the same group", + sact(0, "alice"_n, "pip"_n)); + + t.run(pip_session_priv_key, "pip"_n, 3, + "need authorization of alice but have authorization of pip", + sact(0, "alice"_n, "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws")); + t.run(alice_session_priv_key, "alice"_n, 1, nullptr, + sact(0, "alice"_n, "Qmb7WmZiSDXss5HfuKfoSf6jxTDrHzr8AoAUDeDMLNDuws")); + + t.write_dfuse_history("dfuse-contract-auth-elect.json"); + CompareFile{"contract-auth-elect"}.write_events(t.chain).compare(); +} // TEST_CASE("contract-auth-elect") diff --git a/libraries/abieos/include/eosio/crypto.hpp b/libraries/abieos/include/eosio/crypto.hpp index 30a44a764..d5d6be0aa 100644 --- a/libraries/abieos/include/eosio/crypto.hpp +++ b/libraries/abieos/include/eosio/crypto.hpp @@ -24,7 +24,7 @@ namespace eosio * @ingroup public_key */ - using ecc_public_key = std::array; + using ecc_public_key = std::array; /** * EOSIO WebAuthN public key @@ -77,7 +77,7 @@ namespace eosio */ using public_key = std::variant; - using ecc_private_key = std::array; + using ecc_private_key = std::array; using private_key = std::variant; /** @@ -87,7 +87,7 @@ namespace eosio * @ingroup signature */ - using ecc_signature = std::array; + using ecc_signature = std::array; struct webauthn_signature { @@ -156,7 +156,7 @@ namespace eosio obj = signature_from_string(stream.get_string()); } - std::string to_base58(const char* d, size_t s); - std::vector from_base58(const std::string_view& s); + std::string to_base58(const uint8_t* d, size_t s); + std::vector from_base58(const std::string_view& s); } // namespace eosio diff --git a/libraries/abieos/src/crypto.cpp b/libraries/abieos/src/crypto.cpp index f27988fea..7c41ec5ae 100644 --- a/libraries/abieos/src/crypto.cpp +++ b/libraries/abieos/src/crypto.cpp @@ -243,14 +243,14 @@ signature eosio::signature_from_string(std::string_view s) namespace eosio { - std::string to_base58(const char* d, size_t s) + std::string to_base58(const uint8_t* d, size_t s) { - return binary_to_base58(std::string_view(d, s)); + return binary_to_base58(std::string_view((const char*)d, s)); } - std::vector from_base58(const std::string_view& s) + std::vector from_base58(const std::string_view& s) { - std::vector ret; + std::vector ret; base58_to_binary(ret, s); return ret; } diff --git a/libraries/clchain/include/clchain/graphql.hpp b/libraries/clchain/include/clchain/graphql.hpp index cc92af90b..dd0509697 100644 --- a/libraries/clchain/include/clchain/graphql.hpp +++ b/libraries/clchain/include/clchain/graphql.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ namespace eosio inline constexpr bool use_json_string_for_gql(symbol_code*) { return true; } inline constexpr bool use_json_string_for_gql(symbol*) { return true; } inline constexpr bool use_json_string_for_gql(asset*) { return true; } + inline constexpr bool use_json_string_for_gql(public_key*) { return true; } template constexpr bool use_json_string_for_gql(fixed_bytes*) diff --git a/libraries/eosiolib/contracts/include/eosio/abi_generator.hpp b/libraries/eosiolib/contracts/include/eosio/abi_generator.hpp index 9accdf507..8faddd072 100644 --- a/libraries/eosiolib/contracts/include/eosio/abi_generator.hpp +++ b/libraries/eosiolib/contracts/include/eosio/abi_generator.hpp @@ -184,14 +184,16 @@ namespace eosio template void add_action_args(struct_def& def, std::tuple*, N name, Ns... names) { - def.fields.push_back({name, get_type()}); + if constexpr (!is_not_in_abi((remove_cvref_t*)nullptr)) + def.fields.push_back({name, get_type()}); add_action_args(def, (std::tuple*)nullptr, names...); } template void add_action_args(struct_def& def, std::tuple*) { - def.fields.push_back({"arg" + std::to_string(i), get_type()}); + if constexpr (!is_not_in_abi((remove_cvref_t*)nullptr)) + def.fields.push_back({"arg" + std::to_string(i), get_type()}); add_action_args(def, (std::tuple*)nullptr); } diff --git a/libraries/eosiolib/contracts/include/eosio/action.hpp b/libraries/eosiolib/contracts/include/eosio/action.hpp index 0ec0db883..5a355a84a 100644 --- a/libraries/eosiolib/contracts/include/eosio/action.hpp +++ b/libraries/eosiolib/contracts/include/eosio/action.hpp @@ -54,6 +54,28 @@ namespace eosio } }; // namespace internal_use_do_not_use + template + struct not_in_abi + { + T value; + }; + template + constexpr void eosio_for_each_field(not_in_abi*, F f) + { + } + + template + constexpr bool is_not_in_abi(T*) + { + return false; + } + + template + constexpr bool is_not_in_abi(not_in_abi*) + { + return true; + } + /** * @defgroup action Action * @ingroup contracts @@ -391,6 +413,16 @@ namespace eosio { return std::tuple::type>...>{}; } + template + auto get_args(R (Act::*p)(const not_in_abi&, Args...)) + { + return std::tuple::type>...>{}; + } + template + auto get_args(R (Act::*p)(const not_in_abi&, Args...) const) + { + return std::tuple::type>...>{}; + } template auto get_args_nounwrap(R (Act::*p)(Args...)) diff --git a/packages/box/.env b/packages/box/.env index 7a04dcd3e..cedf30fda 100644 --- a/packages/box/.env +++ b/packages/box/.env @@ -4,6 +4,7 @@ EOS_CHAIN_ID = "f16b1833c747c43682f4386fca9cbb327929334a762755ebec17f6f23c9b8a12 EOS_RPC_PROTOCOL = "https" EOS_RPC_HOST = "wax-test.eosdac.io" EOS_RPC_PORT = "443" +TAPOS_MANAGER_INTERVAL_MINS = "30" EDEN_CONTRACT_ACCOUNT = "test.edev" IPFS_PINATA_JWT = "" @@ -20,3 +21,13 @@ DFUSE_AUTH_NETWORK = "https://auth.eosnation.io" DFUSE_FIRST_BLOCK = "183705819" DFUSE_JSON_TRX_FILE = "dfuse-transactions.json" DFUSE_INTERVAL = 30 + +# serverpays config +SERVER_PAYS_PRIVATE_KEY = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +SERVER_PAYS_ACCOUNT = "payer" +SERVER_PAYS_PERMISSION = "active" +SERVER_PAYS_NOOP_CONTRACT = "payer" +SERVER_PAYS_NOOP_ACTION = "noop" +SERVER_PAYS_CREATE_ABI = "true" + +SESSIONS_ENABLE = "true" \ No newline at end of file diff --git a/packages/box/src/config.ts b/packages/box/src/config.ts index 0c5f0b121..82914f9da 100644 --- a/packages/box/src/config.ts +++ b/packages/box/src/config.ts @@ -22,6 +22,10 @@ export const rpcEndpoint = { protocol: process.env.EOS_RPC_PROTOCOL || "https", host: process.env.EOS_RPC_HOST || "wax-test.eosdac.io", port: Number(process.env.EOS_RPC_PORT || "443"), + chainId: process.env.EOS_CHAIN_ID || "", + taposManagerInterval: Number( + process.env.TAPOS_MANAGER_INTERVAL_MINS || "30" + ), }; console.info(rpcEndpoint); @@ -35,9 +39,9 @@ export const ipfsConfig = { "https://api.pinata.cloud/pinning/pinFileToIPFS", pinataJwt: process.env.IPFS_PINATA_JWT || "", }; -console.info({ - ...ipfsConfig, - pinataJwt: `${ipfsConfig.pinataJwt.substr(0,8)}..${ipfsConfig.pinataJwt.substr(-8)}` +console.info({ + ...ipfsConfig, + pinataJwt: ``, }); export const validUploadActions: ValidUploadActions = { @@ -58,6 +62,25 @@ logger.info( JSON.stringify(validUploadActions, undefined, 2) ); +export const serverPaysConfig = { + serverPaysPrivateKey: process.env.SERVER_PAYS_PRIVATE_KEY || "", + serverPaysAccount: process.env.SERVER_PAYS_ACCOUNT || "payer", + serverPaysPermission: process.env.SERVER_PAYS_PERMISSION || "active", + serverPaysNoopContract: process.env.SERVER_PAYS_NOOP_CONTRACT || "payer", + serverPaysNoopAction: process.env.SERVER_PAYS_NOOP_ACTION || "noop", + serverPaysCreateABI: process.env.SERVER_PAYS_CREATE_ABI === "true", +}; +logger.info( + "Sessions Config\n" + + JSON.stringify( + { ...serverPaysConfig, serverPaysPrivateKey: "" }, + undefined, + 2 + ) +); + +export const enableEdenSessions = process.env.SESSIONS_ENABLE === "true"; + export enum SubchainReceivers { UNKNOWN, DFUSE, diff --git a/packages/box/src/eos.ts b/packages/box/src/eos.ts index 2a62baa72..f1b8f256e 100644 --- a/packages/box/src/eos.ts +++ b/packages/box/src/eos.ts @@ -1,19 +1,35 @@ import * as eosjsJsonRpc from "eosjs/dist/eosjs-jsonrpc"; import * as eosjsApi from "eosjs/dist/eosjs-api"; +import { JsSignatureProvider } from "eosjs/dist/eosjs-jssig"; +import { AuthorityProviderArgs } from "eosjs/dist/eosjs-api-interfaces"; -import { rpcEndpoint } from "./config"; +import { rpcEndpoint, serverPaysConfig } from "./config"; +import { initializeServerPaysNoopAbi, TaposManager } from "./serverpay"; const rpcEndpointUrl = `${rpcEndpoint.protocol}://${rpcEndpoint.host}:${rpcEndpoint.port}`; export const eosJsonRpc = new eosjsJsonRpc.JsonRpc(rpcEndpointUrl, { fetch: require("node-fetch"), }); +const signatureProvider = new JsSignatureProvider([ + serverPaysConfig.serverPaysPrivateKey, +]); + +const authorityProvider = { + // Optimization: don't need /v1/chain/get_required_keys + async getRequiredKeys(args: AuthorityProviderArgs) { + return signatureProvider.getAvailableKeys(); + }, +}; + export const eosDefaultApi = new eosjsApi.Api({ rpc: eosJsonRpc, - signatureProvider: { - getAvailableKeys: async () => [], - sign: async (args: any) => { - throw new Error("implement"); - }, - }, + signatureProvider, + authorityProvider, + chainId: rpcEndpoint.chainId, }); + +initializeServerPaysNoopAbi(eosDefaultApi); + +export const taposManager = new TaposManager(eosJsonRpc); +taposManager.init(); diff --git a/packages/box/src/handlers/index.ts b/packages/box/src/handlers/index.ts index 21906e788..18cf13326 100644 --- a/packages/box/src/handlers/index.ts +++ b/packages/box/src/handlers/index.ts @@ -1,3 +1,4 @@ export * from "./info"; export * from "./ipfs-upload"; export * from "./subchain"; +export * from "./sessions"; diff --git a/packages/box/src/handlers/sessions.ts b/packages/box/src/handlers/sessions.ts new file mode 100644 index 000000000..37d90d67f --- /dev/null +++ b/packages/box/src/handlers/sessions.ts @@ -0,0 +1,98 @@ +import express, { Request, Response } from "express"; +import { + handleErrors, + BadRequestError, + sessionSignRequest, + SessionSignRequest, +} from "@edenos/common"; +import { arrayToHex } from "eosjs/dist/eosjs-serialize"; + +import logger from "../logger"; +import { eosDefaultApi, taposManager } from "../eos"; +import { edenContractAccount, serverPaysConfig } from "../config"; + +export const sessionHandler = express.Router(); + +sessionHandler.post("/sign", async (req: Request, res: Response) => { + try { + const { body } = req; + if (!body) { + throw new BadRequestError(["missing session sign data"]); + } + + const parsedRequest = sessionSignRequest.safeParse(body); + if (parsedRequest.success !== true) { + throw new BadRequestError(parsedRequest.error.flatten()); + } + + const requestData: SessionSignRequest = parsedRequest.data; + return await signSessionRequest(requestData, res); + } catch (error) { + logger.error(`sessionHandler: ${error.message}`); + return handleErrors(res, error); + } +}); + +const signSessionRequest = async ( + sessionSignRequest: SessionSignRequest, + res: Response +) => { + const trx = prepareExecSessionTrx(sessionSignRequest); + const signedTrxResult = await signTrx(trx); + logger.info( + `signed [execsession] for account:${sessionSignRequest.edenAccount} + sequence:${sessionSignRequest.sequence}` + ); + return res.json(signedTrxResult); +}; + +const prepareExecSessionTrx = (sessionSignRequest: SessionSignRequest) => { + const tapos = taposManager.getTapos(); + const expiration = new Date(Date.now() + 2 * 60 * 1000) + .toISOString() + .slice(0, -1); + + const signatureAuth = { + signature: sessionSignRequest.signature, + contract: edenContractAccount, + account: sessionSignRequest.edenAccount, + sequence: sessionSignRequest.sequence, + }; + + return { + ...tapos, + expiration, + actions: [ + { + account: serverPaysConfig.serverPaysNoopContract, + name: serverPaysConfig.serverPaysNoopAction, + authorization: [ + { + actor: serverPaysConfig.serverPaysAccount, + permission: serverPaysConfig.serverPaysPermission, + }, + ], + data: {}, + }, + { + account: edenContractAccount, + name: "run", + authorization: [] as any[], + data: { + auth: ["signature_auth", signatureAuth], + verbs: sessionSignRequest.verbs, + }, + }, + ], + }; +}; + +const signTrx = async (trx: any) => { + const signedTrx = await eosDefaultApi.transact(trx, { + broadcast: false, + }); + return { + signatures: (signedTrx as any).signatures, + packed_trx: arrayToHex((signedTrx as any).serializedTransaction), + }; +}; diff --git a/packages/box/src/index.ts b/packages/box/src/index.ts index 992df2530..e3d3e7577 100644 --- a/packages/box/src/index.ts +++ b/packages/box/src/index.ts @@ -32,3 +32,4 @@ if (subchainConfig.enable) { createWSServer("/v1/subchain", server); startSubchain(); } + diff --git a/packages/box/src/routes.ts b/packages/box/src/routes.ts index d9ad14c54..4f8d5ed66 100644 --- a/packages/box/src/routes.ts +++ b/packages/box/src/routes.ts @@ -1,17 +1,19 @@ import { Router } from "express"; -import { subchainConfig } from "./config"; +import { enableEdenSessions, subchainConfig } from "./config"; import { infoHandler, ipfsUploadConfigHandler, ipfsUploadHandler, subchainHandler, + sessionHandler, } from "./handlers"; const router: Router = Router(); router.get("/", infoHandler); router.post("/v1/ipfs-upload", ipfsUploadConfigHandler, ipfsUploadHandler); +if (enableEdenSessions) router.use("/v1/sessions", sessionHandler); if (subchainConfig.enable) router.use("/v1/subchain", subchainHandler); export default router; diff --git a/packages/box/src/serverpay/abi-initializer.ts b/packages/box/src/serverpay/abi-initializer.ts new file mode 100644 index 000000000..99de8656b --- /dev/null +++ b/packages/box/src/serverpay/abi-initializer.ts @@ -0,0 +1,38 @@ +import { Api } from "eosjs/dist/eosjs-api"; + +import { serverPaysConfig } from "../config"; + +export const initializeServerPaysNoopAbi = (eosApi: Api) => { + if (!serverPaysConfig.serverPaysCreateABI) { + return; + } + + const noopAbi = { + version: "eosio::abi/1.1", + types: [] as any[], + structs: [ + { + name: serverPaysConfig.serverPaysNoopAction, + base: "", + fields: [] as any[], + }, + ], + actions: [ + { + name: serverPaysConfig.serverPaysNoopAction, + type: serverPaysConfig.serverPaysNoopAction, + ricardian_contract: "", + }, + ], + tables: [] as any[], + ricardian_clauses: [] as any[], + error_messages: [] as any[], + abi_extensions: [] as any[], + variants: [] as any[], + }; + + eosApi.cachedAbis.set(serverPaysConfig.serverPaysNoopContract, { + rawAbi: eosApi.jsonToRawAbi(noopAbi), + abi: noopAbi, + }); +}; diff --git a/packages/box/src/serverpay/index.ts b/packages/box/src/serverpay/index.ts new file mode 100644 index 000000000..3d3145950 --- /dev/null +++ b/packages/box/src/serverpay/index.ts @@ -0,0 +1,2 @@ +export * from "./tapos-manager"; +export * from "./abi-initializer"; diff --git a/packages/box/src/serverpay/tapos-manager.ts b/packages/box/src/serverpay/tapos-manager.ts new file mode 100644 index 000000000..fa3590d55 --- /dev/null +++ b/packages/box/src/serverpay/tapos-manager.ts @@ -0,0 +1,56 @@ +import { reverseHex } from "../utility"; +import logger from "../logger"; +import { rpcEndpoint } from "../config"; + +interface Tapos { + ref_block_num: number; + ref_block_prefix: number; +} + +/** + * Tapos Optimization: cache tapos so signature requests don't hit RPC + */ +export class TaposManager { + private rpc: any; + private tapos: Tapos | undefined; + + constructor(rpc: any) { + this.rpc = rpc; + } + + init() { + this.generateTapos(); + } + + getTapos(): Tapos { + if (!this.tapos) { + throw new Error( + "Tapos is not generated yet, please try again later" + ); + } + return this.tapos; + } + + async generateTapos() { + try { + const info = await this.rpc.get_info(); + const prefix = parseInt( + reverseHex(info.last_irreversible_block_id.substr(16, 8)), + 16 + ); + this.tapos = { + ref_block_num: info.last_irreversible_block_num & 0xffff, + ref_block_prefix: prefix, + }; + logger.info(`tapos: ${JSON.stringify(this.tapos)}`); + setTimeout( + () => this.generateTapos(), + rpcEndpoint.taposManagerInterval * 60 * 1000 + ); + } catch (e) { + logger.error(`generateTapos: ${e.message}`); + logger.info("retry in 10s"); + setTimeout(() => this.generateTapos(), 10_000); + } + } +} diff --git a/packages/box/src/utility.ts b/packages/box/src/utility.ts new file mode 100644 index 000000000..d2d7dadb9 --- /dev/null +++ b/packages/box/src/utility.ts @@ -0,0 +1,8 @@ +export const reverseHex = (hexStr: string) => { + return ( + hexStr.substr(6, 2) + + hexStr.substr(4, 2) + + hexStr.substr(2, 2) + + hexStr.substr(0, 2) + ); +}; diff --git a/packages/common/src/api/box-v1.ts b/packages/common/src/api/box-v1.ts index 2df144423..31aa20b18 100644 --- a/packages/common/src/api/box-v1.ts +++ b/packages/common/src/api/box-v1.ts @@ -9,3 +9,12 @@ export const ipfsUploadRequest = z.object({ }); export type IpfsUploadRequest = z.infer; + +export const sessionSignRequest = z.object({ + signature: z.string(), + edenAccount: z.string(), + sequence: z.number(), + verbs: z.array(z.any()), +}); + +export type SessionSignRequest = z.infer; diff --git a/packages/webapp/package.json b/packages/webapp/package.json index 88ae77359..16b94fa19 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -26,6 +26,7 @@ "graphiql": "^1.4.2", "graphql": "^15.5.1", "hash.js": "^1.1.7", + "idb-keyval": "^6.0.3", "ipfs-http-client": "^50.0.0", "ipfs-only-hash": "^4.0.0", "next": "~10", diff --git a/packages/webapp/src/_app/eos/sessions.ts b/packages/webapp/src/_app/eos/sessions.ts new file mode 100644 index 000000000..fad39d4b7 --- /dev/null +++ b/packages/webapp/src/_app/eos/sessions.ts @@ -0,0 +1,200 @@ +import dayjs from "dayjs"; +import { generateKeyPair, sha256 } from "eosjs/dist/eosjs-key-conversions"; +import { KeyType } from "eosjs/dist/eosjs-numeric"; +import { PrivateKey } from "eosjs/dist/eosjs-jssig"; +import { hexToUint8Array, SerialBuffer } from "eosjs/dist/eosjs-serialize"; +import { get as idbGet, set as idbSet } from "idb-keyval"; +import { SessionSignRequest } from "@edenos/common"; + +import { edenContractAccount, box } from "config"; +import { eosDefaultApi, eosJsonRpc } from "_app"; + +const DEFAULT_EXPIRATION_SECONDS = 30 * 24 * 60 * 60; // 30 days +const DEFAULT_SESSION_DESCRIPTION = "eden login"; + +interface SessionKeyData { + publicKey: string; + privateKey: string; + expiration: Date; + lastSequence: number; +} + +class SessionKeysStorage { + constructor(public storageKey = "sessionKeys") {} + + async getKey() { + const data: SessionKeyData | undefined = await idbGet(this.storageKey); + return data; + } + + async saveKey(keyData: SessionKeyData) { + return idbSet(this.storageKey, keyData); + } + + async advanceSequence() { + const data = await this.getKey(); + if (!data) { + throw new Error( + "Unable to advance sequence on missing session key" + ); + } + + data.lastSequence += 1; + await this.saveKey(data); + + return data.lastSequence; + } +} +export const sessionKeysStorage = new SessionKeysStorage(); + +export const generateSessionKey = async ( + expirationSeconds = DEFAULT_EXPIRATION_SECONDS +) => { + // TODO: to be replaced with SubtleCrypto Apis + const { publicKey, privateKey } = generateKeyPair(KeyType.r1, { + secureEnv: true, + }); + + const expiration = dayjs().add(expirationSeconds, "seconds").toDate(); + + return { + publicKey: publicKey.toString(), + privateKey: privateKey.toString(), + lastSequence: 0, + expiration, + }; +}; + +export const newSessionTransaction = async ( + authorizerAccount: string, + sessionKeyData: SessionKeyData, + description = DEFAULT_SESSION_DESCRIPTION +) => { + const key = sessionKeyData.publicKey; + const expiration = + sessionKeyData.expiration.toISOString().slice(0, -4) + "000"; + + return { + actions: [ + { + account: edenContractAccount, + name: "newsession", + authorization: [ + { + actor: authorizerAccount, + permission: "active", + }, + ], + data: { + eden_account: authorizerAccount, + key, + expiration, + description, + }, + }, + ], + }; +}; + +export const signAndBroadcastSessionTransaction = async ( + authorizerAccount: string, + actions: any[] +) => { + const signedSessionTrx = await signSessionTransaction( + authorizerAccount, + actions + ); + console.info("generated signedSessionTrx trx", signedSessionTrx); + + const broadcastedRunTrx = await eosJsonRpc.send_transaction({ + signatures: signedSessionTrx.signatures, + serializedTransaction: hexToUint8Array(signedSessionTrx.packed_trx), + }); + console.info("broadcasted run trx >>>", broadcastedRunTrx); + + await sessionKeysStorage.advanceSequence(); + + return broadcastedRunTrx; +}; + +export const signSessionTransaction = async ( + authorizerAccount: string, + actions: any[] +) => { + const sessionKey = await sessionKeysStorage.getKey(); + if (!sessionKey) { + throw new Error("Session key is not present"); + } + + const sequence = sessionKey.lastSequence + 1; + const verbs = convertActionsToVerbs(actions); + + const signatureAuthSha = await makeSignatureAuthSha( + edenContractAccount, + authorizerAccount, + sequence, + verbs + ); + + const signature = await signSha(sessionKey, signatureAuthSha); + + const data: SessionSignRequest = { + signature: signature.toString(), + edenAccount: authorizerAccount, + sequence, + verbs, + }; + const response = await fetch(`${box.address}/v1/sessions/sign`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + return response.json(); +}; + +const makeSignatureAuthSha = async ( + contract: string, + account: string, + sequence: number, + verbs: any[] +) => { + const signatureAuthBytes = await serializeSignatureAuth( + contract, + account, + sequence, + verbs + ); + return sha256(Buffer.from(signatureAuthBytes)); +}; + +const convertActionsToVerbs = (actions: any[]) => + actions.map((action) => [action.name, action.data]); + +const serializeSignatureAuth = async ( + contract: string, + account: string, + sequence: number, + verbs: any[] +): Promise => { + const contractAbi = await eosDefaultApi.getContract(contract); + const verbType = contractAbi.types.get("verb"); + if (!verbType) { + throw new Error("abi has no verb definition"); + } + const buffer = new SerialBuffer({ + textEncoder: eosDefaultApi.textEncoder, + textDecoder: eosDefaultApi.textDecoder, + }); + buffer.pushName(contract); + buffer.pushName(account); + buffer.pushVaruint32(sequence); + buffer.pushVaruint32(verbs.length); + verbs.forEach((verb) => verbType.serialize(buffer, verb)); + + return buffer.asUint8Array(); +}; + +const signSha = (sessionKey: SessionKeyData, sha: number[]) => { + const privateKey = PrivateKey.fromString(sessionKey.privateKey); + return privateKey.sign(sha, false); +}; diff --git a/packages/webapp/src/_app/eos/ual/softkey/softkey-user.ts b/packages/webapp/src/_app/eos/ual/softkey/softkey-user.ts index 153e430a9..7942d3621 100644 --- a/packages/webapp/src/_app/eos/ual/softkey/softkey-user.ts +++ b/packages/webapp/src/_app/eos/ual/softkey/softkey-user.ts @@ -99,7 +99,7 @@ export class SoftkeyUser extends User { "updated the encryption key to the same as the password successfully!" ); } catch (e) { - console.error("fail to set encryption key", e); + console.error("fail to auto-set encryption key (no harm done)", e); } } diff --git a/packages/webapp/src/_app/hooks/box-queries.ts b/packages/webapp/src/_app/hooks/box-queries.ts index 0845762f5..f8a0e2f4a 100644 --- a/packages/webapp/src/_app/hooks/box-queries.ts +++ b/packages/webapp/src/_app/hooks/box-queries.ts @@ -6,10 +6,10 @@ import dayjs from "dayjs"; import { assetFromString } from "_app"; import { - formatQueriedMemberData, - MemberData, + formatMembersQueryNodeAsMemberNFT, MEMBER_DATA_FRAGMENT, } from "members"; +import { MemberNFT } from "nfts/interfaces"; export interface ElectionStatusQuery { status: { @@ -41,8 +41,8 @@ export interface RoundBasicQueryData { } export interface RoundForUserVotingQueryData extends RoundBasicQueryData { - candidate?: MemberData; - winner?: MemberData; + candidate?: MemberNFT; + winner?: MemberNFT; video: string; } @@ -116,8 +116,12 @@ export const useCurrentMemberElectionVotingData = ( votingFinished: voteNode.group.round.votingFinished, resultsAvailable: voteNode.group.round.resultsAvailable, numGroups: voteNode.group.round.numGroups, - candidate: formatQueriedMemberData(voteNode.candidate), - winner: formatQueriedMemberData(voteNode.group.winner), + candidate: formatMembersQueryNodeAsMemberNFT( + voteNode.candidate + ), + winner: formatMembersQueryNodeAsMemberNFT( + voteNode.group.winner + ), video: voteNode.video, })) || []; } @@ -127,13 +131,13 @@ export const useCurrentMemberElectionVotingData = ( }; export interface VoteQueryData { - voter: MemberData; - candidate?: MemberData; + voter: MemberNFT; + candidate?: MemberNFT; video: string; } export interface RoundGroupQueryData { - winner?: MemberData; + winner?: MemberNFT; votes: VoteQueryData[]; } @@ -217,15 +221,15 @@ const mapQueriedRounds = (queriedRoundsEdges: any) => const mapQueriedRoundsGroups = (queriedRoundsGroupsEdges: any) => queriedRoundsGroupsEdges?.map(({ node: groupNode }: any) => ({ - winner: formatQueriedMemberData(groupNode.winner), + winner: formatMembersQueryNodeAsMemberNFT(groupNode.winner), votes: mapQueriedGroupVotes(groupNode.votes), })) || []; const mapQueriedGroupVotes = (votes: any) => { return ( votes?.map((vote: any) => ({ - voter: formatQueriedMemberData(vote.voter), - candidate: formatQueriedMemberData(vote.candidate), + voter: formatMembersQueryNodeAsMemberNFT(vote.voter), + candidate: formatMembersQueryNodeAsMemberNFT(vote.candidate), video: vote.video, })) || [] ); diff --git a/packages/webapp/src/_app/hooks/queries.ts b/packages/webapp/src/_app/hooks/queries.ts index 0a2747345..c191bf3b0 100644 --- a/packages/webapp/src/_app/hooks/queries.ts +++ b/packages/webapp/src/_app/hooks/queries.ts @@ -6,8 +6,8 @@ import { getMembers, getTreasuryStats, getMembersStats, - MemberData, MemberStats, + useMembersByAccountNamesAsMemberNFTs, } from "members"; import { getCommunityGlobals, getTokenBalanceForAccount } from "_app/api"; import { @@ -40,6 +40,7 @@ import { ElectionCompletedRound, VoteData, } from "elections/interfaces"; +import { MemberNFT } from "nfts/interfaces"; import { EncryptionScope, getEncryptedData } from "encryption/api"; import { TableQueryOptions } from "_app/eos/interfaces"; @@ -149,7 +150,7 @@ export const queryCommunityGlobals = { }; export const queryOngoingElectionData = ( - votingMemberData?: MemberData[], + votingMemberData?: MemberNFT[], currentElection?: CurrentElection, myDelegation?: EdenMember[], currentMember?: EdenMember @@ -409,25 +410,6 @@ export const useVoteData = ( ...queryOptions, }); -export const useMemberDataFromEdenMembers = ( - members?: EdenMember[], - queryOptions: any = {} -) => { - const nftTemplateIds = members?.map((em) => em.nft_template_id); - - let enabled = Boolean(nftTemplateIds?.length); - if ("enabled" in queryOptions) { - enabled = enabled && queryOptions.enabled; - } - - return useQuery({ - ...queryMembers(1, nftTemplateIds?.length, nftTemplateIds), - staleTime: Infinity, - ...queryOptions, - enabled, - }); -}; - export const useMemberDataFromVoteData = (voteData?: VoteData[]) => { const responses = useMemberListByAccountNames( voteData?.map((participant) => participant.member) ?? [] @@ -436,20 +418,19 @@ export const useMemberDataFromVoteData = (voteData?: VoteData[]) => { const areQueriesComplete = responses.every((res) => res.isSuccess); const isLoading = responses.some((res) => res.isLoading); - const edenMembers = responses - .filter((res) => Boolean(res?.data?.nft_template_id)) - .map((res) => res.data as EdenMember); + const accountNames = responses + .filter((res) => Boolean(res?.data?.account)) + .map((res) => res.data as EdenMember) + .map((member) => member.account); - const memberDataRes = useMemberDataFromEdenMembers(edenMembers, { - enabled: !isFetchError && areQueriesComplete, - }); + const memberDataRes = useMembersByAccountNamesAsMemberNFTs(accountNames); return { ...memberDataRes, isLoading: memberDataRes.isLoading || isLoading, isError: memberDataRes.isError || isFetchError, - isSuccess: memberDataRes.isSuccess || areQueriesComplete, - } as UseQueryResult; + isSuccess: areQueriesComplete, + } as UseQueryResult; }; export const useEncryptedData = (scope: EncryptionScope, id: string) => diff --git a/packages/webapp/src/_app/ui/generic-member-chip.tsx b/packages/webapp/src/_app/ui/generic-member-chip.tsx index aa43c3a4d..9a3086fd9 100644 --- a/packages/webapp/src/_app/ui/generic-member-chip.tsx +++ b/packages/webapp/src/_app/ui/generic-member-chip.tsx @@ -1,8 +1,7 @@ import { DelegateBadge, ProfileImage } from "_app/ui"; -import { MemberData } from "members/interfaces"; interface Props { - member: MemberData; + imageUrl: string; isDelegate?: boolean; actionComponent?: React.ReactNode; contentComponent: React.ReactNode; @@ -12,7 +11,7 @@ interface Props { } export const GenericMemberChip = ({ - member, + imageUrl, isDelegate, // TODO: depend on info in member for this onClickChip, onClickProfileImage, @@ -33,7 +32,7 @@ export const GenericMemberChip = ({ >
} onClick={onClickProfileImage || onClickChip} /> diff --git a/packages/webapp/src/_app/ui/nav-profile.tsx b/packages/webapp/src/_app/ui/nav-profile.tsx index 07ee4b1b1..558ecd612 100644 --- a/packages/webapp/src/_app/ui/nav-profile.tsx +++ b/packages/webapp/src/_app/ui/nav-profile.tsx @@ -4,14 +4,10 @@ import { Popover } from "@headlessui/react"; import { usePopper } from "react-popper"; import { IoMdLogIn } from "react-icons/io"; -import { MemberStatus } from "_app"; -import { - useCurrentMember, - useMemberDataFromEdenMembers, - useSignOut, -} from "_app/hooks"; -import { Button, ProfileImage, Text } from "_app/ui"; import { ROUTES } from "_app/routes"; +import { useSignOut } from "_app/hooks"; +import { Button, ProfileImage, Text } from "_app/ui"; +import { Member, useCurrentMember } from "members"; import { useUALAccount } from "../eos"; @@ -20,25 +16,10 @@ interface Props { } export const NavProfile = ({ location }: Props) => { - const [ualAccount, _, ualShowModal] = useUALAccount(); - const accountName = ualAccount?.accountName; - - const { - data: member, - isLoading: isLoadingCurrentMember, - isError: isErrorCurrentMember, - } = useCurrentMember(); - const { - data: memberData, - isLoading: isLoadingMemberData, - isError: isErrorMemberData, - } = useMemberDataFromEdenMembers(member ? [member] : []); - - const isActiveMember = member?.status === MemberStatus.ActiveMember; + const [_ualAccount, _, ualShowModal] = useUALAccount(); + const { data: member } = useCurrentMember(); - const userProfile = memberData?.[0]; - - if (!ualAccount) { + if (!member?.accountName) { return (
-); - -interface HeaderProps { - stage: RoundStage; - roundIndex: number; - roundStartTime: Dayjs; - roundEndTime: Dayjs; - currentStageEndTime: Dayjs; - meetingStartTime: Dayjs; - postMeetingStartTime: Dayjs; -} - -const Header = ({ - stage, - roundIndex, - roundStartTime, - roundEndTime, - currentStageEndTime, - meetingStartTime, - postMeetingStartTime, -}: HeaderProps) => { - return ( - - } - sublineComponent={ - - {roundStartTime.format("LT")} -{" "} - {roundEndTime.format("LT z")} - - } - > - {stage === RoundStage.Meeting && ( - - )} - - ); -}; - -interface HeadlineProps { - roundIndex: number; - stage: RoundStage; - currentStageEndTime: Dayjs; -} - -const RoundHeaderHeadline = ({ - roundIndex, - stage, - currentStageEndTime, -}: HeadlineProps) => { - const { hmmss } = useCountdown({ endTime: currentStageEndTime.toDate() }); - const roundNum = roundIndex + 1; - switch (stage) { - case RoundStage.PreMeeting: - return ( - - Round {roundNum} starts in:{" "} - {hmmss} - - ); - case RoundStage.PostMeeting: - return ( - - Round {roundNum} finalizes in:{" "} - {hmmss} - - ); - case RoundStage.Complete: - return ( - - Round {roundIndex + 1} finalizing... - - ); - } - return ( - - Round {roundIndex + 1} in progress - - ); -}; diff --git a/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header-components/index.ts b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header-components/index.ts new file mode 100644 index 000000000..3fbfacafc --- /dev/null +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header-components/index.ts @@ -0,0 +1 @@ +export * from "./pie-mer"; diff --git a/packages/webapp/src/elections/components/pie-mer.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header-components/pie-mer.tsx similarity index 100% rename from packages/webapp/src/elections/components/pie-mer.tsx rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header-components/pie-mer.tsx diff --git a/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header.tsx new file mode 100644 index 000000000..33241d930 --- /dev/null +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/header.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import dayjs from "dayjs"; + +import { useCountdown } from "_app"; +import { Text } from "_app/ui"; +import { RoundStage } from "elections/interfaces"; + +import RoundHeader from "../round-header"; +import { VotePieMer } from "./header-components"; + +interface HeaderProps { + stage: RoundStage; + roundIndex: number; + roundStartTime: dayjs.Dayjs; + roundEndTime: dayjs.Dayjs; + currentStageEndTime: dayjs.Dayjs; + meetingStartTime: dayjs.Dayjs; + postMeetingStartTime: dayjs.Dayjs; +} + +export const Header = ({ + stage, + roundIndex, + roundStartTime, + roundEndTime, + currentStageEndTime, + meetingStartTime, + postMeetingStartTime, +}: HeaderProps) => { + return ( + + } + sublineComponent={ + + {roundStartTime.format("LT")} -{" "} + {roundEndTime.format("LT z")} + + } + > + {stage === RoundStage.Meeting && ( + + )} + + ); +}; + +export default Header; + +interface HeadlineProps { + roundIndex: number; + stage: RoundStage; + currentStageEndTime: dayjs.Dayjs; +} + +const RoundHeaderHeadline = ({ + roundIndex, + stage, + currentStageEndTime, +}: HeadlineProps) => { + const { hmmss } = useCountdown({ endTime: currentStageEndTime.toDate() }); + const roundNum = roundIndex + 1; + switch (stage) { + case RoundStage.PreMeeting: + return ( + + Round {roundNum} starts in:{" "} + {hmmss} + + ); + case RoundStage.PostMeeting: + return ( + + Round {roundNum} finalizes in:{" "} + {hmmss} + + ); + case RoundStage.Complete: + return ( + + Round {roundIndex + 1} finalizing... + + ); + } + return ( + + Round {roundIndex + 1} in progress + + ); +}; diff --git a/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/index.ts b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/index.ts new file mode 100644 index 000000000..8ee29f62c --- /dev/null +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/index.ts @@ -0,0 +1,4 @@ +export * from "./header"; +export * from "./round-info-panel"; +export * from "./participants-voting-panel"; +export * from "./participants-waiting-panel"; diff --git a/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/participants-voting-panel.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/participants-voting-panel.tsx new file mode 100644 index 000000000..1c15b5b7b --- /dev/null +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/participants-voting-panel.tsx @@ -0,0 +1,137 @@ +import React, { useState } from "react"; +import { useQueryClient } from "react-query"; +import { BiCheck } from "react-icons/bi"; + +import { onError, useUALAccount } from "_app"; +import { + queryMemberGroupParticipants, + useCurrentMember, +} from "_app/hooks/queries"; +import { Button, Container } from "_app/ui"; +import { ActiveStateConfigType, VoteData } from "elections/interfaces"; +import { MemberNFT } from "nfts/interfaces"; + +import { setVote } from "../../../transactions"; +import { VideoUploadButton } from "../video-upload-button"; +import VotingRoundParticipants from "./voting-round-participants"; + +interface ParticipantsVotingPanelProps { + members?: MemberNFT[]; + voteData: VoteData[]; + roundIndex: number; + electionConfig?: ActiveStateConfigType; +} + +export const ParticipantsVotingPanel = ({ + members, + voteData, + roundIndex, + electionConfig, +}: ParticipantsVotingPanelProps) => { + const [selectedMember, setSelected] = useState(null); + const [isSubmittingVote, setIsSubmittingVote] = useState(false); + + const queryClient = useQueryClient(); + + const [ualAccount] = useUALAccount(); + const { data: loggedInMember } = useCurrentMember(); + + const userVoterStats = voteData!.find( + (vs) => vs.member === loggedInMember?.account + ); + + const userVotingFor = members?.find( + (m) => m.account === userVoterStats?.candidate + ); + + const onSubmitVote = async () => { + if (!selectedMember) return; + setIsSubmittingVote(true); + try { + const authorizerAccount = ualAccount.accountName; + const transaction = setVote( + authorizerAccount, + roundIndex, + selectedMember?.account + ); + await ualAccount.signTransaction(transaction, { + broadcast: true, + }); + + // invalidate current member query to update participating status + await new Promise((resolve) => setTimeout(resolve, 3000)); + queryClient.invalidateQueries( + queryMemberGroupParticipants( + loggedInMember?.account, + roundIndex, + electionConfig + ).queryKey + ); + } catch (error) { + console.error(error); + onError(error as Error); + } + setIsSubmittingVote(false); + }; + + return ( + <> + setSelected(m)} + userVotingFor={userVotingFor?.account} + /> + +
+
+ +
+ +
+ +
+
+
+ + ); +}; + +export default ParticipantsVotingPanel; + +interface VoteButtonProps { + selectedMember: MemberNFT | null; + isSubmittingVote: boolean; + userVotingFor?: MemberNFT; + onSubmitVote: () => Promise; +} + +const VoteButton = ({ + selectedMember, + isSubmittingVote, + userVotingFor, + onSubmitVote, +}: VoteButtonProps) => ( + +); diff --git a/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/participants-waiting-panel.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/participants-waiting-panel.tsx new file mode 100644 index 000000000..bd984ab29 --- /dev/null +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/participants-waiting-panel.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +import { ElectionParticipantChip } from "elections"; +import { MemberNFT } from "nfts/interfaces"; + +interface ParticipantsWaitingPanelProps { + members?: MemberNFT[]; + roundIndex: number; +} + +export const ParticipantsWaitingPanel = ({ + members, + roundIndex, +}: ParticipantsWaitingPanelProps) => ( +
+ {members?.map((member) => ( + + ))} +
+); + +export default ParticipantsWaitingPanel; diff --git a/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info-panel.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info-panel.tsx new file mode 100644 index 000000000..bc1635a41 --- /dev/null +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info-panel.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import dayjs from "dayjs"; + +import { Container, Heading, Text } from "_app/ui"; +import { + ActiveStateConfigType, + RoundStage, + VoteData, +} from "elections/interfaces"; + +import { Consensometer } from "./round-info"; +import { MeetingLink } from "./round-info/meeting-link"; +import { VideoUploadButton } from "../video-upload-button"; + +interface RoundInfoPanelProps { + stage: RoundStage; + roundIndex: number; + meetingStartTime: dayjs.Dayjs; + electionConfig?: ActiveStateConfigType; + voteData?: VoteData[]; + isVotingOpen: boolean; +} + +export const RoundInfoPanel = ({ + stage, + roundIndex, + meetingStartTime, + electionConfig, + voteData, + isVotingOpen, +}: RoundInfoPanelProps) => { + return ( + +
+ {[RoundStage.PreMeeting, RoundStage.Meeting].includes( + stage + ) && ( +
+ +
+ )} + {[RoundStage.Meeting, RoundStage.PostMeeting].includes( + stage + ) && ( +
+ +
+ )} +
+
+ Meeting group members + + {stage === RoundStage.PreMeeting + ? "Make sure you have your meeting link ready and stand by. You'll be on a video call with the following Eden members momentarily." + : stage === RoundStage.Meeting + ? "Meet with your group. Align on a leader >2/3 majority. Select your leader and submit your vote below." + : stage === RoundStage.Complete + ? "If you're the delegate elect, stand by. The next round will start momentarily." + : "This round is finalizing. Please submit any outstanding votes now. You will be able to come back later to upload election videos if your video isn't ready yet."} + +
+
+ {voteData && isVotingOpen && ( + + )} +
+
+ ); +}; + +export default RoundInfoPanel; diff --git a/packages/webapp/src/elections/components/ongoing-election-components/consensometer.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/consensometer.tsx similarity index 100% rename from packages/webapp/src/elections/components/ongoing-election-components/consensometer.tsx rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/consensometer.tsx diff --git a/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/index.ts b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/index.ts new file mode 100644 index 000000000..1a1536695 --- /dev/null +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/index.ts @@ -0,0 +1 @@ +export * from "./consensometer"; diff --git a/packages/webapp/src/elections/components/ongoing-election-components/meeting-link/index.ts b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/index.ts similarity index 100% rename from packages/webapp/src/elections/components/ongoing-election-components/meeting-link/index.ts rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/index.ts diff --git a/packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-buttons.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-buttons.tsx similarity index 100% rename from packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-buttons.tsx rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-buttons.tsx diff --git a/packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-link-modal.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-link-modal.tsx similarity index 100% rename from packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-link-modal.tsx rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-link-modal.tsx diff --git a/packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-link.md b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-link.md similarity index 100% rename from packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-link.md rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-link.md diff --git a/packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-link.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-link.tsx similarity index 98% rename from packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-link.tsx rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-link.tsx index e8679b4ae..576b13fab 100644 --- a/packages/webapp/src/elections/components/ongoing-election-components/meeting-link/meeting-link.tsx +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/round-info/meeting-link/meeting-link.tsx @@ -30,7 +30,6 @@ export enum MeetingStep { interface RequestMeetingLinkProps { roundIndex: number; meetingStartTime: Dayjs; - meetingDurationMs: number; electionConfig: ActiveStateConfigType; stage: RoundStage; } @@ -42,7 +41,6 @@ interface RequestMeetingLinkProps { export const MeetingLink = ({ roundIndex, meetingStartTime, - meetingDurationMs, electionConfig, stage, }: RequestMeetingLinkProps) => { @@ -138,7 +136,7 @@ export const MeetingLink = ({ await checkSubmissionIsAllowed(); const topic = `Eden Election - Round #${roundIndex + 1}`; - const durationInMinutes = meetingDurationMs / 1000 / 60; + const durationInMinutes = electionEnvVars.meetingDurationMs / 1000 / 60; const startTime = meetingStartTime.toISOString().split(".")[0] + "Z"; const responseData = await generateZoomMeetingLink( diff --git a/packages/webapp/src/elections/components/ongoing-election-components/voting-round-participants.tsx b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/voting-round-participants.tsx similarity index 89% rename from packages/webapp/src/elections/components/ongoing-election-components/voting-round-participants.tsx rename to packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/voting-round-participants.tsx index e66e22182..0815e654b 100644 --- a/packages/webapp/src/elections/components/ongoing-election-components/voting-round-participants.tsx +++ b/packages/webapp/src/elections/components/ongoing-election-components/ongoing-round/voting-round-participants.tsx @@ -1,14 +1,14 @@ import { Flipper, Flipped } from "react-flip-toolkit"; + import { VotingMemberChip } from "elections"; import { VoteData } from "elections/interfaces"; -import { MembersGrid } from "members"; -import { MemberData } from "members/interfaces"; +import { MemberNFT } from "nfts/interfaces"; interface VotingRoundParticipantsProps { - members?: MemberData[]; + members?: MemberNFT[]; voteData: VoteData[]; - selectedMember: MemberData | null; - onSelectMember: (member: MemberData) => void; + selectedMember: MemberNFT | null; + onSelectMember: (member: MemberNFT) => void; userVotingFor?: string; } @@ -19,7 +19,7 @@ const VotingRoundParticipants = ({ onSelectMember, userVotingFor, }: VotingRoundParticipantsProps) => { - const getVoteCountForMember = (member: MemberData) => { + const getVoteCountForMember = (member: MemberNFT) => { return voteData.filter((vd) => vd.candidate === member.account).length; }; @@ -27,7 +27,7 @@ const VotingRoundParticipants = ({ (a, b) => getVoteCountForMember(b) - getVoteCountForMember(a) ); - const selectMember = (member: MemberData) => { + const selectMember = (member: MemberNFT) => { if (member.account === selectedMember?.account) return; onSelectMember(member); }; diff --git a/packages/webapp/src/elections/components/video-upload-button.tsx b/packages/webapp/src/elections/components/ongoing-election-components/video-upload-button.tsx similarity index 100% rename from packages/webapp/src/elections/components/video-upload-button.tsx rename to packages/webapp/src/elections/components/ongoing-election-components/video-upload-button.tsx diff --git a/packages/webapp/src/elections/interfaces.ts b/packages/webapp/src/elections/interfaces.ts index 3febe8553..53e743f9e 100644 --- a/packages/webapp/src/elections/interfaces.ts +++ b/packages/webapp/src/elections/interfaces.ts @@ -1,4 +1,5 @@ -import { EdenMember, MemberData } from "members"; +import { EdenMember } from "members/interfaces"; +import { MemberNFT } from "nfts/interfaces"; const NUM_PARTICIPANTS_IN_SORTITION_ROUND = 1; const MAX_PARTICIPANTS_IN_SORTITION_ROUND = 13; @@ -105,7 +106,7 @@ export enum RoundStage { export interface ElectionCompletedRound { participants: EdenMember[]; // .length will be number of participants and empty if no round happened - participantsMemberData?: MemberData[]; + participantsMemberData?: MemberNFT[]; didReachConsensus?: boolean; delegate?: EdenMember; } @@ -120,6 +121,6 @@ export interface Election { ongoingRound: { roundIndex?: number; participants: EdenMember[]; - participantsMemberData: MemberData[]; + participantsMemberData: MemberNFT[]; }; } diff --git a/packages/webapp/src/inductions/components/induction-journeys/common/member-card-preview.tsx b/packages/webapp/src/inductions/components/induction-journeys/common/member-card-preview.tsx index 0f612abd6..c21708faf 100644 --- a/packages/webapp/src/inductions/components/induction-journeys/common/member-card-preview.tsx +++ b/packages/webapp/src/inductions/components/induction-journeys/common/member-card-preview.tsx @@ -1,19 +1,19 @@ import { Card } from "_app"; -import { MemberData, MemberHoloCard, MemberCard } from "members"; +import { Member, MemberHoloCard, MemberCard } from "members"; export const MemberCardPreview = ({ cardTitle = "Invitee information", - memberData, + member, }: { cardTitle?: string; - memberData: MemberData; + member: Member; }) => (
- +
- +
); diff --git a/packages/webapp/src/inductions/components/induction-journeys/invitee-journey.tsx b/packages/webapp/src/inductions/components/induction-journeys/invitee-journey.tsx index ef218dbcd..c046ab655 100644 --- a/packages/webapp/src/inductions/components/induction-journeys/invitee-journey.tsx +++ b/packages/webapp/src/inductions/components/induction-journeys/invitee-journey.tsx @@ -1,10 +1,10 @@ import React, { Dispatch, SetStateAction, useState } from "react"; import { Heading, Link, Text, useIsCommunityActive } from "_app"; -import { MemberData } from "members"; +import { Member } from "members"; import { - convertPendingProfileToMemberData, + convertPendingProfileToMember, EndorsementsStatus, InductionExpiresIn, InductionStepGenesis, @@ -96,7 +96,7 @@ export default InviteeJourney; interface ContainerProps { step: InductionStepInvitee | InductionStepGenesis; - memberPreview?: MemberData; + memberPreview?: Member; children: React.ReactNode; } @@ -105,7 +105,7 @@ const Container = ({ step, memberPreview, children }: ContainerProps) => ( {children} - {memberPreview && } + {memberPreview && } ); @@ -188,7 +188,7 @@ const PendingCeremonyVideoStep = ({ induction, setIsRevisitingProfile, }: PendingCeremonyVideoStepProps) => { - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -196,7 +196,7 @@ const PendingCeremonyVideoStep = ({ return ( @@ -220,7 +220,7 @@ const PendingEndorsementStep = ({ endorsements, setIsRevisitingProfile, }: PendingCompletionStepProps) => { - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -228,7 +228,7 @@ const PendingEndorsementStep = ({ return ( Endorsements @@ -256,7 +256,7 @@ const PendingDonationStep = ({ endorsements, setIsRevisitingProfile, }: PendingCompletionStepProps) => { - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -269,7 +269,7 @@ const PendingDonationStep = ({ ? InductionStepInvitee.Donate : InductionStepGenesis.Donate } - memberPreview={memberData} + memberPreview={member} > Pending donation diff --git a/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-form.tsx b/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-form.tsx index 8665c56b1..8ed4a2b85 100644 --- a/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-form.tsx +++ b/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-form.tsx @@ -1,17 +1,10 @@ import { FormEvent, useState } from "react"; -import { - useFormFields, - Form, - Heading, - Button, - HelpLink, - handleFileChange, - Image, -} from "_app"; import { edenContractAccount, validUploadActions } from "config"; -import { EdenNftSocialHandles } from "nfts"; +import { useFormFields, handleFileChange, ipfsUrl } from "_app"; +import { Form, Heading, Button, HelpLink, Image } from "_app/ui"; import { NewMemberProfile } from "inductions"; +import { MemberSocialHandles } from "members/interfaces"; interface Props { newMemberProfile: NewMemberProfile; @@ -48,7 +41,7 @@ export const InductionProfileForm = ({ const socialHandles = { ...socialFields }; Object.keys(socialHandles).forEach((keyString) => { - const key = keyString as keyof EdenNftSocialHandles; + const key = keyString as keyof MemberSocialHandles; if (!socialHandles[key]) delete socialHandles[key]; }); @@ -235,8 +228,8 @@ const ProfileImage = ({ image }: { image?: File | string }) => { ); let imageUrl: string; - if (typeof image === "string") { - imageUrl = `https://ipfs.io/ipfs/${image}`; + if (typeof image == "string") { + imageUrl = ipfsUrl(image); } else { imageUrl = URL.createObjectURL(image); } @@ -251,9 +244,7 @@ const ProfileImage = ({ image }: { image?: File | string }) => { ); }; -const convertNewMemberProfileSocial = ( - social: string -): EdenNftSocialHandles => { +const convertNewMemberProfileSocial = (social: string): MemberSocialHandles => { const socialHandles = JSON.parse(social || "{}"); return { eosCommunity: socialHandles.eosCommunity || "", diff --git a/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-preview.tsx b/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-preview.tsx index ad6117b69..8a1631b1e 100644 --- a/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-preview.tsx +++ b/packages/webapp/src/inductions/components/induction-journeys/invitee/profile-preview.tsx @@ -11,7 +11,7 @@ import { useUALAccount, } from "_app"; import { - convertPendingProfileToMemberData, + convertPendingProfileToMember, MemberCardPreview, setInductionProfileTransaction, } from "inductions"; @@ -70,7 +70,7 @@ export const InductionProfilePreview = ({ setDidSubmitProfile(true); } catch (error) { - onError(error, "Unable to set the profile"); + onError(error as Error, "Unable to set the profile"); } setIsLoading(false); }; @@ -81,7 +81,7 @@ export const InductionProfilePreview = ({ const img = URL.createObjectURL(selectedPhoto); pendingProfile = { ...profileInfo!, img }; } - return convertPendingProfileToMemberData( + return convertPendingProfileToMember( pendingProfile!, induction.invitee ); @@ -89,7 +89,7 @@ export const InductionProfilePreview = ({ return ( <> - +
( {children} - {memberPreview && } + {memberPreview && } ); @@ -125,7 +125,7 @@ const RecommendReview = ({ ); const SubmittedVideoStep = ({ induction }: { induction: Induction }) => { - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -133,7 +133,7 @@ const SubmittedVideoStep = ({ induction }: { induction: Induction }) => { return ( @@ -151,7 +151,7 @@ const VideoStep = ({ isRevisitingVideo, setSubmittedVideo, }: VideoStepProps) => { - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -159,7 +159,7 @@ const VideoStep = ({ return ( { const [ualAccount] = useUALAccount(); - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -209,7 +209,7 @@ const PendingEndorsementStep = ({ return ( Endorsements @@ -241,7 +241,7 @@ const PendingDonationStep = ({ setIsRevisitingVideo, }: PendingCompletionProps) => { const { data: isCommunityActive } = useIsCommunityActive(); - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -253,7 +253,7 @@ const PendingDonationStep = ({ ? InductionStepInviter.PendingDonation : InductionStepGenesis.Donate } - memberPreview={memberData} + memberPreview={member} > Pending donation diff --git a/packages/webapp/src/inductions/components/induction-journeys/third-party-journey.tsx b/packages/webapp/src/inductions/components/induction-journeys/third-party-journey.tsx index f3b9359a7..d1729a262 100644 --- a/packages/webapp/src/inductions/components/induction-journeys/third-party-journey.tsx +++ b/packages/webapp/src/inductions/components/induction-journeys/third-party-journey.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Heading, Text, useIsCommunityActive } from "_app"; -import { MemberData } from "members"; +import { Member } from "members"; import { - convertPendingProfileToMemberData, + convertPendingProfileToMember, EndorsementsStatus, InductionExpiresIn, InductionStepGenesis, @@ -17,7 +17,7 @@ import { Endorsement, Induction, InductionStatus } from "inductions/interfaces"; interface ContainerProps { step: InductionStepInvitee | InductionStepGenesis; - memberPreview?: MemberData; + memberPreview?: Member; children: React.ReactNode; } @@ -26,7 +26,7 @@ const Container = ({ step, memberPreview, children }: ContainerProps) => ( {children} - {memberPreview && } + {memberPreview && } ); @@ -83,7 +83,7 @@ const PendingProfileStep = ({ induction }: { induction: Induction }) => { }; const PendingVideoStep = ({ induction }: { induction: Induction }) => { - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -91,7 +91,7 @@ const PendingVideoStep = ({ induction }: { induction: Induction }) => { return ( @@ -107,7 +107,7 @@ const PendingEndorsementStep = ({ induction, endorsements, }: PendingCompletionProps) => { - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -115,7 +115,7 @@ const PendingEndorsementStep = ({ return ( Endorsements @@ -132,7 +132,7 @@ const PendingDonationStep = ({ endorsements, }: PendingCompletionProps) => { const { data: isCommunityActive } = useIsCommunityActive(); - const memberData = convertPendingProfileToMemberData( + const member = convertPendingProfileToMember( induction.new_member_profile, induction.invitee, induction.video @@ -144,7 +144,7 @@ const PendingDonationStep = ({ ? InductionStepInvitee.Donate : InductionStepGenesis.Donate } - memberPreview={memberData} + memberPreview={member} > Pending donation diff --git a/packages/webapp/src/inductions/utils.ts b/packages/webapp/src/inductions/utils.ts index 6564b6b0a..e714ffc75 100644 --- a/packages/webapp/src/inductions/utils.ts +++ b/packages/webapp/src/inductions/utils.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; -import { eosBlockTimestampISO } from "_app"; -import { MemberData, memberDataDefaults } from "members"; +import { eosBlockTimestampISO, ipfsUrl } from "_app"; +import { Member, memberDefaults } from "members"; import { Endorsement, Induction, @@ -12,19 +12,27 @@ import { const INDUCTION_EXPIRATION_DAYS = 7; -export const convertPendingProfileToMemberData = ( +export const convertPendingProfileToMember = ( profile: NewMemberProfile, inviteeChainAccountName: string, inductionVideo?: string -): MemberData => ({ - ...memberDataDefaults, - name: profile.name, - image: profile.img, - account: inviteeChainAccountName, - bio: profile.bio, - socialHandles: JSON.parse(profile.social || "{}"), - inductionVideo: inductionVideo || "", - attributions: profile.attributions || "", +): Member => ({ + ...memberDefaults, + accountName: inviteeChainAccountName, + profile: { + name: profile.name, + image: { + cid: profile.img, + url: ipfsUrl(profile.img), + attributions: profile.attributions || "", + }, + bio: profile.bio, + socialHandles: JSON.parse(profile.social || "{}"), + }, + inductionVideo: { + cid: inductionVideo || "", + url: inductionVideo ? ipfsUrl(inductionVideo) : "", + }, }); export const getInductionStatus = ( diff --git a/packages/webapp/src/members/api/fixtures.ts b/packages/webapp/src/members/api/fixtures.ts index 37f0fb76b..629c02f9a 100644 --- a/packages/webapp/src/members/api/fixtures.ts +++ b/packages/webapp/src/members/api/fixtures.ts @@ -1,5 +1,6 @@ -import { EdenMember, MemberData, MemberStats } from "members"; import { ElectionParticipationStatus, MemberStatus } from "_app/api/interfaces"; +import { EdenMember, MemberStats } from "members"; +import { MemberNFT } from "nfts/interfaces"; export const fixtureEdenMembers: EdenMember[] = [ { @@ -149,7 +150,7 @@ export const fixtureEdenMembers: EdenMember[] = [ }, ]; -export const fixtureMemberData: MemberData[] = [ +export const fixtureMemberData: MemberNFT[] = [ { templateId: 147800, name: "Alice", @@ -484,5 +485,5 @@ export const fixtureEdenMembersInGroup = ( export const getFixtureEdenMember = (memberAccount: string): EdenMember => fixtureEdenMembers.find((member) => member.account === memberAccount)!; -export const getFixtureMemberData = (memberAccount: string): MemberData => +export const getFixtureMemberData = (memberAccount: string): MemberNFT => fixtureMemberData.find((member) => member.account === memberAccount)!; diff --git a/packages/webapp/src/members/api/members.ts b/packages/webapp/src/members/api/members.ts index 630b8d71b..7bd923ece 100644 --- a/packages/webapp/src/members/api/members.ts +++ b/packages/webapp/src/members/api/members.ts @@ -3,12 +3,12 @@ import { getAccountCollection, getAuctions, getTemplates } from "nfts/api"; import { AssetData, AuctionableTemplateData, - EdenNftSocialHandles, + MemberNFT, TemplateData, } from "nfts/interfaces"; -import { MemberData } from "../interfaces"; import { fixtureMemberData } from "./fixtures"; +import { Member, MemberSocialHandles } from "../interfaces"; export const getMembers = async ( page: number, @@ -16,7 +16,7 @@ export const getMembers = async ( ids: string[] = [], sortField = "created", order = "asc" -): Promise => { +): Promise => { if (devUseFixtureData) { let data = fixtureMemberData; if (ids.length) { @@ -33,14 +33,14 @@ export const getMembers = async ( export const getNewMembers = async ( page?: number, limit?: number -): Promise => { +): Promise => { const data = await getAuctions(edenContractAccount, undefined, page, limit); return data.map(convertAtomicAssetToMemberWithSalesData); }; -export const getCollection = async (account: string): Promise => { +export const getCollection = async (account: string): Promise => { const assets = await getAccountCollection(account); - const members: MemberData[] = assets.map(convertAtomicAssetToMember); + const members: MemberNFT[] = assets.map(convertAtomicAssetToMember); const assetsOnAuction = await getAuctions(account); assetsOnAuction .map(convertAtomicAssetToMemberWithSalesData) @@ -48,7 +48,7 @@ export const getCollection = async (account: string): Promise => { return members.sort((a, b) => a.createdAt - b.createdAt); }; -export const memberDataDefaults = { +export const memberNFTDefaults: MemberNFT = { templateId: 0, name: "", image: "", @@ -59,8 +59,29 @@ export const memberDataDefaults = { attributions: "", createdAt: 0, }; -const convertAtomicTemplateToMember = (data: TemplateData): MemberData => ({ - ...memberDataDefaults, + +export const memberDefaults: Member = { + createdAt: 0, + accountName: "", + profile: { + name: "", + image: { + cid: "", + url: "", + attributions: "", + }, + bio: "", + socialHandles: {}, + }, + inductionVideo: { + cid: "", + url: "", + }, + participatingInElection: false, +}; + +const convertAtomicTemplateToMember = (data: TemplateData): MemberNFT => ({ + ...memberNFTDefaults, templateId: parseInt(data.template_id), createdAt: parseInt(data.created_at_time), name: data.immutable_data.name, @@ -72,7 +93,7 @@ const convertAtomicTemplateToMember = (data: TemplateData): MemberData => ({ socialHandles: parseSocial(data.immutable_data.social || "{}"), }); -const convertAtomicAssetToMember = (data: AssetData): MemberData => ({ +const convertAtomicAssetToMember = (data: AssetData): MemberNFT => ({ ...convertAtomicTemplateToMember(data.template), assetData: { assetId: data.asset_id, @@ -83,7 +104,7 @@ const convertAtomicAssetToMember = (data: AssetData): MemberData => ({ const convertAtomicAssetToMemberWithSalesData = ( data: AuctionableTemplateData -): MemberData => { +): MemberNFT => { const member = convertAtomicTemplateToMember(data); member.assetData = { assetId: data.assetId, @@ -99,7 +120,7 @@ const convertAtomicAssetToMemberWithSalesData = ( return member; }; -const parseSocial = (socialHandlesJsonString: string): EdenNftSocialHandles => { +const parseSocial = (socialHandlesJsonString: string): MemberSocialHandles => { try { return JSON.parse(socialHandlesJsonString); } catch (e) { diff --git a/packages/webapp/src/members/components/home/members-list.tsx b/packages/webapp/src/members/components/home/members-list.tsx index f8153df8b..8a08e9675 100644 --- a/packages/webapp/src/members/components/home/members-list.tsx +++ b/packages/webapp/src/members/components/home/members-list.tsx @@ -7,18 +7,22 @@ import { } from "react-virtualized"; import { LoadingContainer, MessageContainer } from "_app/ui"; -import { MemberChip, MemberData, useMembersWithAssets } from "members"; +import { Member, MemberChip, useMembersWithAssets } from "members"; +import { MemberNFT } from "nfts/interfaces"; -const findMember = (member: MemberData, query: string) => - member.account.includes(query.toLowerCase()) || - member.name.toLowerCase().includes(query.toLowerCase()); +const findMember = ( + memberData: { member: Member; nft?: MemberNFT }, + query: string +) => + memberData.member.accountName.includes(query.toLowerCase()) || + memberData.member.profile.name.toLowerCase().includes(query.toLowerCase()); interface Props { searchValue: string; } export const MembersList = ({ searchValue }: Props) => { - const { members: allMembers, isLoading, isError } = useMembersWithAssets(); + const { data, isLoading, isError } = useMembersWithAssets(); if (isLoading) { return ; @@ -33,7 +37,7 @@ export const MembersList = ({ searchValue }: Props) => { ); } - if (!allMembers.length) { + if (!data.length) { return ( { ); } - const members = allMembers.filter((member) => - findMember(member, searchValue) + const members = data.filter((memberData) => + findMember(memberData, searchValue) ); return ( @@ -62,13 +66,16 @@ export const MembersList = ({ searchValue }: Props) => { onScroll={onChildScroll} rowCount={members.length} rowHeight={77} - rowRenderer={({ index, style }: ListRowProps) => ( - - )} + rowRenderer={({ index, style }: ListRowProps) => { + const { nft, member } = members[index]; + return ( + + ); + }} noRowsRenderer={() => ( { return ( -
+
- {showBalance && } + {showBalance && } - + {member.inductionVideo && ( )} - +
); diff --git a/packages/webapp/src/members/components/member-chip-components/nft-info.tsx b/packages/webapp/src/members/components/member-chip-components/nft-info.tsx index 69b4c7964..4da6906c6 100644 --- a/packages/webapp/src/members/components/member-chip-components/nft-info.tsx +++ b/packages/webapp/src/members/components/member-chip-components/nft-info.tsx @@ -3,9 +3,9 @@ import React from "react"; import { atomicAssets } from "config"; import { assetToLocaleString, openInNewTab, useCountdown } from "_app"; import { NFT } from "_app/ui/icons"; -import { AssetData, MemberAuctionData, MemberData } from "members"; +import { MemberNFTAssetData, MemberNFTAuctionData, MemberNFT } from "nfts"; -export const NFTInfo = ({ member }: { member: MemberData }) => { +export const NFTInfo = ({ member }: { member: MemberNFT }) => { if (member.auctionData) { return ; } @@ -23,7 +23,11 @@ export const NFTInfo = ({ member }: { member: MemberData }) => { export default NFTInfo; -const AuctionBadge = ({ auctionData }: { auctionData: MemberAuctionData }) => { +const AuctionBadge = ({ + auctionData, +}: { + auctionData: MemberNFTAuctionData; +}) => { const countdown = useCountdown({ endTime: new Date(auctionData.bidEndTime as number), interval: 30000, @@ -52,7 +56,7 @@ const SaleBadge = ({ assetData, saleId, }: { - assetData: AssetData; + assetData: MemberNFTAssetData; saleId: string; }) => (
); -const AssetBadge = ({ assetData }: { assetData: AssetData }) => ( +const AssetBadge = ({ assetData }: { assetData: MemberNFTAssetData }) => ( { e.stopPropagation(); diff --git a/packages/webapp/src/members/components/member-chip.tsx b/packages/webapp/src/members/components/member-chip.tsx index 7a8d58fda..e0fe828a7 100644 --- a/packages/webapp/src/members/components/member-chip.tsx +++ b/packages/webapp/src/members/components/member-chip.tsx @@ -4,31 +4,59 @@ import dayjs from "dayjs"; import { blockExplorerAccountBaseUrl } from "config"; import { ROUTES } from "_app/routes"; -import { GenericMemberChip, openInNewTab } from "_app"; +import { GenericMemberChip, ipfsUrl, openInNewTab } from "_app"; +import { MemberNFT } from "nfts/interfaces"; import { MemberChipTelegramLink, NFTInfo } from "./member-chip-components"; -import { MemberData } from "../interfaces"; +import { Member } from "../interfaces"; + +const isNFT = (member: Member | MemberNFT): member is MemberNFT => + (member as MemberNFT).name !== undefined; interface MemberChipProps { - member: MemberData; + member: Member | MemberNFT; [x: string]: any; } export const MemberChip = ({ member, ...containerProps }: MemberChipProps) => { const router = useRouter(); + let accountName: string; + let name: string; + + if (isNFT(member)) { + accountName = member.account; + name = member.name; + } else { + accountName = member.accountName; + name = member.profile.name; + } + const onClick = (e: React.MouseEvent) => { - if (member.account) { + if (accountName) { e.stopPropagation(); - router.push(`${ROUTES.MEMBERS.href}/${member.account}`); + router.push(`${ROUTES.MEMBERS.href}/${accountName}`); } else { - openInNewTab(`${blockExplorerAccountBaseUrl}/${member.name}`); + openInNewTab(`${blockExplorerAccountBaseUrl}/${name}`); } }; + if (isNFT(member)) { + return ( + + } + {...containerProps} + /> + ); + } + return ( @@ -40,14 +68,13 @@ export const MemberChip = ({ member, ...containerProps }: MemberChipProps) => { export default MemberChip; -interface MemberDetailsProps { - member: MemberData; +interface MemberNFTDetailsProps { + member: MemberNFT; onClick?: (e: React.MouseEvent) => void; } -const MemberDetails = ({ member, onClick }: MemberDetailsProps) => { +const MemberNFTDetails = ({ member, onClick }: MemberNFTDetailsProps) => { const hasNftInfo = member.auctionData || member.assetData; - const isNotMember = member.createdAt === 0; const formattedJoinDate = dayjs(member.createdAt).format("YYYY.MM.DD"); return ( @@ -55,8 +82,6 @@ const MemberDetails = ({ member, onClick }: MemberDetailsProps) => {
{hasNftInfo ? ( - ) : isNotMember ? ( -

not an eden member

) : (

Joined {formattedJoinDate}

)} @@ -66,3 +91,29 @@ const MemberDetails = ({ member, onClick }: MemberDetailsProps) => {
); }; + +interface MemberDetailsProps { + member: Member; + onClick?: (e: React.MouseEvent) => void; +} + +const MemberDetails = ({ member, onClick }: MemberDetailsProps) => { + const isNotMember = member.createdAt === 0; + const formattedJoinDate = dayjs(member.createdAt).format("YYYY.MM.DD"); + + return ( +
+
+ {isNotMember ? ( +

not an eden member

+ ) : ( +

Joined {formattedJoinDate}

+ )} +
+

{member.profile.name}

+ +
+ ); +}; diff --git a/packages/webapp/src/members/components/member-collections.tsx b/packages/webapp/src/members/components/member-collections.tsx index 85e5dfd73..f919b7736 100644 --- a/packages/webapp/src/members/components/member-collections.tsx +++ b/packages/webapp/src/members/components/member-collections.tsx @@ -3,12 +3,13 @@ import { Tab } from "@headlessui/react"; import { Container, LoadingContainer, MessageContainer, Text } from "_app"; import { MemberChip, MembersGrid } from "members"; - -import { MemberData } from "../interfaces"; import { useMemberNFTCollection, useMemberNFTCollectors } from "nfts/hooks"; +import { MemberNFT } from "nfts/interfaces"; + +import { Member } from "../interfaces"; interface Props { - member: MemberData; + member: Member; } export const MemberCollections = ({ member }: Props) => { @@ -20,10 +21,16 @@ export const MemberCollections = ({ member }: Props) => { - + - + @@ -46,8 +53,16 @@ const StyledTab = ({ children }: { children: React.ReactNode }) => ( ); -const Collection = ({ member: { account, name } }: Props) => { - const { data: nfts, isLoading, isError } = useMemberNFTCollection(account); +const Collection = ({ + accountName, + name, +}: { + accountName: string; + name: string; +}) => { + const { data: nfts, isLoading, isError } = useMemberNFTCollection( + accountName + ); if (isLoading) return ; @@ -78,7 +93,7 @@ const Collection = ({ member: { account, name } }: Props) => { - {(member) => ( + {(member: MemberNFT) => ( { ); }; -const Collectors = ({ member: { account, name } }: Props) => { +const Collectors = ({ + accountName, + name, +}: { + accountName: string; + name: string; +}) => { const { data: collectors, isLoading, isError } = useMemberNFTCollectors( - account + accountName ); if (isLoading) return ; @@ -123,7 +144,7 @@ const Collectors = ({ member: { account, name } }: Props) => { - {(member) => ( + {(member: MemberNFT) => (
{inducted && ( @@ -59,13 +55,13 @@ export const MemberHoloCard = ({ className="font-medium leading-none" style={{ fontSize: width * 0.058 }} > - {member.name} + {member.profile.name}

- Eden: @{member.account} + Eden: @{member.accountName}

diff --git a/packages/webapp/src/members/components/member-social-links.tsx b/packages/webapp/src/members/components/member-social-links.tsx index 416719d53..a0fabad2c 100644 --- a/packages/webapp/src/members/components/member-social-links.tsx +++ b/packages/webapp/src/members/components/member-social-links.tsx @@ -5,40 +5,41 @@ import { IoChatbubblesOutline } from "react-icons/io5"; import { explorerAccountUrl, SocialButton } from "_app"; import { EosCommunityIcon } from "_app/ui/icons"; -import { MemberData } from "../interfaces"; +import { MemberSocialHandles } from "../interfaces"; import { getValidSocialLink } from "../helpers/social-links"; interface Props { - member: MemberData; + accountName: string; + socialHandles: MemberSocialHandles; } -export const MemberSocialLinks = ({ member }: Props) => { - const linkedinHandle = getValidSocialLink(member.socialHandles.linkedin); - const facebookHandle = getValidSocialLink(member.socialHandles.facebook); - const twitterHandle = getValidSocialLink(member.socialHandles.twitter); - const telegramHandle = getValidSocialLink(member.socialHandles.telegram); +export const MemberSocialLinks = ({ accountName, socialHandles }: Props) => { + const linkedinHandle = getValidSocialLink(socialHandles.linkedin); + const facebookHandle = getValidSocialLink(socialHandles.facebook); + const twitterHandle = getValidSocialLink(socialHandles.twitter); + const telegramHandle = getValidSocialLink(socialHandles.telegram); return (
- {member.socialHandles.eosCommunity && ( + {socialHandles.eosCommunity && ( )} - {member.socialHandles.blog && ( + {socialHandles.blog && ( )} {twitterHandle && ( diff --git a/packages/webapp/src/members/components/members-grid.tsx b/packages/webapp/src/members/components/members-grid.tsx index 02dd11741..7be8b966b 100644 --- a/packages/webapp/src/members/components/members-grid.tsx +++ b/packages/webapp/src/members/components/members-grid.tsx @@ -1,13 +1,16 @@ import React from "react"; -import { MemberData } from "../interfaces"; + +import { MemberNFT } from "nfts/interfaces"; + +import { Member } from "../interfaces"; interface Props { - members?: MemberData[]; + members?: Member[] | MemberNFT[]; dataTestId?: string; children( - value: MemberData, + value: Member | MemberNFT, index: number, - array: MemberData[] + array: Member[] | MemberNFT[] ): React.ReactNode; maxCols?: 1 | 2 | 3; } diff --git a/packages/webapp/src/members/helpers/formatters.ts b/packages/webapp/src/members/helpers/formatters.ts index da5eafd83..98285b6ce 100644 --- a/packages/webapp/src/members/helpers/formatters.ts +++ b/packages/webapp/src/members/helpers/formatters.ts @@ -1,12 +1,14 @@ -import { MemberData, MembersQueryNode } from "members/interfaces"; +import { ipfsUrl } from "_app"; +import { Member, MembersQueryNode } from "members/interfaces"; +import { MemberNFT } from "nfts/interfaces"; /******************************************** * MICROCHAIN GRAPHQL QUERY RESULT FORMATTERS *******************************************/ -export const formatQueriedMemberData = ( +export const formatMembersQueryNodeAsMemberNFT = ( data: MembersQueryNode -): MemberData | undefined => { +): MemberNFT | undefined => { if (!data) return; return { createdAt: data.createdAt ? new Date(data.createdAt).getTime() : 0, @@ -19,3 +21,43 @@ export const formatQueriedMemberData = ( inductionVideo: data.inductionVideo, }; }; + +export const formatMembersQueryNodeAsMember = ( + data: MembersQueryNode +): Member | undefined => { + if (!data) return; + return { + createdAt: data.createdAt ? new Date(data.createdAt).getTime() : 0, + accountName: data.account, + profile: { + name: data.profile.name, + image: { + cid: data.profile.img, + url: ipfsUrl(data.profile.img), + attributions: data.profile.attributions, + }, + bio: data.profile.bio, + socialHandles: JSON.parse(data.profile.social), + }, + inductionVideo: { + cid: data.inductionVideo, + url: ipfsUrl(data.inductionVideo), + }, + encryptionKey: undefined, // Include once exposed + participatingInElection: data.participating, + delegateRank: undefined, // Include once exposed + representativeAccountName: undefined, // Include once exposed + }; +}; + +// TODO: Remove after we transition everything we can to Member from MemberNFT +export const formatMemberAsMemberNFT = (member: Member): MemberNFT => ({ + createdAt: member.createdAt, + account: member.accountName, + name: member.profile.name, + image: member.profile.image.cid, + attributions: member.profile.image.attributions, + bio: member.profile.bio, + socialHandles: member.profile.socialHandles, + inductionVideo: member.inductionVideo.cid, +}); diff --git a/packages/webapp/src/members/hooks/queries.ts b/packages/webapp/src/members/hooks/queries.ts index 50c57a19c..3400ee1a7 100644 --- a/packages/webapp/src/members/hooks/queries.ts +++ b/packages/webapp/src/members/hooks/queries.ts @@ -1,12 +1,18 @@ import { useQuery as useReactQuery } from "react-query"; import { useQuery as useBoxQuery } from "@edenos/eden-subchain-client/dist/ReactSubchain"; -import { formatQueriedMemberData, getNewMembers } from "members"; -import { MemberData, MembersQuery } from "members/interfaces"; +import { useUALAccount } from "_app"; +import { + formatMembersQueryNodeAsMember, + getNewMembers, + formatMemberAsMemberNFT, +} from "members"; +import { Member, MembersQuery } from "members/interfaces"; +import { MemberNFT } from "nfts/interfaces"; export const MEMBER_DATA_FRAGMENT = ` - account createdAt + account profile { name img @@ -15,6 +21,7 @@ export const MEMBER_DATA_FRAGMENT = ` bio } inductionVideo + participating `; export const useMembers = () => { @@ -28,14 +35,14 @@ export const useMembers = () => { } }`); - let formattedMembers: MemberData[] = []; + let formattedMembers: Member[] = []; if (!result.data) return { ...result, data: formattedMembers }; const memberEdges = result.data.members.edges; if (memberEdges) { formattedMembers = memberEdges.map( - (member) => formatQueriedMemberData(member.node) as MemberData + (member) => formatMembersQueryNodeAsMember(member.node) as Member ); } @@ -56,11 +63,26 @@ export const useMemberByAccountName = (account: string) => { if (!result.data) return { ...result, data: null }; const memberNode = result.data.members.edges[0]?.node; - const member = formatQueriedMemberData(memberNode) ?? null; + const member = formatMembersQueryNodeAsMember(memberNode) ?? null; return { ...result, data: member }; }; -const sortMembersByDateDESC = (a: MemberData, b: MemberData) => +export const useCurrentMember = () => { + const [ualAccount] = useUALAccount(); + return useMemberByAccountName(ualAccount?.accountName); +}; + +export const useMembersByAccountNames = ( + accountNames: string[] | undefined = [] +) => { + const { data: allMembers, ...memberQueryMetaData } = useMembers(); + const members = allMembers.filter((member) => + accountNames.includes(member.accountName) + ); + return { data: members, ...memberQueryMetaData }; +}; + +const sortMembersByDateDESC = (a: Member, b: Member) => b.createdAt - a.createdAt; export const queryNewMembers = (page: number, pageSize: number) => ({ @@ -69,7 +91,7 @@ export const queryNewMembers = (page: number, pageSize: number) => ({ }); export const useMembersWithAssets = () => { - const NEW_MEMBERS_PAGE_SIZE = 10000; + const NEW_MEMBERS_PAGE_SIZE = 100; const newMembers = useReactQuery({ ...queryNewMembers(1, NEW_MEMBERS_PAGE_SIZE), }); @@ -80,20 +102,36 @@ export const useMembersWithAssets = () => { const isError = newMembers.isError || allMembers.isError || !allMembers.data; - let members: MemberData[] = []; + if (!allMembers.data.length) { + return { data: [], isLoading, isError }; + } - if (allMembers.data.length) { - const mergeAuctionData = (member: MemberData) => { - const newMemberRecord = newMembers.data?.find( - (newMember) => newMember.account === member.account - ); - return newMemberRecord ?? member; + const mergeAuctionData = (member: Member) => { + const memberNFT = newMembers.data?.find( + (newMember) => newMember.account === member.accountName + ); + if (!memberNFT) return { member }; + return { + member, + nft: memberNFT, }; + }; - members = allMembers.data - .sort(sortMembersByDateDESC) - .map(mergeAuctionData); - } + const data: { member: Member; nft?: MemberNFT }[] = allMembers.data + .sort(sortMembersByDateDESC) + .map(mergeAuctionData); + + return { data, isLoading, isError }; +}; + +/******************************************************************************** + * TODO: Refactor the following away once reliant components use Member interface + ********************************************************************************/ - return { members, isLoading, isError }; +export const useMembersByAccountNamesAsMemberNFTs = ( + accountNames: string[] | undefined = [] +) => { + const query = useMembersByAccountNames(accountNames); + const memberNFTs = query.data.map(formatMemberAsMemberNFT); + return { ...query, data: memberNFTs }; }; diff --git a/packages/webapp/src/members/interfaces.ts b/packages/webapp/src/members/interfaces.ts index 45c597a8c..49f179b6f 100644 --- a/packages/webapp/src/members/interfaces.ts +++ b/packages/webapp/src/members/interfaces.ts @@ -1,35 +1,49 @@ -import { EdenNftSocialHandles } from "nfts/interfaces"; -import { Asset, ElectionParticipationStatus, MemberStatus } from "_app"; +import { ElectionParticipationStatus, MemberStatus } from "_app"; export type VoteDataQueryOptionsByField = { fieldName?: string; fieldValue: string; }; -export interface MemberData { - createdAt: number; - account: string; - name: string; - image: string; +interface ProfileImage { + cid: string; + url: string; attributions: string; +} + +interface InductionVideo { + cid: string; + url: string; +} + +interface MemberProfile { + name: string; + image: ProfileImage; bio: string; - socialHandles: EdenNftSocialHandles; - inductionVideo: string; - templateId?: number; - auctionData?: MemberAuctionData; - assetData?: AssetData; - saleId?: string; + socialHandles: MemberSocialHandles; } -export interface AssetData { - assetId: string; - templateMint: number; +export interface MemberSocialHandles { + eosCommunity?: string; + twitter?: string; + linkedin?: string; + telegram?: string; + facebook?: string; + blog?: string; } -export interface MemberAuctionData { - auctionId: string; - price: Asset; - bidEndTime?: number; +export interface Member { + createdAt: number; + accountName: string; + profile: MemberProfile; + inductionVideo: InductionVideo; + encryptionKey?: string; // Include once exposed (as optional) + // Member's participation status is updated once they lose a round (updated as soon as a new value is known), + // ie. a member's opt-in participation status lifetime is only from the start of Round 1 + // until the end of the Round they lose (or end of the election) + participatingInElection: boolean; + delegateRank?: number; // Include once exposed + representativeAccountName?: string; // Include once exposed } export interface EdenMember { @@ -80,4 +94,5 @@ export interface MembersQueryNode { bio: string; }; inductionVideo: string; + participating: boolean; } diff --git a/packages/webapp/src/nfts/api/nfts.ts b/packages/webapp/src/nfts/api/nfts.ts index 201c33a70..da76b6e35 100644 --- a/packages/webapp/src/nfts/api/nfts.ts +++ b/packages/webapp/src/nfts/api/nfts.ts @@ -38,7 +38,7 @@ export const getAuctions = async ( seller?: string, templateIds?: string[], page = 1, - limit = 9999 + limit = 100 // max 100 enforced by AA Auctions API ): Promise => { let url = `${atomicAssets.apiMarketUrl}/auctions?state=1&collection_name=${atomicAssets.collection}&schema_name=${atomicAssets.schema}&page=${page}&limit=${limit}&order=desc&sort=created${FETCH_AFTER_TIMESTAMP}`; diff --git a/packages/webapp/src/nfts/hooks/queries.ts b/packages/webapp/src/nfts/hooks/queries.ts index f93bb8e42..42996849e 100644 --- a/packages/webapp/src/nfts/hooks/queries.ts +++ b/packages/webapp/src/nfts/hooks/queries.ts @@ -2,18 +2,22 @@ import { useQuery as useReactQuery } from "react-query"; import { useQuery as useBoxQuery } from "@edenos/eden-subchain-client/dist/ReactSubchain"; import { atomicAssets, edenContractAccount } from "config"; -import { formatQueriedMemberData, MEMBER_DATA_FRAGMENT } from "members"; -import { getCollection, memberDataDefaults } from "members/api"; -import { MemberData, MembersQueryNode } from "members/interfaces"; -import { NFTCollectorsQuery } from "nfts/interfaces"; +import { + formatMembersQueryNodeAsMemberNFT, + MEMBER_DATA_FRAGMENT, +} from "members"; +import { getCollection, memberNFTDefaults } from "members/api"; +import { MembersQueryNode } from "members/interfaces"; +import { MemberNFT, NFTCollectorsQuery } from "nfts/interfaces"; +// NOTE: Eden member NFTs may be deprecated soon. export const queryMemberNFTCollection = (account: string) => ({ queryKey: ["query_member_nft_collection", account], queryFn: () => getCollection(account), }); export const useMemberNFTCollection = (account: string) => { - return useReactQuery({ + return useReactQuery({ ...queryMemberNFTCollection(account), }); }; @@ -37,7 +41,7 @@ export const useMemberNFTCollectors = (account: string) => { } }`); - let collectors: MemberData[] = []; + let collectors: MemberNFT[] = []; if (!result.data) return { ...result, data: collectors }; @@ -57,7 +61,7 @@ const isAuction = (account: string) => const formatCollectorAsMemberData = (owner: MembersQueryNode) => { if (owner.profile) { - return formatQueriedMemberData(owner) as MemberData; + return formatMembersQueryNodeAsMemberNFT(owner) as MemberNFT; } - return { ...memberDataDefaults, name: owner.account }; + return { ...memberNFTDefaults, name: owner.account }; }; diff --git a/packages/webapp/src/nfts/interfaces.ts b/packages/webapp/src/nfts/interfaces.ts index d2c93c5f1..e2717ff63 100644 --- a/packages/webapp/src/nfts/interfaces.ts +++ b/packages/webapp/src/nfts/interfaces.ts @@ -1,25 +1,39 @@ import { Asset } from "_app"; -import { MembersQueryNode } from "members/interfaces"; +import { MembersQueryNode, MemberSocialHandles } from "members/interfaces"; -export interface EdenNftData { - name: string; - img: string; +/****************************** + * NFT UI INTERFACES + *****************************/ +export interface MemberNFT { + createdAt: number; account: string; - bio: string; - video: string; + name: string; + image: string; attributions: string; - social?: string; + bio: string; + socialHandles: MemberSocialHandles; + inductionVideo: string; + templateId?: number; + auctionData?: MemberNFTAuctionData; + assetData?: MemberNFTAssetData; + saleId?: string; +} + +export interface MemberNFTAuctionData { + auctionId: string; + price: Asset; + bidEndTime?: number; } -export interface EdenNftSocialHandles { - eosCommunity?: string; - twitter?: string; - linkedin?: string; - telegram?: string; - facebook?: string; - blog?: string; +export interface MemberNFTAssetData { + assetId: string; + templateMint: number; } +/****************************** + * NFT API INTERFACES + *****************************/ + export interface TemplateData { template_id: string; immutable_data: EdenNftData; @@ -42,6 +56,16 @@ export interface AuctionableTemplateData extends TemplateData { templateMint: number; } +interface EdenNftData { + name: string; + img: string; + account: string; + bio: string; + video: string; + attributions: string; + social?: string; +} + /****************************** * NFT GRAPHQL QUERY INTERFACES *****************************/ diff --git a/packages/webapp/src/pages/delegates/index.tsx b/packages/webapp/src/pages/delegates/index.tsx index 4e7772dcc..38c0d4789 100644 --- a/packages/webapp/src/pages/delegates/index.tsx +++ b/packages/webapp/src/pages/delegates/index.tsx @@ -5,18 +5,20 @@ import { SideNavLayout, useCurrentElection, useElectionState, - useMemberDataFromEdenMembers, useMyDelegation, } from "_app"; import { Container, Heading, LoadingContainer, Text } from "_app/ui"; import { ElectionStatus } from "elections/interfaces"; -import { MemberGateContainer } from "members"; +import { + MemberGateContainer, + useMembersByAccountNamesAsMemberNFTs, +} from "members"; import { ErrorLoadingDelegation, ElectionInProgress, NoDelegationToDisplay, } from "delegates/components/statuses"; -import MyDelegation from "delegates/components/my-delegation"; // avoid circular depenency +import MyDelegation from "delegates/components/my-delegation"; // avoid circular dependency export const DelegatesPage = () => { const { @@ -45,7 +47,9 @@ export const DelegatesPage = () => { data: myDelegationMemberData, isLoading: isLoadingMemberData, isError: isErrorMemberData, - } = useMemberDataFromEdenMembers(myDelegation); + } = useMembersByAccountNamesAsMemberNFTs( + myDelegation?.map((delegate) => delegate.account) + ); const isLoading = isLoadingCurrentElection || diff --git a/packages/webapp/src/pages/election/components.tsx b/packages/webapp/src/pages/election/components.tsx index 0f6a5462b..c80724f39 100644 --- a/packages/webapp/src/pages/election/components.tsx +++ b/packages/webapp/src/pages/election/components.tsx @@ -6,6 +6,7 @@ import { dehydrate } from "react-query/hydration"; import { FluidLayout, queryMembersStats, queryMembers } from "_app"; import { Container, Heading } from "_app/ui"; import { MembersGrid } from "members"; +import { MemberNFT } from "nfts/interfaces"; import { VotingMemberChip, DelegateChip } from "elections"; const MEMBERS_PAGE_SIZE = 18; @@ -48,7 +49,7 @@ export const MembersPage = (props: Props) => { {/* TODO: Hard-coded values here should come from fixtures. */} - {(member) => ( + {(member: MemberNFT) => ( { {members.error && "Fail to load members"} - {(member) => ( + {(member: MemberNFT) => ( { const { @@ -254,7 +254,7 @@ const LoaderSection = () => ( interface HeaderProps { isOngoing: boolean; roundIndex: number; - winner?: MemberData; + winner?: MemberNFT; roundStartTime?: dayjs.Dayjs; roundEndTime?: dayjs.Dayjs; } diff --git a/packages/webapp/src/pages/election/stats.tsx b/packages/webapp/src/pages/election/stats.tsx index c3bd15de6..7a9dbb2c9 100644 --- a/packages/webapp/src/pages/election/stats.tsx +++ b/packages/webapp/src/pages/election/stats.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { RoundBasicQueryData, RoundGroupQueryData, @@ -8,7 +9,6 @@ import { useCurrentGlobalElectionData, } from "_app"; import { Container, Heading, Loader, Expander, Text } from "_app/ui"; - import { Avatars, DelegateChip, @@ -18,11 +18,10 @@ import { VoteData, VotingMemberChip, } from "elections"; -import { MemberData, MembersGrid } from "members"; -import { - ConsensometerBlocks, - RoundHeader, -} from "elections/components/ongoing-election-components"; +import { MembersGrid } from "members"; +import { MemberNFT } from "nfts/interfaces"; +import { RoundHeader } from "elections/components/ongoing-election-components"; +import { ConsensometerBlocks } from "elections/components/ongoing-election-components/ongoing-round/round-info/consensometer"; export const ElectionStatsPage = () => { const { @@ -185,7 +184,7 @@ const GroupSegment = ({ // TODO: revisit this, unfortunately the MembersGrid only accepts MemberData, // even though we don't need it to display the required summarized member // chip data - const members: MemberData[] = group.votes.map((vote) => vote.voter); + const members: MemberNFT[] = group.votes.map((vote) => vote.voter); const membersStats = group.votes.reduce((membersVotingMap, vote) => { membersVotingMap[vote.voter.account] = { @@ -226,7 +225,7 @@ const GroupSegment = ({ }; interface GroupProps { - members: MemberData[]; + members: MemberNFT[]; isFinished: boolean; groupMembersStats: GroupMembersStats; header?: React.ReactNode; @@ -236,7 +235,7 @@ const RegularGroup = ({ members, groupMembersStats, header }: GroupProps) => { return ( - {(member) => { + {(member: MemberNFT) => { return ( { return ( - {(member) => { + {(member: MemberNFT) => { const delegateTitle = isFinished && groupMembersStats[member.account].isDelegate ? "Head Chief" diff --git a/packages/webapp/src/pages/members/[id].tsx b/packages/webapp/src/pages/members/[id].tsx index ae63227bf..4b73dd724 100644 --- a/packages/webapp/src/pages/members/[id].tsx +++ b/packages/webapp/src/pages/members/[id].tsx @@ -42,8 +42,8 @@ export const MemberPage = () => { } return ( - - + + diff --git a/packages/webapp/src/pages/sessions.tsx b/packages/webapp/src/pages/sessions.tsx new file mode 100644 index 000000000..19396b813 --- /dev/null +++ b/packages/webapp/src/pages/sessions.tsx @@ -0,0 +1,85 @@ +import { + Button, + onError, + SideNavLayout, + useCurrentMember, + useUALAccount, +} from "_app"; +import { + signAndBroadcastSessionTransaction, + generateSessionKey, + newSessionTransaction, + sessionKeysStorage, +} from "_app/eos/sessions"; +import { initializeInductionTransaction } from "inductions"; + +export const Sessions = () => { + const { data: currentMember } = useCurrentMember(); + const [ualAccount] = useUALAccount(); + + // Example to be called when user logs in to eden or when the session is expiring + // and a new one is desired + const onCreateSessionExample = async () => { + try { + const newSessionKey = await generateSessionKey(); + console.info("created session key", newSessionKey); + + const transaction = await newSessionTransaction( + ualAccount.accountName, + newSessionKey + ); + console.info("generated newsession transaction", transaction); + + const signedTrx = await ualAccount.signTransaction(transaction, { + broadcast: true, + }); + console.info("newsession signedTrx", signedTrx); + + await sessionKeysStorage.saveKey(newSessionKey); + + return { newSessionKey }; + } catch (e) { + console.error(e); + onError(e as Error); + } + }; + + const onRunSessionExample = async () => { + try { + const { + transaction: inductionTrx, + } = initializeInductionTransaction(ualAccount.accountName, "ahab", [ + "pip", + "egeon", + ]); + + // extract the actions from the transaction + const { actions } = inductionTrx; + + // sign actions with session key + await signAndBroadcastSessionTransaction( + ualAccount.accountName, + actions + ); + } catch (e) { + console.error(e); + onError(e as Error); + } + }; + + return ( + +
+

Hi, {currentMember?.account}! Testing Sessions

+ + +
+
+ ); +}; + +export default Sessions; diff --git a/scripts/eden_chain_runner.sh b/scripts/eden_chain_runner.sh index fcdab550b..d7202d55a 100755 --- a/scripts/eden_chain_runner.sh +++ b/scripts/eden_chain_runner.sh @@ -14,9 +14,15 @@ cleos wallet import --private-key $CONTRACTS_PKS || true echo "Executing $RUNNER" cltester -v $RUNNER > eden-runner.log 2>&1 & +until cleos get info | grep -m 1 "chain_id"; do + echo "Waiting for nodeos to be responsive..." + sleep 1 +done sleep 5 cleos set abi eosio.token token.abi cleos set abi atomicassets atomicassets.abi cleos set abi atomicmarket atomicmarket.abi cleos set abi eden.gm eden.abi + +if [ -n "$2" ]; then tail -fn +1 eden-runner.log; fi diff --git a/yarn.lock b/yarn.lock index 2120f0826..c7be123ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6006,6 +6006,13 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +idb-keyval@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.0.3.tgz#e47246a15e55d0fff9fa204fd9ca06f90ff30c52" + integrity sha512-yh8V7CnE6EQMu9YDwQXhRxwZh4nv+8xm/HV4ZqK4IiYFJBWYGjJuykADJbSP+F/GDXUBwCSSNn/14IpGL81TuA== + dependencies: + safari-14-idb-fix "^3.0.0" + ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -9674,6 +9681,11 @@ rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.7: dependencies: tslib "^1.9.0" +safari-14-idb-fix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440" + integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog== + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"