From 7c9d7ec64dc6a948914734c67337d5e24506da46 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Sat, 29 Jul 2023 14:54:47 +0200 Subject: [PATCH 01/45] eos: Update to antelope CDT 3.1.0 - ABIs changed the field names of maps from "key" and "value" to "first" and "second". - `eosio-cpp` has been renamed to `cdt-cpp` - abigen is part of `cdt-cpp` and no longer a seperate build step - Note: there appears to be a bug in the ABI generation when using version 4.0.0, so we stick with 3.1.0 for now --- Makefile | 10 +++------- contracts/force/force.hpp | 1 - contracts/vaccount/vaccount.hpp | 2 +- tests/e2e/dao.cljs | 28 +++++++++++++--------------- tests/e2e/force.cljs | 6 +++--- 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index a01644a..c434f36 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ -EOS_CC ?= eosio-cpp -ABI_CC ?= eosio-abigen +EOS_CC ?= cdt-cpp SKIP_CONTRACTS := $(wildcard contracts/swap/*.cpp contracts/taskproxy/*.cpp) @@ -7,14 +6,11 @@ SRC = $(filter-out $(SKIP_CONTRACTS), $(wildcard contracts/*/*.cpp)) WASM = $(SRC:.cpp=.wasm) ABI = $(WASM:.wasm=.abi) -all: $(WASM) $(ABI) +all: $(WASM) -%.wasm: %.cpp %.hpp $(%-shared.hpp) +%.abi %.wasm: %.cpp %.hpp $(%-shared.hpp) $(EOS_CC) -o $@ $< -%.abi: %.cpp - $(ABI_CC) -contract=$(basename $(^F)) -output $@ $^ - .PHONY: serve-docs clean clean: diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index bebd59c..75b48d4 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -451,7 +451,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { indexed_by<"acc"_n, const_mem_fun>> quali_table; typedef multi_index<"userquali"_n, userquali> user_quali_table; - const eosio::name settings_pk = "settings"_n; struct [[eosio::table]] settings { diff --git a/contracts/vaccount/vaccount.hpp b/contracts/vaccount/vaccount.hpp index dca5a72..3e43036 100644 --- a/contracts/vaccount/vaccount.hpp +++ b/contracts/vaccount/vaccount.hpp @@ -6,7 +6,7 @@ #include #include -#include "../proposals/proposals-shared.hpp" +using namespace eosio; enum class VaccountType { Address = 0, diff --git a/tests/e2e/dao.cljs b/tests/e2e/dao.cljs index 26f01cc..aa501ce 100644 --- a/tests/e2e/dao.cljs +++ b/tests/e2e/dao.cljs @@ -6,7 +6,8 @@ [cljs.core.async :refer [go] ] [cljs.core.async.interop :refer [ Date: Sat, 29 Jul 2023 15:58:49 +0200 Subject: [PATCH 02/45] force: Remove batch qualifications and merkle proofs --- contracts/force/force.cpp | 100 ++++++++++++-------------------------- contracts/force/force.hpp | 46 ++++++------------ tests/e2e/force.cljs | 82 +++++++++---------------------- 3 files changed, 68 insertions(+), 160 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index e815c86..1d3a0b0 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -61,9 +61,13 @@ void force::rmcampaign(uint32_t campaign_id, vaccount::vaddress owner, vaccount: camp_tbl.erase(camp_itr); } -void force::mkbatch(uint32_t id, uint32_t campaign_id, content content, - checksum256 task_merkle_root, uint32_t repetitions, - std::optional qualis, eosio::name payer, vaccount::sig sig) { +void force::mkbatch(uint32_t id, + uint32_t campaign_id, + content content, + checksum256 task_merkle_root, + uint32_t repetitions, + eosio::name payer, + vaccount::sig sig) { campaign_table camp_tbl(_self, _self.value); auto& camp = camp_tbl.get(campaign_id, "campaign not found"); @@ -84,8 +88,6 @@ void force::mkbatch(uint32_t id, uint32_t campaign_id, content content, b.repetitions = repetitions; b.reward.emplace(camp.reward); b.num_tasks = 0; - if (qualis.has_value()) - b.qualis.emplace(qualis.value()); }); } @@ -249,16 +251,7 @@ void force::require_batchjoin(uint32_t account_id, uint64_t batch_pk, bool try_t auto camp = camp_tbl.get(batch.campaign_id, "campaign not found"); user_quali_table user_quali_tbl(_self, _self.value); - std::map qualis; - - if (!camp.qualis.has_value() && batch.qualis.has_value()) { - qualis = batch.qualis.value(); - } else { - qualis = camp.qualis.value(); - if (batch.qualis.has_value()) { - qualis.insert(batch.qualis.value().begin(), batch.qualis.value().end()); - } - } + auto qualis = camp.qualis.value();; for (auto q : qualis) { uint32_t quali_id = std::get<0>(q); @@ -283,34 +276,12 @@ void force::require_batchjoin(uint32_t account_id, uint64_t batch_pk, bool try_t } } -void force::require_merkle(std::vector proof, std::vector position, - checksum256 root, checksum256 data_hash) { - uint8_t hashlen = 32; - checksum256 last_hash = data_hash; - - for (size_t i = 0; i < proof.size(); i++) { - std::array arr1 = last_hash.extract_as_byte_array(); - std::array arr2 = proof[i].extract_as_byte_array(); - - std::array combined; - if (position[i] == 1) { - std::copy(arr1.cbegin(), arr1.cend(), combined.begin()); - std::copy(arr2.cbegin(), arr2.cend(), combined.begin() + 32); - } else if (position[i] == 0) { - std::copy(arr2.cbegin(), arr2.cend(), combined.begin()); - std::copy(arr1.cbegin(), arr1.cend(), combined.begin() + 32); - } - - void* ptr = static_cast(combined.data()); - last_hash = sha256((char*) ptr, 64); - } - - eosio::check(root == last_hash, "invalid merkle proof"); -} - -void force::reservetask(std::vector proof, std::vector position, - std::vector data, uint32_t campaign_id, uint32_t batch_id, - uint32_t account_id, name payer, vaccount::sig sig) { +void force::reservetask(uint32_t campaign_id, + uint32_t account_id, + uint32_t last_task_done, + name payer, + vaccount::sig sig) { + uint32_t batch_id = 0; submission_table submission_tbl(_self, _self.value); batch_table batch_tbl(_self, _self.value); campaign_table campaign_tbl(_self, _self.value); @@ -322,45 +293,36 @@ void force::reservetask(std::vector proof, std::vector pos auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); eosio::check(batch.tasks_done >= 0 && batch.num_tasks > 0, "batch paused"); - // TODO: verify depth of tree so cant be spoofed with partial proof - - // we prepend the batch_pk to the data so that each data point has a unique - // entry in the submission table. - const uint16_t datasize = data.size() + sizeof batch_pk; - char buff[datasize]; - std::copy(static_cast(static_cast(&batch_pk)), - static_cast(static_cast(&batch_pk)) + sizeof batch_pk, - &buff[0]); - std::copy(data.cbegin(), data.cend(), &buff[0] + sizeof batch_pk); - - checksum256 data_hash = sha256(&buff[0], datasize); - reservetask_params params = {6, data_hash, campaign_id, batch_id}; + + + // TODO: find task id + uint32_t task_idx = 1; + + reservetask_params params = {6, task_idx, campaign_id}; require_vaccount(account_id, pack(params), sig); // printhex(&data_hash.extract_as_byte_array()[0], 32); - require_merkle(proof, position, batch.task_merkle_root, data_hash); uint32_t submission_id = submission_tbl.available_primary_key(); // check if repetitions are not done - auto by_leaf = submission_tbl.get_index<"leaf"_n>(); - auto itr_start = by_leaf.lower_bound(data_hash); - auto itr_end = by_leaf.upper_bound(data_hash); - uint32_t rep_count = 0; - - for (; itr_start != itr_end; itr_start++) { - rep_count++; - auto& subm = *itr_start; - eosio::check(subm.account_id != account_id, "account already did task"); - } - eosio::check(rep_count < batch.repetitions, "task already completed"); + // auto by_leaf = submission_tbl.get_index<"leaf"_n>(); + // auto itr_start = by_leaf.lower_bound(data_hash); + // auto itr_end = by_leaf.upper_bound(data_hash); + // uint32_t rep_count = 0; + + // for (; itr_start != itr_end; itr_start++) { + // rep_count++; + // auto& subm = *itr_start; + // eosio::check(subm.account_id != account_id, "account already did task"); + // } + // eosio::check(rep_count < batch.repetitions, "task already completed"); submission_tbl.emplace(payer, [&](auto& s) { s.id = submission_id; s.account_id = account_id; - s.leaf_hash = data_hash; s.batch_id = batch_pk; s.paid = false; s.submitted_on = time_point_sec(now()); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 75b48d4..9ad78a5 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -32,15 +32,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { Exclusive = 1 }; - template - void cleanTable(name code, uint64_t account, const uint32_t batchSize){ - T db(code, account); - uint32_t counter = 0; - auto itr = db.begin(); - while (itr != db.end() && counter++ < batchSize) { - itr = db.erase(itr); - } - } force(eosio::name receiver, eosio::name code, eosio::datastream ds) : eosio::contract(receiver, code, ds), _config(_self, _self.value), _settings(_self, _self.value) @@ -80,7 +71,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { content content, checksum256 task_merkle_root, uint32_t repetitions, - std::optional qualis, eosio::name payer, vaccount::sig sig); @@ -104,12 +94,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { vaccount::sig sig); [[eosio::action]] - void reservetask(std::vector proof, - std::vector position, - std::vector data, - uint32_t campaign_id, - uint32_t batch_id, + void reservetask(uint32_t campaign_id, uint32_t account_id, + uint32_t last_task_done, eosio::name payer, vaccount::sig sig); @@ -172,15 +159,15 @@ class [[eosio::contract("force")]] force : public eosio::contract { vaccount::sig sig, std::optional fee); - [[eosio::action]] - void clean() { - require_auth(_self); - cleanTable(_self, _self.value, 100); - cleanTable(_self, _self.value, 100); - cleanTable(_self, _self.value, 100); - cleanTable(_self, _self.value, 100); - cleanTable(_self, _self.value, 100); - }; + // [[eosio::action]] + // void clean() { + // require_auth(_self); + // cleanTable(_self, _self.value, 100); + // cleanTable(_self, _self.value, 100); + // cleanTable(_self, _self.value, 100); + // cleanTable(_self, _self.value, 100); + // cleanTable(_self, _self.value, 100); + // }; [[eosio::action]] void migrate(eosio::name payer, eosio::name fee_contract, float fee_percentage) { @@ -224,10 +211,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { struct reservetask_params { uint8_t mark; - checksum256 leaf_hash; + uint32_t last_task_done; uint32_t campaign_id; - uint32_t batch_id; - EOSLIB_SERIALIZE(reservetask_params, (mark)(leaf_hash)(campaign_id)(batch_id)); + EOSLIB_SERIALIZE(reservetask_params, (mark)(last_task_done)(campaign_id)); }; struct submittask_params { @@ -346,11 +332,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t repetitions; uint32_t tasks_done; uint32_t num_tasks; - eosio::binary_extension> qualis; eosio::binary_extension reward; uint64_t primary_key() const { return (uint64_t{campaign_id} << 32) | id; } - uint32_t by_campaign() const { return campaign_id; } }; struct [[eosio::table]] batchjoin { @@ -380,17 +364,15 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t id; std::optional account_id; std::optional content; - checksum256 leaf_hash; uint64_t batch_id; std::optional data; bool paid; eosio::time_point_sec submitted_on; uint64_t primary_key() const { return id; } - checksum256 by_leaf() const { return leaf_hash; } uint64_t by_batch() const { return batch_id; } - EOSLIB_SERIALIZE(submission, (id)(account_id)(content)(leaf_hash)(batch_id) + EOSLIB_SERIALIZE(submission, (id)(account_id)(content)(batch_id) (data)(paid)(submitted_on)) }; diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 4fe8117..128b777 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -227,11 +227,12 @@ (.asUint8Array (doto (new (.-SerialBuffer Serialize)) (.push 13) (.pushUint32 acc-id)))) -(defn pack-reservetask-params [leaf-hash camp-id batch-id] +(defn pack-reservetask-params [last-task-done camp-id] (.asUint8Array (doto (new (.-SerialBuffer Serialize)) - (.push 6) (.pushUint8ArrayChecked (vacc/hex->bytes leaf-hash) 32) - (.pushUint32 camp-id) (.pushUint32 batch-id)))) + (.push 6) + (.pushUint32 last-task-done) + (.pushUint32 camp-id)))) (defn pack-submittask-params [sub-id data] (.asUint8Array @@ -358,13 +359,11 @@ :task_merkle_root merkle-root :repetitions 100 :payer acc-2 - :qualis nil :sig nil}))) (testing "campaign owner can create batch" (bytes %)) task-data-prep) - tree (MerkleTree. (clj->js leaves) sha256) - root (.toString (.getRoot tree) "hex") - ;; _ (prn "merkle root for " batch-pk root) - params-1 (pack-reservetask-params (first leaves) 0 0) - params-2 (pack-reservetask-params (second leaves) 0 0)] - (for [i (range 2)] - (let [proof (.getProof tree (nth leaves i))] - [(map #(buf->hex (.-data %)) proof) - (map #(if (= (.-position %) "left") 0 1) proof) - (nth leaves i)])))) - (async-deftest reservetask - (let [[proof-000 proof-001 & _] (make-proof "0000000000000000") - [proof-200 & _] (make-proof "0200000000000000") - [proof-020 & _] (make-proof "0000000002000000")] + (let [] (testing "can make reservation" ( Date: Sun, 30 Jul 2023 01:24:55 +0200 Subject: [PATCH 03/45] force: Implement new reservation system (wip) --- contracts/force/force.cpp | 204 +++++++++++-------- contracts/force/force.hpp | 67 ++++--- tests/e2e/force.cljs | 406 ++++++++++++++++++++------------------ 3 files changed, 383 insertions(+), 294 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 1d3a0b0..2374caf 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -22,10 +22,13 @@ void force::mkcampaign(vaccount::vaddress owner, content content, eosio::extende [&](auto& c) { c.id = camp_id; + c.tasks_done = 0; + c.active_batch = 0; c.content = content; c.owner = owner; c.reward = reward; c.qualis.emplace(qualis); + c.total_tasks = 0; }); } @@ -119,7 +122,7 @@ void force::cleartasks(uint32_t batch_id, uint32_t campaign_id) { auto batch_itr = batch_tbl.find(batch_pk); eosio::check(batch_itr == batch_tbl.end(), "batch still exists"); - // remove the submissoins in this batch + // remove the submissions in this batch submission_table submission_tbl(_self, _self.value); auto by_batch = submission_tbl.get_index<"batch"_n>(); auto itr_start = by_batch.lower_bound(batch_pk); @@ -163,6 +166,12 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si b.balance -= batch_fee; }); + camp_tbl.modify(camp, + eosio::same_payer, + [&](auto& b) { + b.total_tasks += num_tasks; + }); + if (batch_fee.quantity.amount > 0) { action(permission_level{_self, "xfer"_n}, settings.vaccount_contract, @@ -235,98 +244,131 @@ void force::uassignquali(uint32_t quali_id, uint32_t user_id, eosio::name payer, user_quali_tbl.erase(user_quali); } -void force::require_batchjoin(uint32_t account_id, uint64_t batch_pk, bool try_to_join, name payer) { - batchjoin_table join_tbl(_self, _self.value); - uint128_t pk = (uint128_t{batch_pk} << 64) | (uint64_t{account_id} << 32); - - auto by_accbatch = join_tbl.get_index<"accbatch"_n>(); - auto entry = by_accbatch.find(pk); - bool has_joined = (entry != by_accbatch.end()); - if (!has_joined) { - eosio::check(try_to_join, "batch not joined"); - - batch_table batch_tbl(_self, _self.value); - auto batch = batch_tbl.get(batch_pk, "batch not found"); - campaign_table camp_tbl(_self, _self.value); - auto camp = camp_tbl.get(batch.campaign_id, "campaign not found"); - user_quali_table user_quali_tbl(_self, _self.value); - - auto qualis = camp.qualis.value();; - - for (auto q : qualis) { - uint32_t quali_id = std::get<0>(q); - uint8_t quali_type = std::get<1>(q); - uint64_t user_quali_id = (uint64_t{account_id} << 32) | quali_id; - auto user_quali = user_quali_tbl.find(user_quali_id); - bool has_quali = (user_quali != user_quali_tbl.end()); - if (has_quali) - eosio::check(quali_type == force::Inclusive, "qualification excluded"); - else - eosio::check(quali_type == force::Exclusive, "missing qualification"); - } - - uint64_t join_id = join_tbl.available_primary_key(); - join_tbl.emplace(payer, - [&](auto& j) - { - j.account_id = account_id; - j.batch_id = batch_pk; - j.id = join_id; - }); - } -} - void force::reservetask(uint32_t campaign_id, uint32_t account_id, uint32_t last_task_done, name payer, vaccount::sig sig) { - uint32_t batch_id = 0; - submission_table submission_tbl(_self, _self.value); - batch_table batch_tbl(_self, _self.value); - campaign_table campaign_tbl(_self, _self.value); - - uint64_t batch_pk = (uint64_t{campaign_id} << 32) | batch_id; - require_batchjoin(account_id, batch_pk, true, payer); + reservetask_params params = {6, last_task_done, campaign_id}; + require_vaccount(account_id, pack(params), sig); - auto& batch = batch_tbl.get(batch_pk, "batch not found"); + campaign_table campaign_tbl(_self, _self.value); auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); - eosio::check(batch.tasks_done >= 0 && batch.num_tasks > 0, "batch paused"); - - - // TODO: find task id - uint32_t task_idx = 1; - - reservetask_params params = {6, task_idx, campaign_id}; - require_vaccount(account_id, pack(params), sig); + uint32_t batch_id = campaign.active_batch; + uint64_t batch_pk = (uint64_t{campaign_id} << 32) | batch_id; + batch_table batch_tbl(_self, _self.value); + auto& batch = batch_tbl.get(batch_pk, "no batches available"); + + eosio::check(campaign.tasks_done < batch.start_task_idx + batch.num_tasks, + "no more tasks in campaign"); + + // TODO: check qualifications + + // check if user has a reservation already + uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; + reservation_table reservation_tbl(_self, _self.value); + auto by_acccamp = reservation_tbl.get_index<"acccamp"_n>(); + auto existing_reservation = by_acccamp.find(acccamp_pk); + eosio::check(existing_reservation == by_acccamp.end(), "you already have a reservation"); + + // find the last task idx the user completed in the campaign + acctaskidx_table acctaskidx_tbl(_self, _self.value); + auto user_last_task_check = acctaskidx_tbl.find(acccamp_pk); + + if (user_last_task_check == acctaskidx_tbl.end()) { + acctaskidx_tbl.emplace(payer, + [&](auto& i) + { + i.campaign_id = campaign_id; + i.account_id = account_id; + i.value = 0; + }); + } - // printhex(&data_hash.extract_as_byte_array()[0], 32); + auto& user_last_task = acctaskidx_tbl.get(acccamp_pk); + + eosio::check(campaign.total_tasks > user_last_task.value, + "no more tasks for you"); + + // reserve suitable task idx to the user + uint32_t task_idx = std::max(campaign.tasks_done, user_last_task.value); + + // check if there is an earlier expired tasks to claim instead + auto by_camp = reservation_tbl.get_index<"camp"_n>(); + auto by_camp_itr = by_camp.find(campaign_id); + if (by_camp_itr != by_camp.end() && + past_delay(by_camp_itr->reserved_on, "release_task") && + // only claim reservations that come before our assigned task idx + task_idx >= by_camp_itr->task_idx) { + auto& res = *by_camp_itr; + uint64_t bump_id = reservation_tbl.available_primary_key(); + reservation_tbl.modify(res, + payer, + [&](auto& r) + { + // we bump the ID as this one will not expire for a while + r.id = bump_id; + r.account_id = account_id; + r.reserved_on = time_point_sec(now()); + }); + // NOTE: early return here + return; + } - uint32_t submission_id = submission_tbl.available_primary_key(); + // check how many reps are done for this task + uint64_t repsdone_pk = (uint64_t{campaign_id} << 32) | task_idx; + repsdone_table repsdone_tbl(_self, _self.value); + auto repetitions_done = repsdone_tbl.find(repsdone_pk); - // check if repetitions are not done - // auto by_leaf = submission_tbl.get_index<"leaf"_n>(); - // auto itr_start = by_leaf.lower_bound(data_hash); - // auto itr_end = by_leaf.upper_bound(data_hash); - // uint32_t rep_count = 0; + // check if this task is done + bool has_reps_done_row = !(repetitions_done == repsdone_tbl.end()); + bool task_done = false; + if (batch.repetitions == 1) { + task_done = true; + } else { + if (!has_reps_done_row) { + repsdone_tbl.emplace(payer, + [&](auto& i) + { + i.campaign_id = campaign_id; + i.task_idx = task_idx; + i.value = 1; + }); + } else { + if (repetitions_done->value + 1 == batch.repetitions) + task_done = true; + } + } - // for (; itr_start != itr_end; itr_start++) { - // rep_count++; - // auto& subm = *itr_start; - // eosio::check(subm.account_id != account_id, "account already did task"); - // } - // eosio::check(rep_count < batch.repetitions, "task already completed"); + // update campaign counters if task is done + acctaskidx_tbl.modify(user_last_task, payer, [&](auto& i) { i.value = task_idx + 1; }); + if (task_done) { + if (has_reps_done_row) + repsdone_tbl.erase(repetitions_done); + + bool batch_done = (campaign.tasks_done >= batch.num_tasks); + campaign_tbl.modify(campaign, + eosio::same_payer, + [&](auto& c) { + c.tasks_done += 1; + if (batch_done) { + c.active_batch += 1; + } + }); + } - submission_tbl.emplace(payer, - [&](auto& s) - { - s.id = submission_id; - s.account_id = account_id; - s.batch_id = batch_pk; - s.paid = false; - s.submitted_on = time_point_sec(now()); - }); + uint64_t reservation_id = reservation_tbl.available_primary_key(); + reservation_tbl.emplace(payer, + [&](auto& r) + { + r.id = reservation_id; + r.task_idx = task_idx; + r.account_id = account_id; + r.batch_id = batch_pk; + r.reserved_on = time_point_sec(now()); + r.campaign_id = campaign_id; + }); } void force::payout(uint64_t payment_id, std::optional sig) { @@ -449,8 +491,6 @@ void force::reclaimtask(uint64_t task_id, uint32_t account_id, eosio::check(batch.tasks_done >= 0 && batch.num_tasks > 0, "cannot reclaim task on paused batch."); - require_batchjoin(account_id, sub.batch_id, true, payer); - task_params params = {15, task_id, account_id}; require_vaccount(account_id, pack(params), sig); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 9ad78a5..e1290af 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -195,20 +195,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { if (type_delay == "payout") delay = _config.get().payout_delay_sec; else if (type_delay == "release_task") delay = _config.get().release_task_delay_sec; - return time_point_sec(now()) > (base_time + delay); + return time_point_sec(now()) >= (base_time + delay); } - // Helper. Assumes we already did require_vaccount on account_id - void require_batchjoin(uint32_t account_id, - uint64_t batch_id, - bool try_to_join, - eosio::name payer); - - void require_merkle(std::vector proof, - std::vector position, - eosio::checksum256 root, - eosio::checksum256 data); - struct reservetask_params { uint8_t mark; uint32_t last_task_done; @@ -313,6 +302,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { struct [[eosio::table]] campaign { uint32_t id; + uint32_t tasks_done; + uint32_t total_tasks; + uint32_t active_batch; vaccount::vaddress owner; content content; eosio::extended_asset reward; @@ -320,7 +312,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t primary_key() const { return (uint64_t) id; } - EOSLIB_SERIALIZE(campaign, (id)(owner)(content)(reward)(qualis)) + EOSLIB_SERIALIZE(campaign, (id)(tasks_done)(total_tasks)(active_batch)(owner)(content)(reward)(qualis)) }; struct [[eosio::table]] batch { @@ -332,18 +324,24 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t repetitions; uint32_t tasks_done; uint32_t num_tasks; + uint32_t start_task_idx; eosio::binary_extension reward; uint64_t primary_key() const { return (uint64_t{campaign_id} << 32) | id; } }; - struct [[eosio::table]] batchjoin { - // This id is only necessary for uniqueness of primary key - uint64_t id; + struct [[eosio::table]] acctaskidx { uint32_t account_id; - uint64_t batch_id; - uint64_t primary_key() const { return id; } - uint128_t by_account_batch() const { return (uint128_t{batch_id} << 64) | (uint64_t{account_id} << 32); } + uint32_t campaign_id; + uint32_t value; + uint64_t primary_key() const { return (uint64_t{account_id} << 32) | campaign_id; } + }; + + struct [[eosio::table]] repsdone { + uint32_t campaign_id; + uint32_t task_idx; + uint32_t value; + uint64_t primary_key() const { return (uint64_t{campaign_id} << 32) | task_idx; } }; struct [[eosio::table]] payment { @@ -360,6 +358,24 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t by_account() const { return (uint64_t) account_id; } }; + struct [[eosio::table]] reservation { + // auto incrementing id to ensure task ordering. the id gets + // "bumped" everytime the reservation expires and is refreshed. + uint64_t id; + uint32_t task_idx; + uint32_t account_id; + uint64_t batch_id; + eosio::time_point_sec reserved_on; + uint32_t campaign_id; + + uint64_t primary_key() const { return id; } + // index to check if user has a reservation for a + // campaign. account_id in the front, so can be used as account + // filter + uint64_t by_account_campaign() const { return (uint64_t{account_id} << 32) | campaign_id; } + uint64_t by_camp() const { return campaign_id; } + }; + struct [[eosio::table]] submission { uint64_t id; std::optional account_id; @@ -411,16 +427,17 @@ class [[eosio::contract("force")]] force : public eosio::contract { typedef multi_index<"campaign"_n, campaign> campaign_table; typedef multi_index<"batch"_n, batch> batch_table; + typedef multi_index<"repsdone"_n, repsdone> repsdone_table; + typedef multi_index<"acctaskidx"_n, acctaskidx> acctaskidx_table; + typedef multi_index< - "batchjoin"_n, - batchjoin, - indexed_by<"accbatch"_n, - const_mem_fun>> - batchjoin_table; + "reservation"_n, reservation, + indexed_by<"acccamp"_n, const_mem_fun>, + indexed_by<"camp"_n, const_mem_fun>> + reservation_table; typedef multi_index< "submission"_n, submission, - indexed_by<"leaf"_n, const_mem_fun>, indexed_by<"batch"_n, const_mem_fun>> submission_table; diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 128b777..75284bb 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -122,12 +122,12 @@ ( Date: Sun, 30 Jul 2023 11:41:15 +0200 Subject: [PATCH 04/45] force: Move task expiration time to campaign level it used to be on a global config level --- contracts/force/force.cpp | 52 +++++++++++++++++++++++++-------------- contracts/force/force.hpp | 9 ++++++- tests/e2e/force.cljs | 42 ++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 2374caf..9e97d66 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -9,8 +9,13 @@ void force::init(eosio::name vaccount_contract, uint32_t force_vaccount_id, release_task_delay_sec}, _self); } -void force::mkcampaign(vaccount::vaddress owner, content content, eosio::extended_asset reward, - camp_quali_map qualis, eosio::name payer, vaccount::sig sig) { +void force::mkcampaign(vaccount::vaddress owner, + content content, + uint32_t max_task_time, + eosio::extended_asset reward, + camp_quali_map qualis, + eosio::name payer, + vaccount::sig sig) { campaign_table camp_tbl(_self, _self.value); uint32_t camp_id = camp_tbl.available_primary_key(); // TODO: add owner, reward, and qualis to the params @@ -26,6 +31,7 @@ void force::mkcampaign(vaccount::vaddress owner, content content, eosio::extende c.active_batch = 0; c.content = content; c.owner = owner; + c.max_task_time = max_task_time; c.reward = reward; c.qualis.emplace(qualis); c.total_tasks = 0; @@ -274,9 +280,9 @@ void force::reservetask(uint32_t campaign_id, // find the last task idx the user completed in the campaign acctaskidx_table acctaskidx_tbl(_self, _self.value); - auto user_last_task_check = acctaskidx_tbl.find(acccamp_pk); + auto user_last_task_check = (acctaskidx_tbl.find(acccamp_pk) == acctaskidx_tbl.end()); - if (user_last_task_check == acctaskidx_tbl.end()) { + if (user_last_task_check) { acctaskidx_tbl.emplace(payer, [&](auto& i) { @@ -287,31 +293,39 @@ void force::reservetask(uint32_t campaign_id, } auto& user_last_task = acctaskidx_tbl.get(acccamp_pk); + uint32_t user_next_task_idx = user_last_task_check ? 0 : user_last_task.value + 1; - eosio::check(campaign.total_tasks > user_last_task.value, + eosio::check(!user_last_task_check || campaign.total_tasks > user_last_task.value, "no more tasks for you"); // reserve suitable task idx to the user - uint32_t task_idx = std::max(campaign.tasks_done, user_last_task.value); + uint32_t task_idx = std::max(campaign.tasks_done, user_next_task_idx); - // check if there is an earlier expired tasks to claim instead + // check if there is an earlier expired reservatoin to claim instead auto by_camp = reservation_tbl.get_index<"camp"_n>(); auto by_camp_itr = by_camp.find(campaign_id); if (by_camp_itr != by_camp.end() && - past_delay(by_camp_itr->reserved_on, "release_task") && - // only claim reservations that come before our assigned task idx + has_expired(by_camp_itr->reserved_on, campaign.max_task_time) && + // only claim reservations that come before our assigned task + // idx. if the user were to steal future indexis, bumping + // acctaskidx would mean the users misses out on tasks, and + // omitting so would let him do this repetition twice. task_idx >= by_camp_itr->task_idx) { auto& res = *by_camp_itr; uint64_t bump_id = reservation_tbl.available_primary_key(); - reservation_tbl.modify(res, - payer, - [&](auto& r) - { - // we bump the ID as this one will not expire for a while - r.id = bump_id; - r.account_id = account_id; - r.reserved_on = time_point_sec(now()); - }); + + // we must re-insert the reservation in order to bump the id + reservation_tbl.erase(res); + reservation_tbl.emplace(payer, + [&](auto& r) + { + r.id = bump_id; + r.task_idx = res.task_idx; + r.account_id = account_id; + r.batch_id = batch_pk; + r.reserved_on = time_point_sec(now()); + r.campaign_id = campaign_id; + }); // NOTE: early return here return; } @@ -342,7 +356,7 @@ void force::reservetask(uint32_t campaign_id, } // update campaign counters if task is done - acctaskidx_tbl.modify(user_last_task, payer, [&](auto& i) { i.value = task_idx + 1; }); + acctaskidx_tbl.modify(user_last_task, payer, [&](auto& i) { i.value = task_idx; }); if (task_done) { if (has_reps_done_row) repsdone_tbl.erase(repetitions_done); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index e1290af..8bb7289 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -46,6 +46,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { [[eosio::action]] void mkcampaign(vaccount::vaddress owner, content content, + uint32_t max_task_time, eosio::extended_asset reward, camp_quali_map qualis, eosio::name payer, @@ -190,6 +191,10 @@ class [[eosio::contract("force")]] force : public eosio::contract { } private: + inline bool has_expired(time_point_sec base_time, uint32_t delay) { + return time_point_sec(now()) >= (base_time + delay); + } + inline bool past_delay(time_point_sec base_time, std::string type_delay) { auto delay = NULL; @@ -307,12 +312,14 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t active_batch; vaccount::vaddress owner; content content; + uint32_t max_task_time; eosio::extended_asset reward; eosio::binary_extension> qualis; uint64_t primary_key() const { return (uint64_t) id; } - EOSLIB_SERIALIZE(campaign, (id)(tasks_done)(total_tasks)(active_batch)(owner)(content)(reward)(qualis)) + EOSLIB_SERIALIZE(campaign, (id)(tasks_done)(total_tasks)(active_batch)(owner)(content) + (max_task_time)(reward)(qualis)) }; struct [[eosio::table]] batch { diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 75284bb..37034db 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -253,6 +253,7 @@ ( Date: Mon, 31 Jul 2023 00:20:17 +0200 Subject: [PATCH 05/45] force: Implement new task submission system --- contracts/force/force.cpp | 73 ++++++++++++---- contracts/force/force.hpp | 17 ++-- tests/e2e/force.cljs | 173 ++++++++++++++++++++++++++------------ 3 files changed, 186 insertions(+), 77 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 9e97d66..1109ef4 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -170,6 +170,11 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si [&](auto& b) { b.num_tasks = num_tasks; b.balance -= batch_fee; + // if this batch becomes the active batch of the + // campaign track it starting index + if (camp.active_batch == b.id) { + b.start_task_idx = camp.tasks_done; + } }); camp_tbl.modify(camp, @@ -361,15 +366,31 @@ void force::reservetask(uint32_t campaign_id, if (has_reps_done_row) repsdone_tbl.erase(repetitions_done); - bool batch_done = (campaign.tasks_done >= batch.num_tasks); + bool batch_done = ((campaign.tasks_done + 1) >= (batch.start_task_idx + batch.num_tasks)); campaign_tbl.modify(campaign, eosio::same_payer, - [&](auto& c) { + [&](auto& c) + { c.tasks_done += 1; if (batch_done) { c.active_batch += 1; } }); + + // if we roll over to a new batch, we must track at which task index it starts + if (batch_done) { + uint64_t next_batch_pk = (uint64_t{campaign_id} << 32) | (batch_id + 1); + auto next_batch = batch_tbl.find(next_batch_pk); + + if (next_batch != batch_tbl.end()) { + batch_tbl.modify(*next_batch, + eosio::same_payer, + [&](auto &b) + { + b.start_task_idx = campaign.tasks_done; + }); + } + } } uint64_t reservation_id = reservation_tbl.available_primary_key(); @@ -378,7 +399,7 @@ void force::reservetask(uint32_t campaign_id, { r.id = reservation_id; r.task_idx = task_idx; - r.account_id = account_id; + r.account_id.emplace(account_id); r.batch_id = batch_pk; r.reserved_on = time_point_sec(now()); r.campaign_id = campaign_id; @@ -411,31 +432,47 @@ void force::payout(uint64_t payment_id, std::optional sig) { .send(); } -void force::submittask(uint64_t submission_id, std::string data, uint32_t account_id, - uint64_t batch_id, name payer, vaccount::sig sig) { +void force::submittask(uint32_t campaign_id, uint32_t task_idx, + std::string data, uint32_t account_id, + name payer, vaccount::sig sig) { + uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; + reservation_table reservation_tbl(_self, _self.value); + auto by_acccamp = reservation_tbl.get_index<"acccamp"_n>(); + auto res = by_acccamp.find(acccamp_pk); + + eosio::check(res != by_acccamp.end(), "already submitted or not reserved by you"); + eosio::check(res->account_id.has_value(), "not reserved"); + eosio::check(res->account_id.value() == account_id, "different account"); + eosio::check(res->task_idx == task_idx, "wrong task index"); + submission_table submission_tbl(_self, _self.value); payment_table payment_tbl(_self, _self.value); batch_table batch_tbl(_self, _self.value); campaign_table campaign_tbl(_self, _self.value); - auto& sub = submission_tbl.get(submission_id, "submission not found"); - eosio::check(sub.account_id.has_value(), "task not reserved"); - eosio::check(sub.account_id == account_id, "different account"); - eosio::check(!sub.data.has_value(), "already submitted"); - submission_tbl.modify(sub, payer, [&](auto& s) { s.data.emplace(data); }); + reservation_tbl.erase(*res); + submission_tbl.emplace(payer, + [&](auto& s) + { + s.id = res->id; + s.campaign_id = campaign_id; + s.task_idx = task_idx; + s.account_id.emplace(account_id); + s.data.emplace(data); + s.batch_id = res->batch_id; + s.paid = false; + s.submitted_on = time_point_sec(now()); + }); - auto& batch = batch_tbl.get(sub.batch_id, "batch not found"); - auto& camp = campaign_tbl.get(batch.campaign_id); + auto& batch = batch_tbl.get(res->batch_id); - batch_tbl.modify(batch, eosio::same_payer, [&](auto& b) { b.tasks_done++; }); + submittask_params params = {5, campaign_id, task_idx, data}; + require_vaccount(account_id, pack(params), sig); if (batch.reward.value().quantity.amount > 0) { - submittask_params params = {5, submission_id, data}; - require_vaccount(account_id, pack(params), sig); - uint64_t payment_id = payment_tbl.available_primary_key(); - uint128_t payment_sk = (uint128_t{batch_id} << 64) | (uint64_t{account_id} << 32); + uint128_t payment_sk = (uint128_t{res->batch_id} << 64) | (uint64_t{account_id} << 32); auto payment_idx = payment_tbl.get_index<"accbatch"_n>(); auto payment = payment_idx.find(payment_sk); @@ -445,7 +482,7 @@ void force::submittask(uint64_t submission_id, std::string data, uint32_t accoun { p.id = payment_id; p.account_id = account_id; - p.batch_id = batch_id; + p.batch_id = res->batch_id; p.pending = batch.reward.value(); p.last_submission_time = time_point_sec(now()); }); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 8bb7289..583d795 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -102,10 +102,10 @@ class [[eosio::contract("force")]] force : public eosio::contract { vaccount::sig sig); [[eosio::action]] - void submittask(uint64_t task_id, + void submittask(uint32_t campaign_id, + uint32_t task_idx, std::string data, uint32_t account_id, - uint64_t batch_id, eosio::name payer, vaccount::sig sig); @@ -212,9 +212,10 @@ class [[eosio::contract("force")]] force : public eosio::contract { struct submittask_params { uint8_t mark; - uint64_t submission_id; + uint32_t campaign_id; + uint32_t task_idx; std::string data; - EOSLIB_SERIALIZE(submittask_params, (mark)(submission_id)(data)); + EOSLIB_SERIALIZE(submittask_params, (mark)(campaign_id)(task_idx)(data)); }; struct mkcampaign_params { @@ -370,7 +371,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { // "bumped" everytime the reservation expires and is refreshed. uint64_t id; uint32_t task_idx; - uint32_t account_id; + std::optional account_id; uint64_t batch_id; eosio::time_point_sec reserved_on; uint32_t campaign_id; @@ -379,12 +380,14 @@ class [[eosio::contract("force")]] force : public eosio::contract { // index to check if user has a reservation for a // campaign. account_id in the front, so can be used as account // filter - uint64_t by_account_campaign() const { return (uint64_t{account_id} << 32) | campaign_id; } + uint64_t by_account_campaign() const { return (uint64_t{account_id.value()} << 32) | campaign_id; } uint64_t by_camp() const { return campaign_id; } }; struct [[eosio::table]] submission { uint64_t id; + uint32_t campaign_id; + uint32_t task_idx; std::optional account_id; std::optional content; uint64_t batch_id; @@ -395,7 +398,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t primary_key() const { return id; } uint64_t by_batch() const { return batch_id; } - EOSLIB_SERIALIZE(submission, (id)(account_id)(content)(batch_id) + EOSLIB_SERIALIZE(submission, (id)(campaign_id)(task_idx)(account_id)(content)(batch_id) (data)(paid)(submitted_on)) }; diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 37034db..b7b0dfd 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -55,10 +55,10 @@ (println "acc-2 " acc-2) (def accs [["address" (vacc/pub->addr vacc/keypair-pub)] - ["name" acc-3] - ["name" acc-2] - ["name" acc-4] - ["name" acc-5]]) + ["name" acc-3] ;; vaccount id = 1 + ["name" acc-2] ;; + ["name" acc-4] ;; vaccount id = 3 + ["name" acc-5]]) ;; vaccount-id = 4 (def force-vacc-id 5) @@ -396,6 +396,15 @@ :sig nil})) (hex (.digest (.update (.hash ec) data)))) -(async-deftest closebatch +#_(async-deftest closebatch (let [params (pack-closebatch-params (get-composite-key 0 5))] - (testing "campaign owner can pause batch"x - (is (= ( Date: Mon, 31 Jul 2023 00:38:53 +0200 Subject: [PATCH 06/45] force: Remove `last_task_done` argument form `reservetask` it serves as a nonce for transactions with a raw signature, but there is not need to have it as an action paramter --- contracts/force/force.cpp | 8 ++++---- contracts/force/force.hpp | 1 - tests/e2e/force.cljs | 29 +++++++---------------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 1109ef4..ba3fbd0 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -257,12 +257,8 @@ void force::uassignquali(uint32_t quali_id, uint32_t user_id, eosio::name payer, void force::reservetask(uint32_t campaign_id, uint32_t account_id, - uint32_t last_task_done, name payer, vaccount::sig sig) { - reservetask_params params = {6, last_task_done, campaign_id}; - require_vaccount(account_id, pack(params), sig); - campaign_table campaign_tbl(_self, _self.value); auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); @@ -300,6 +296,10 @@ void force::reservetask(uint32_t campaign_id, auto& user_last_task = acctaskidx_tbl.get(acccamp_pk); uint32_t user_next_task_idx = user_last_task_check ? 0 : user_last_task.value + 1; + reservetask_params params = {6, user_next_task_idx, campaign_id}; + require_vaccount(account_id, pack(params), sig); + + eosio::check(!user_last_task_check || campaign.total_tasks > user_last_task.value, "no more tasks for you"); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 583d795..06f6b4f 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -97,7 +97,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { [[eosio::action]] void reservetask(uint32_t campaign_id, uint32_t account_id, - uint32_t last_task_done, eosio::name payer, vaccount::sig sig); diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index b7b0dfd..0377ccc 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -675,7 +675,6 @@ ( Date: Thu, 3 Aug 2023 23:26:23 +0200 Subject: [PATCH 07/45] force: Add campaign data structures for nft qualifications --- contracts/force/force.cpp | 19 ++++++++++++------- contracts/force/force.hpp | 32 +++++++++++++++++++++++++------- tests/e2e/force.cljs | 13 +++++++------ 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index ba3fbd0..36b502b 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -13,7 +13,7 @@ void force::mkcampaign(vaccount::vaddress owner, content content, uint32_t max_task_time, eosio::extended_asset reward, - camp_quali_map qualis, + std::vector qualis, eosio::name payer, vaccount::sig sig) { campaign_table camp_tbl(_self, _self.value); @@ -33,14 +33,18 @@ void force::mkcampaign(vaccount::vaddress owner, c.owner = owner; c.max_task_time = max_task_time; c.reward = reward; - c.qualis.emplace(qualis); + c.qualis = qualis; c.total_tasks = 0; }); } -void force::editcampaign(uint32_t campaign_id, vaccount::vaddress owner, content content, - eosio::extended_asset reward, camp_quali_map qualis, - eosio::name payer, vaccount::sig sig) { +void force::editcampaign(uint32_t campaign_id, + vaccount::vaddress owner, + content content, + eosio::extended_asset reward, + std::vector qualis, + eosio::name payer, + vaccount::sig sig) { campaign_table camp_tbl(_self, _self.value); auto& camp = camp_tbl.get(campaign_id, "campaign does not exist"); @@ -54,7 +58,7 @@ void force::editcampaign(uint32_t campaign_id, vaccount::vaddress owner, content { c.content = content; c.reward = reward; - c.qualis.emplace(qualis); + c.qualis = qualis; }); } @@ -258,7 +262,8 @@ void force::uassignquali(uint32_t quali_id, uint32_t user_id, eosio::name payer, void force::reservetask(uint32_t campaign_id, uint32_t account_id, name payer, - vaccount::sig sig) { + vaccount::sig sig, + std::optional> quali_assets) { campaign_table campaign_tbl(_self, _self.value); auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 06f6b4f..1b8d8bb 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -8,6 +8,7 @@ #include #include #include "../vaccount/vaccount-shared.hpp" +#include "../dao/atomicassets-interface.hpp" using namespace eosio; @@ -27,11 +28,27 @@ class [[eosio::contract("force")]] force : public eosio::contract { typedef std::tuple content; - enum QualiType { - Inclusive = 0, - Exclusive = 1 + // assets and templates are uint32_t, while collections and schemas are eosio::name + typedef std::variant QUALI_ATOMIC_ADDRESS; + + // this allocates some data for in the future + struct QualiDataFilter { + uint8_t attr_id; + uint8_t filter_type; + std::vector data; }; + struct Quali { + uint8_t type; + QUALI_ATOMIC_ADDRESS address; + std::optional data_filter; + }; + + enum QualiType { + Collection = 0, + Template = 1, + Asset = 2 + }; force(eosio::name receiver, eosio::name code, eosio::datastream ds) : eosio::contract(receiver, code, ds), _config(_self, _self.value), _settings(_self, _self.value) @@ -48,7 +65,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { content content, uint32_t max_task_time, eosio::extended_asset reward, - camp_quali_map qualis, + std::vector qualis, eosio::name payer, vaccount::sig sig); @@ -57,7 +74,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { vaccount::vaddress owner, content content, eosio::extended_asset reward, - camp_quali_map qualis, + std::vector qualis, eosio::name payer, vaccount::sig sig); @@ -98,7 +115,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { void reservetask(uint32_t campaign_id, uint32_t account_id, eosio::name payer, - vaccount::sig sig); + vaccount::sig sig, + std::optional> quali_assets); [[eosio::action]] void submittask(uint32_t campaign_id, @@ -314,7 +332,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { content content; uint32_t max_task_time; eosio::extended_asset reward; - eosio::binary_extension> qualis; + std::vector qualis; uint64_t primary_key() const { return (uint64_t) id; } diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 0377ccc..a3585f9 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -310,7 +310,7 @@ :owner ["name" acc-2] :content {:field_0 0 :field_1 vacc/hash160-1} :reward {:quantity "3.0000 EFX" :contract token-acc} - :qualis [{"first" 0 "second" 0}] + :qualis [{:type 0 :address ["uint32" 123] :data_filter nil}] :payer acc-2 :sig nil}))) @@ -383,7 +383,6 @@ :task_merkle_root merkle-root :repetitions 1 :payer acc-2 - :qualis [{"first" 1 "second" 0}] :sig nil})) ( Date: Wed, 9 Aug 2023 09:30:10 +0200 Subject: [PATCH 08/45] force: Add AtomicAsset qualifications --- contracts/force/force.cpp | 60 +++++++++++++++++++++++--- contracts/force/force.hpp | 14 +++--- contracts/vaccount/vaccount-shared.hpp | 21 +++++++++ 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 36b502b..f668c05 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -261,9 +261,9 @@ void force::uassignquali(uint32_t quali_id, uint32_t user_id, eosio::name payer, void force::reservetask(uint32_t campaign_id, uint32_t account_id, + std::optional> quali_assets, name payer, - vaccount::sig sig, - std::optional> quali_assets) { + vaccount::sig sig) { campaign_table campaign_tbl(_self, _self.value); auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); @@ -275,7 +275,55 @@ void force::reservetask(uint32_t campaign_id, eosio::check(campaign.tasks_done < batch.start_task_idx + batch.num_tasks, "no more tasks in campaign"); - // TODO: check qualifications + // check qualifications + settings settings = get_settings(); + auto vacc = vaccount::get_vaccount(settings.vaccount_contract, account_id); + bool is_eos = vaccount::is_eos(vacc->address); + eosio::name asset_owner = is_eos ? vaccount::get_name(vacc->address) : _self; + auto acc_assets_tbl = atomicassets::get_assets(asset_owner); + auto force_assets_tbl = atomicassets::get_assets(_self); + + if (campaign.qualis.size() > 0) + eosio::check(quali_assets.has_value() && quali_assets.value().size() == campaign.qualis.size(), + "wrong number of quali_assets"); + + for (int i = 0; i < campaign.qualis.size(); i++) { + auto quali = campaign.qualis[i]; + uint64_t asset_id = quali_assets.value()[i]; + atomicassets::assets_s asset; + + // check right asset owner + auto acc_asset = acc_assets_tbl.find(asset_id); + auto force_asset = force_assets_tbl.find(asset_id); + bool asset_is_eos = acc_asset != acc_assets_tbl.end(); + eosio::check(asset_is_eos || force_asset != force_assets_tbl.end(), + "asset now owned by you"); + asset = asset_is_eos ? *acc_asset : *force_asset; + + // if this is a vaccount, we must additionaly check the asset is owned by it + if (!asset_is_eos) { + auto schema_tbl = atomicassets::get_schemas(asset.collection_name); + auto schema = schema_tbl.get(asset.schema_name.value, "asset not owned by vaccount"); + auto data_map = atomicdata::deserialize(asset.mutable_serialized_data, schema.format); + auto asset_vacc_owner = std::get(data_map["vaccount"]); + eosio::check(asset_vacc_owner == account_id, "asset not owned by vaccount"); + } + + switch (quali.type) { + case Collection: + eosio::check(asset.collection_name == std::get(quali.address), + "wrong collection"); + break; + case Template: + eosio::check(asset.template_id == std::get(quali.address), + "wrong template"); + break; + case Asset: + eosio::check(asset.asset_id == std::get(quali.address), + "wrong asset"); + break; + } + } // check if user has a reservation already uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; @@ -437,8 +485,10 @@ void force::payout(uint64_t payment_id, std::optional sig) { .send(); } -void force::submittask(uint32_t campaign_id, uint32_t task_idx, - std::string data, uint32_t account_id, +void force::submittask(uint32_t campaign_id, + uint32_t task_idx, + std::string data, + uint32_t account_id, name payer, vaccount::sig sig) { uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; reservation_table reservation_tbl(_self, _self.value); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 1b8d8bb..fc56424 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -9,6 +9,7 @@ #include #include "../vaccount/vaccount-shared.hpp" #include "../dao/atomicassets-interface.hpp" +#include "atomicdata.hpp" using namespace eosio; @@ -28,8 +29,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { typedef std::tuple content; - // assets and templates are uint32_t, while collections and schemas are eosio::name - typedef std::variant QUALI_ATOMIC_ADDRESS; + // assets is uint64_t, templates are uint32_t, collections and schemas are eosio::name + typedef std::variant QUALI_ATOMIC_ADDRESS; // this allocates some data for in the future struct QualiDataFilter { @@ -114,9 +115,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { [[eosio::action]] void reservetask(uint32_t campaign_id, uint32_t account_id, + std::optional> quali_assets, eosio::name payer, - vaccount::sig sig, - std::optional> quali_assets); + vaccount::sig sig); [[eosio::action]] void submittask(uint32_t campaign_id, @@ -245,6 +246,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint8_t mark; uint32_t campaign_id; content content; + std::vector qualis; EOSLIB_SERIALIZE(editcampaign_params, (mark)(campaign_id)(content)); }; @@ -399,6 +401,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { // filter uint64_t by_account_campaign() const { return (uint64_t{account_id.value()} << 32) | campaign_id; } uint64_t by_camp() const { return campaign_id; } + uint64_t by_account() const { return account_id.value(); } }; struct [[eosio::table]] submission { @@ -460,7 +463,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { typedef multi_index< "reservation"_n, reservation, indexed_by<"acccamp"_n, const_mem_fun>, - indexed_by<"camp"_n, const_mem_fun>> + indexed_by<"camp"_n, const_mem_fun>, + indexed_by<"acc"_n, const_mem_fun>> reservation_table; typedef multi_index< diff --git a/contracts/vaccount/vaccount-shared.hpp b/contracts/vaccount/vaccount-shared.hpp index 31d7e1f..ccc15a2 100644 --- a/contracts/vaccount/vaccount-shared.hpp +++ b/contracts/vaccount/vaccount-shared.hpp @@ -65,4 +65,25 @@ namespace vaccount { "account"_n, account, indexed_by<"token"_n, const_mem_fun>> account_table; + + std::optional get_vaccount(eosio::name vacc_account, uint32_t id) { + account_table acc_tbl(vacc_account, vacc_account.value); + auto acc = acc_tbl.find(id); + if (acc != acc_tbl.end()) + return { *acc }; + return std::nullopt; + } + + bool is_eos(vaddress addr) { + auto addr_type = addr.index(); + return addr_type == 1; + } + + eosio::name get_name(vaddress addr) { + return std::get(addr); + } + + address get_addresss(vaddress addr) { + return std::get
(addr); + } } From 7697adb3ffc922576abfe7a590e1a85dd93ce9a8 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 9 Aug 2023 15:26:10 +0200 Subject: [PATCH 09/45] force: Add e2e tests for AtomicAsset NFTs --- .gitignore | 2 + contracts/force/atomicdata.hpp | 519 ++++++++++++++++ contracts/force/base58.hpp | 129 ++++ contracts/force/force.cpp | 2 +- tests/atomicassets.abi | 1006 ++++++++++++++++++++++++++++++++ tests/atomicassets.wasm | Bin 0 -> 244822 bytes tests/e2e/force.cljs | 87 ++- 7 files changed, 1736 insertions(+), 9 deletions(-) create mode 100644 contracts/force/atomicdata.hpp create mode 100644 contracts/force/base58.hpp create mode 100644 tests/atomicassets.abi create mode 100644 tests/atomicassets.wasm diff --git a/.gitignore b/.gitignore index fc3b44e..39f0dad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ build *.abi *.wasm +!tests/*.wasm +!tests/*.abi node_modules .idea/ .vscode/ diff --git a/contracts/force/atomicdata.hpp b/contracts/force/atomicdata.hpp new file mode 100644 index 0000000..0a5bb36 --- /dev/null +++ b/contracts/force/atomicdata.hpp @@ -0,0 +1,519 @@ +#pragma once + +#include +#include "base58.hpp" + +using namespace eosio; +using namespace std; + +namespace atomicdata { + + //Custom vector types need to be defined because otherwise a bug in the ABI serialization + //would cause the ABI to be invalid + typedef std::vector INT8_VEC; + typedef std::vector INT16_VEC; + typedef std::vector INT32_VEC; + typedef std::vector INT64_VEC; + typedef std::vector UINT8_VEC; + typedef std::vector UINT16_VEC; + typedef std::vector UINT32_VEC; + typedef std::vector UINT64_VEC; + typedef std::vector FLOAT_VEC; + typedef std::vector DOUBLE_VEC; + typedef std::vector STRING_VEC; + + typedef std::variant <\ + int8_t, int16_t, int32_t, int64_t, \ + uint8_t, uint16_t, uint32_t, uint64_t, \ + float, double, std::string, \ + atomicdata::INT8_VEC, atomicdata::INT16_VEC, atomicdata::INT32_VEC, atomicdata::INT64_VEC, \ + atomicdata::UINT8_VEC, atomicdata::UINT16_VEC, atomicdata::UINT32_VEC, atomicdata::UINT64_VEC, \ + atomicdata::FLOAT_VEC, atomicdata::DOUBLE_VEC, atomicdata::STRING_VEC> ATOMIC_ATTRIBUTE; + + typedef std::map ATTRIBUTE_MAP; + + struct FORMAT { + std::string name; + std::string type; + }; + + static constexpr uint64_t RESERVED = 4; + + + vector toVarintBytes(uint64_t number, uint64_t original_bytes = 8) { + if (original_bytes < 8) { + uint64_t bitmask = ((uint64_t) 1 << original_bytes * 8) - 1; + number &= bitmask; + } + + vector bytes = {}; + while (number >= 128) { + // sets msb, stores remainder in lower bits + bytes.push_back((uint8_t)(128 + number % 128)); + number /= 128; + } + bytes.push_back((uint8_t) number); + + return bytes; + } + + uint64_t unsignedFromVarintBytes(vector ::iterator &itr) { + uint64_t number = 0; + uint64_t multiplier = 1; + + while (*itr >= 128) { + number += (((uint64_t) * itr) - 128) * multiplier; + itr++; + multiplier *= 128; + } + number += ((uint64_t) * itr) * multiplier; + itr++; + + return number; + } + + //It is expected that the number is smaller than 2^byte_amount + vector toIntBytes(uint64_t number, uint64_t byte_amount) { + vector bytes = {}; + for (uint64_t i = 0; i < byte_amount; i++) { + bytes.push_back((uint8_t) number % 256); + number /= 256; + } + return bytes; + } + + uint64_t unsignedFromIntBytes(vector ::iterator &itr, uint64_t original_bytes = 8) { + uint64_t number = 0; + uint64_t multiplier = 1; + + for (uint64_t i = 0; i < original_bytes; i++) { + number += ((uint64_t) * itr) * multiplier; + multiplier *= 256; + itr++; + } + + return number; + } + + + uint64_t zigzagEncode(int64_t value) { + if (value < 0) { + return (uint64_t)(-1 * (value + 1)) * 2 + 1; + } else { + return (uint64_t) value * 2; + } + } + + int64_t zigzagDecode(uint64_t value) { + if (value % 2 == 0) { + return (int64_t)(value / 2); + } else { + return (int64_t)(value / 2) * -1 - 1; + } + } + + + vector serialize_attribute(const string &type, const ATOMIC_ATTRIBUTE &attr) { + if (type.find("[]", type.length() - 2) == type.length() - 2) { + //Type is an array + string base_type = type.substr(0, type.length() - 2); + + if (std::holds_alternative (attr)) { + INT8_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + INT16_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + INT32_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + INT64_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + UINT8_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + UINT16_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + UINT32_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + UINT64_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + FLOAT_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + DOUBLE_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } else if (std::holds_alternative (attr)) { + STRING_VEC vec = std::get (attr); + vector serialized_data = toVarintBytes(vec.size()); + for (auto child : vec) { + ATOMIC_ATTRIBUTE child_attr = child; + vector serialized_element = serialize_attribute(base_type, child_attr); + serialized_data.insert(serialized_data.end(), serialized_element.begin(), serialized_element.end()); + } + return serialized_data; + + } + } + + if (type == "int8") { + check(std::holds_alternative (attr), "Expected a int8, but got something else"); + return toVarintBytes(zigzagEncode(std::get (attr)), 1); + } else if (type == "int16") { + check(std::holds_alternative (attr), "Expected a int16, but got something else"); + return toVarintBytes(zigzagEncode(std::get (attr)), 2); + } else if (type == "int32") { + check(std::holds_alternative (attr), "Expected a int32, but got something else"); + return toVarintBytes(zigzagEncode(std::get (attr)), 4); + } else if (type == "int64") { + check(std::holds_alternative (attr), "Expected a int64, but got something else"); + return toVarintBytes(zigzagEncode(std::get (attr)), 8); + + } else if (type == "uint8") { + check(std::holds_alternative (attr), "Expected a uint8, but got something else"); + return toVarintBytes(std::get (attr), 1); + } else if (type == "uint16") { + check(std::holds_alternative (attr), "Expected a uint16, but got something else"); + return toVarintBytes(std::get (attr), 2); + } else if (type == "uint32") { + check(std::holds_alternative (attr), "Expected a uint32, but got something else"); + return toVarintBytes(std::get (attr), 4); + } else if (type == "uint64") { + check(std::holds_alternative (attr), "Expected a uint64, but got something else"); + return toVarintBytes(std::get (attr), 8); + + } else if (type == "fixed8" || type == "byte") { + check(std::holds_alternative (attr), "Expected a uint8 (fixed8 / byte), but got something else"); + return toIntBytes(std::get (attr), 1); + } else if (type == "fixed16") { + check(std::holds_alternative (attr), "Expected a uint16 (fixed16), but got something else"); + return toIntBytes(std::get (attr), 2); + } else if (type == "fixed32") { + check(std::holds_alternative (attr), "Expected a uint32 (fixed32), but got something else"); + return toIntBytes(std::get (attr), 4); + } else if (type == "fixed64") { + check(std::holds_alternative (attr), "Expected a uint64 (fixed64), but got something else"); + return toIntBytes(std::get (attr), 8); + + } else if (type == "float") { + check(std::holds_alternative (attr), "Expected a float, but got something else"); + float float_value = std::get (attr); + auto *byte_value = reinterpret_cast(&float_value); + vector serialized_data = {}; + serialized_data.reserve(4); + for (int i = 0; i < 4; i++) { + serialized_data.push_back(*(byte_value + i)); + } + return serialized_data; + + } else if (type == "double") { + check(std::holds_alternative (attr), "Expected a double, but got something else"); + double float_value = std::get (attr); + auto *byte_value = reinterpret_cast(&float_value); + vector serialized_data = {}; + serialized_data.reserve(8); + for (int i = 0; i < 8; i++) { + serialized_data.push_back(*(byte_value + i)); + } + return serialized_data; + + } else if (type == "string" || type == "image") { + check(std::holds_alternative (attr), "Expected a string, but got something else"); + string text = std::get (attr); + vector serialized_data(text.begin(), text.end()); + + vector length_bytes = toVarintBytes(text.length()); + serialized_data.insert(serialized_data.begin(), length_bytes.begin(), length_bytes.end()); + return serialized_data; + + } else if (type == "ipfs") { + check(std::holds_alternative (attr), "Expected a string (ipfs), but got something else"); + vector result = {}; + check(DecodeBase58(std::get (attr), result), + "Error when decoding IPFS string"); + vector length_bytes = toVarintBytes(result.size()); + result.insert(result.begin(), length_bytes.begin(), length_bytes.end()); + return result; + + } else if (type == "bool") { + check(std::holds_alternative (attr), + "Expected a bool (needs to be provided as uint8_t because of C++ restrictions), but got something else"); + uint8_t value = std::get (attr); + check(value == 0 || value == 1, + "Bools need to be provided as an uin8_t that is either 0 or 1"); + return {value}; + + } else { + check(false, "No type could be matched - " + type); + return {}; //This point can never be reached because the check above will always throw. + //Just to silence the compiler warning + } + } + + + ATOMIC_ATTRIBUTE deserialize_attribute(const string &type, vector ::iterator &itr) { + if (type.find("[]", type.length() - 2) == type.length() - 2) { + //Type is an array + uint64_t array_length = unsignedFromVarintBytes(itr); + string base_type = type.substr(0, type.length() - 2); + + if (type == "int8[]") { + INT8_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + } else if (type == "int16[]") { + INT16_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + } else if (type == "int32[]") { + INT32_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + } else if (type == "int64[]") { + INT64_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + + } else if (type == "uint8[]" || type == "fixed8[]" || type == "bool[]") { + UINT8_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + } else if (type == "uint16[]" || type == "fixed16[]") { + UINT16_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + } else if (type == "uint32[]" || type == "fixed32[]") { + UINT32_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + } else if (type == "uint64[]" || type == "fixed64[]") { + UINT64_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + + } else if (type == "float[]") { + FLOAT_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + + } else if (type == "double[]") { + DOUBLE_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + + } else if (type == "string[]" || type == "image[]") { + STRING_VEC vec = {}; + for (uint64_t i = 0; i < array_length; i++) { + vec.push_back(std::get (deserialize_attribute(base_type, itr))); + } + return vec; + + } + } + + if (type == "int8") { + return (int8_t) zigzagDecode(unsignedFromVarintBytes(itr)); + } else if (type == "int16") { + return (int16_t) zigzagDecode(unsignedFromVarintBytes(itr)); + } else if (type == "int32") { + return (int32_t) zigzagDecode(unsignedFromVarintBytes(itr)); + } else if (type == "int64") { + return (int64_t) zigzagDecode(unsignedFromVarintBytes(itr)); + + } else if (type == "uint8") { + return (uint8_t) unsignedFromVarintBytes(itr); + } else if (type == "uint16") { + return (uint16_t) unsignedFromVarintBytes(itr); + } else if (type == "uint32") { + return (uint32_t) unsignedFromVarintBytes(itr); + } else if (type == "uint64") { + return (uint64_t) unsignedFromVarintBytes(itr); + + } else if (type == "fixed8") { + return (uint8_t) unsignedFromIntBytes(itr, 1); + } else if (type == "fixed16") { + return (uint16_t) unsignedFromIntBytes(itr, 2); + } else if (type == "fixed32") { + return (uint32_t) unsignedFromIntBytes(itr, 4); + } else if (type == "fixed64") { + return (uint64_t) unsignedFromIntBytes(itr, 8); + + } else if (type == "float") { + uint8_t array_repr[4]; + for (uint8_t &i : array_repr) { + i = *itr; + itr++; + } + auto *val = reinterpret_cast(&array_repr); + return *val; + + } else if (type == "double") { + uint8_t array_repr[8]; + for (uint8_t &i : array_repr) { + i = *itr; + itr++; + } + auto *val = reinterpret_cast(&array_repr); + return *val; + + } else if (type == "string" || type == "image") { + uint64_t string_length = unsignedFromVarintBytes(itr); + string text(itr, itr + string_length); + + itr += string_length; + return text; + + } else if (type == "ipfs") { + uint64_t array_length = unsignedFromVarintBytes(itr); + vector byte_array = {}; + byte_array.insert(byte_array.begin(), itr, itr + array_length); + + itr += array_length; + return EncodeBase58(byte_array); + + } else if (type == "bool" || type == "byte") { + uint8_t next_byte = *itr; + itr++; + return next_byte; + + } else { + check(false, "No type could be matched - " + type); + return ""; //This point can never be reached because the check above will always throw. + //Just to silence the compiler warning + } + } + + + vector serialize(ATTRIBUTE_MAP attr_map, const vector &format_lines) { + uint64_t number = 0; + vector serialized_data = {}; + for (atomicassets::FORMAT line : format_lines) { + auto attribute_itr = attr_map.find(line.name); + if (attribute_itr != attr_map.end()) { + const vector &identifier = toVarintBytes(number + RESERVED); + serialized_data.insert(serialized_data.end(), identifier.begin(), identifier.end()); + + const vector &child_data = serialize_attribute(line.type, attribute_itr->second); + serialized_data.insert(serialized_data.end(), child_data.begin(), child_data.end()); + + attr_map.erase(attribute_itr); + } + number++; + } + if (attr_map.begin() != attr_map.end()) { + check(false, + "The following attribute could not be serialized, because it is not specified in the provided format: " + + attr_map.begin()->first); + } + return serialized_data; + } + + + ATTRIBUTE_MAP deserialize(const vector &data, const vector &format_lines) { + ATTRIBUTE_MAP attr_map = {}; + + auto itr = data.begin(); + while (itr != data.end()) { + uint64_t identifier = unsignedFromVarintBytes(itr); + atomicassets::FORMAT format = format_lines.at(identifier - RESERVED); + attr_map[format.name] = deserialize_attribute(format.type, itr); + } + + return attr_map; + } +} diff --git a/contracts/force/base58.hpp b/contracts/force/base58.hpp new file mode 100644 index 0000000..61fa552 --- /dev/null +++ b/contracts/force/base58.hpp @@ -0,0 +1,129 @@ +// Copyright (c) 2014-2019 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//(Slightly modified for the needs of our eosio contract) + +#include +#include + +/** All alphanumeric characters except for "0", "I", "O", and "l" */ +static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; +static const int8_t mapBase58[256] = { + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8,-1,-1,-1,-1,-1,-1, + -1, 9,10,11,12,13,14,15, 16,-1,17,18,19,20,21,-1, + 22,23,24,25,26,27,28,29, 30,31,32,-1,-1,-1,-1,-1, + -1,33,34,35,36,37,38,39, 40,41,42,43,-1,44,45,46, + 47,48,49,50,51,52,53,54, 55,56,57,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, +}; + + +std::string EncodeBase58(const unsigned char* pbegin, const unsigned char* pend) +{ + // Skip & count leading zeroes. + int zeroes = 0; + int length = 0; + while (pbegin != pend && *pbegin == 0) { + pbegin++; + zeroes++; + } + // Allocate enough space in big-endian base58 representation. + int size = (pend - pbegin) * 138 / 100 + 1; // log(256) / log(58), rounded up. + std::vector b58(size); + // Process the bytes. + while (pbegin != pend) { + int carry = *pbegin; + int i = 0; + // Apply "b58 = b58 * 256 + ch". + for (std::vector::reverse_iterator it = b58.rbegin(); (carry != 0 || i < length) && (it != b58.rend()); it++, i++) { + carry += 256 * (*it); + *it = carry % 58; + carry /= 58; + } + + assert(carry == 0); + length = i; + pbegin++; + } + // Skip leading zeroes in base58 result. + std::vector::iterator it = b58.begin() + (size - length); + while (it != b58.end() && *it == 0) + it++; + // Translate the result into a string. + std::string str; + str.reserve(zeroes + (b58.end() - it)); + str.assign(zeroes, '1'); + while (it != b58.end()) + str += pszBase58[*(it++)]; + return str; +} + +std::string EncodeBase58(const std::vector& vch) +{ + return EncodeBase58(vch.data(), vch.data() + vch.size()); +} + + +//Removed the max return length. +bool DecodeBase58(const char* psz, std::vector& vch) +{ + // Skip leading spaces. + while (*psz && isspace(*psz)) + psz++; + // Skip and count leading '1's. + int zeroes = 0; + int length = 0; + while (*psz == '1') { + zeroes++; + psz++; + } + // Allocate enough space in big-endian base256 representation. + int size = strlen(psz) * 733 /1000 + 1; // log(58) / log(256), rounded up. + std::vector b256(size); + // Process the characters. + static_assert(sizeof(mapBase58)/sizeof(mapBase58[0]) == 256, "mapBase58.size() should be 256"); // guarantee not out of range + while (*psz && !isspace(*psz)) { + // Decode base58 character + int carry = mapBase58[(uint8_t)*psz]; + if (carry == -1) // Invalid b58 character + return false; + int i = 0; + for (std::vector::reverse_iterator it = b256.rbegin(); (carry != 0 || i < length) && (it != b256.rend()); ++it, ++i) { + carry += 58 * (*it); + *it = carry % 256; + carry /= 256; + } + assert(carry == 0); + length = i; + psz++; + } + // Skip trailing spaces. + while (isspace(*psz)) + psz++; + if (*psz != 0) + return false; + // Skip leading zeroes in b256. + std::vector::iterator it = b256.begin() + (size - length); + // Copy result into output vector. + vch.reserve(zeroes + (b256.end() - it)); + vch.assign(zeroes, 0x00); + while (it != b256.end()) + vch.push_back(*(it++)); + return true; +} + +bool DecodeBase58(const std::string& str, std::vector& vchRet) +{ + return DecodeBase58(str.c_str(), vchRet); +} diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index f668c05..65c9ada 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -295,7 +295,7 @@ void force::reservetask(uint32_t campaign_id, // check right asset owner auto acc_asset = acc_assets_tbl.find(asset_id); auto force_asset = force_assets_tbl.find(asset_id); - bool asset_is_eos = acc_asset != acc_assets_tbl.end(); + bool asset_is_eos = (acc_asset != acc_assets_tbl.end()); eosio::check(asset_is_eos || force_asset != force_assets_tbl.end(), "asset now owned by you"); asset = asset_is_eos ? *acc_asset : *force_asset; diff --git a/tests/atomicassets.abi b/tests/atomicassets.abi new file mode 100644 index 0000000..5ebcf81 --- /dev/null +++ b/tests/atomicassets.abi @@ -0,0 +1,1006 @@ +{ + "version": "eosio::abi/1.1", + "types": [{ + "new_type_name": "ATOMIC_ATTRIBUTE", + "type": "variant_int8_int16_int32_int64_uint8_uint16_uint32_uint64_float32_float64_string_INT8_VEC_INT16_VEC_INT32_VEC_INT64_VEC_UINT8_VEC_UINT16_VEC_UINT32_VEC_UINT64_VEC_FLOAT_VEC_DOUBLE_VEC_STRING_VEC" + },{ + "new_type_name": "ATTRIBUTE_MAP", + "type": "pair_string_ATOMIC_ATTRIBUTE[]" + },{ + "new_type_name": "DOUBLE_VEC", + "type": "float64[]" + },{ + "new_type_name": "FLOAT_VEC", + "type": "float32[]" + },{ + "new_type_name": "INT16_VEC", + "type": "int16[]" + },{ + "new_type_name": "INT32_VEC", + "type": "int32[]" + },{ + "new_type_name": "INT64_VEC", + "type": "int64[]" + },{ + "new_type_name": "INT8_VEC", + "type": "bytes" + },{ + "new_type_name": "STRING_VEC", + "type": "string[]" + },{ + "new_type_name": "UINT16_VEC", + "type": "uint16[]" + },{ + "new_type_name": "UINT32_VEC", + "type": "uint32[]" + },{ + "new_type_name": "UINT64_VEC", + "type": "uint64[]" + },{ + "new_type_name": "UINT8_VEC", + "type": "uint8[]" + } + ], + "structs": [{ + "name": "FORMAT", + "base": "", + "fields": [{ + "name": "name", + "type": "string" + },{ + "name": "type", + "type": "string" + } + ] + },{ + "name": "acceptoffer", + "base": "", + "fields": [{ + "name": "offer_id", + "type": "uint64" + } + ] + },{ + "name": "addcolauth", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "account_to_add", + "type": "name" + } + ] + },{ + "name": "addconftoken", + "base": "", + "fields": [{ + "name": "token_contract", + "type": "name" + },{ + "name": "token_symbol", + "type": "symbol" + } + ] + },{ + "name": "addnotifyacc", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "account_to_add", + "type": "name" + } + ] + },{ + "name": "admincoledit", + "base": "", + "fields": [{ + "name": "collection_format_extension", + "type": "FORMAT[]" + } + ] + },{ + "name": "announcedepo", + "base": "", + "fields": [{ + "name": "owner", + "type": "name" + },{ + "name": "symbol_to_announce", + "type": "symbol" + } + ] + },{ + "name": "assets_s", + "base": "", + "fields": [{ + "name": "asset_id", + "type": "uint64" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "template_id", + "type": "int32" + },{ + "name": "ram_payer", + "type": "name" + },{ + "name": "backed_tokens", + "type": "asset[]" + },{ + "name": "immutable_serialized_data", + "type": "uint8[]" + },{ + "name": "mutable_serialized_data", + "type": "uint8[]" + } + ] + },{ + "name": "backasset", + "base": "", + "fields": [{ + "name": "payer", + "type": "name" + },{ + "name": "asset_owner", + "type": "name" + },{ + "name": "asset_id", + "type": "uint64" + },{ + "name": "token_to_back", + "type": "asset" + } + ] + },{ + "name": "balances_s", + "base": "", + "fields": [{ + "name": "owner", + "type": "name" + },{ + "name": "quantities", + "type": "asset[]" + } + ] + },{ + "name": "burnasset", + "base": "", + "fields": [{ + "name": "asset_owner", + "type": "name" + },{ + "name": "asset_id", + "type": "uint64" + } + ] + },{ + "name": "canceloffer", + "base": "", + "fields": [{ + "name": "offer_id", + "type": "uint64" + } + ] + },{ + "name": "collections_s", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "author", + "type": "name" + },{ + "name": "allow_notify", + "type": "bool" + },{ + "name": "authorized_accounts", + "type": "name[]" + },{ + "name": "notify_accounts", + "type": "name[]" + },{ + "name": "market_fee", + "type": "float64" + },{ + "name": "serialized_data", + "type": "uint8[]" + } + ] + },{ + "name": "config_s", + "base": "", + "fields": [{ + "name": "asset_counter", + "type": "uint64" + },{ + "name": "template_counter", + "type": "int32" + },{ + "name": "offer_counter", + "type": "uint64" + },{ + "name": "collection_format", + "type": "FORMAT[]" + },{ + "name": "supported_tokens", + "type": "extended_symbol[]" + } + ] + },{ + "name": "createcol", + "base": "", + "fields": [{ + "name": "author", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "allow_notify", + "type": "bool" + },{ + "name": "authorized_accounts", + "type": "name[]" + },{ + "name": "notify_accounts", + "type": "name[]" + },{ + "name": "market_fee", + "type": "float64" + },{ + "name": "data", + "type": "ATTRIBUTE_MAP" + } + ] + },{ + "name": "createoffer", + "base": "", + "fields": [{ + "name": "sender", + "type": "name" + },{ + "name": "recipient", + "type": "name" + },{ + "name": "sender_asset_ids", + "type": "uint64[]" + },{ + "name": "recipient_asset_ids", + "type": "uint64[]" + },{ + "name": "memo", + "type": "string" + } + ] + },{ + "name": "createschema", + "base": "", + "fields": [{ + "name": "authorized_creator", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "schema_format", + "type": "FORMAT[]" + } + ] + },{ + "name": "createtempl", + "base": "", + "fields": [{ + "name": "authorized_creator", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "transferable", + "type": "bool" + },{ + "name": "burnable", + "type": "bool" + },{ + "name": "max_supply", + "type": "uint32" + },{ + "name": "immutable_data", + "type": "ATTRIBUTE_MAP" + } + ] + },{ + "name": "declineoffer", + "base": "", + "fields": [{ + "name": "offer_id", + "type": "uint64" + } + ] + },{ + "name": "extended_symbol", + "base": "", + "fields": [{ + "name": "sym", + "type": "symbol" + },{ + "name": "contract", + "type": "name" + } + ] + },{ + "name": "extendschema", + "base": "", + "fields": [{ + "name": "authorized_editor", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "schema_format_extension", + "type": "FORMAT[]" + } + ] + },{ + "name": "forbidnotify", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + } + ] + },{ + "name": "init", + "base": "", + "fields": [] + },{ + "name": "locktemplate", + "base": "", + "fields": [{ + "name": "authorized_editor", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "template_id", + "type": "int32" + } + ] + },{ + "name": "logbackasset", + "base": "", + "fields": [{ + "name": "asset_owner", + "type": "name" + },{ + "name": "asset_id", + "type": "uint64" + },{ + "name": "backed_token", + "type": "asset" + } + ] + },{ + "name": "logburnasset", + "base": "", + "fields": [{ + "name": "asset_owner", + "type": "name" + },{ + "name": "asset_id", + "type": "uint64" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "template_id", + "type": "int32" + },{ + "name": "backed_tokens", + "type": "asset[]" + },{ + "name": "old_immutable_data", + "type": "ATTRIBUTE_MAP" + },{ + "name": "old_mutable_data", + "type": "ATTRIBUTE_MAP" + },{ + "name": "asset_ram_payer", + "type": "name" + } + ] + },{ + "name": "logmint", + "base": "", + "fields": [{ + "name": "asset_id", + "type": "uint64" + },{ + "name": "authorized_minter", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "template_id", + "type": "int32" + },{ + "name": "new_asset_owner", + "type": "name" + },{ + "name": "immutable_data", + "type": "ATTRIBUTE_MAP" + },{ + "name": "mutable_data", + "type": "ATTRIBUTE_MAP" + },{ + "name": "backed_tokens", + "type": "asset[]" + },{ + "name": "immutable_template_data", + "type": "ATTRIBUTE_MAP" + } + ] + },{ + "name": "lognewoffer", + "base": "", + "fields": [{ + "name": "offer_id", + "type": "uint64" + },{ + "name": "sender", + "type": "name" + },{ + "name": "recipient", + "type": "name" + },{ + "name": "sender_asset_ids", + "type": "uint64[]" + },{ + "name": "recipient_asset_ids", + "type": "uint64[]" + },{ + "name": "memo", + "type": "string" + } + ] + },{ + "name": "lognewtempl", + "base": "", + "fields": [{ + "name": "template_id", + "type": "int32" + },{ + "name": "authorized_creator", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "transferable", + "type": "bool" + },{ + "name": "burnable", + "type": "bool" + },{ + "name": "max_supply", + "type": "uint32" + },{ + "name": "immutable_data", + "type": "ATTRIBUTE_MAP" + } + ] + },{ + "name": "logsetdata", + "base": "", + "fields": [{ + "name": "asset_owner", + "type": "name" + },{ + "name": "asset_id", + "type": "uint64" + },{ + "name": "old_data", + "type": "ATTRIBUTE_MAP" + },{ + "name": "new_data", + "type": "ATTRIBUTE_MAP" + } + ] + },{ + "name": "logtransfer", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "from", + "type": "name" + },{ + "name": "to", + "type": "name" + },{ + "name": "asset_ids", + "type": "uint64[]" + },{ + "name": "memo", + "type": "string" + } + ] + },{ + "name": "mintasset", + "base": "", + "fields": [{ + "name": "authorized_minter", + "type": "name" + },{ + "name": "collection_name", + "type": "name" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "template_id", + "type": "int32" + },{ + "name": "new_asset_owner", + "type": "name" + },{ + "name": "immutable_data", + "type": "ATTRIBUTE_MAP" + },{ + "name": "mutable_data", + "type": "ATTRIBUTE_MAP" + },{ + "name": "tokens_to_back", + "type": "asset[]" + } + ] + },{ + "name": "offers_s", + "base": "", + "fields": [{ + "name": "offer_id", + "type": "uint64" + },{ + "name": "sender", + "type": "name" + },{ + "name": "recipient", + "type": "name" + },{ + "name": "sender_asset_ids", + "type": "uint64[]" + },{ + "name": "recipient_asset_ids", + "type": "uint64[]" + },{ + "name": "memo", + "type": "string" + },{ + "name": "ram_payer", + "type": "name" + } + ] + },{ + "name": "pair_string_ATOMIC_ATTRIBUTE", + "base": "", + "fields": [{ + "name": "key", + "type": "string" + },{ + "name": "value", + "type": "ATOMIC_ATTRIBUTE" + } + ] + },{ + "name": "payofferram", + "base": "", + "fields": [{ + "name": "payer", + "type": "name" + },{ + "name": "offer_id", + "type": "uint64" + } + ] + },{ + "name": "remcolauth", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "account_to_remove", + "type": "name" + } + ] + },{ + "name": "remnotifyacc", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "account_to_remove", + "type": "name" + } + ] + },{ + "name": "schemas_s", + "base": "", + "fields": [{ + "name": "schema_name", + "type": "name" + },{ + "name": "format", + "type": "FORMAT[]" + } + ] + },{ + "name": "setassetdata", + "base": "", + "fields": [{ + "name": "authorized_editor", + "type": "name" + },{ + "name": "asset_owner", + "type": "name" + },{ + "name": "asset_id", + "type": "uint64" + },{ + "name": "new_mutable_data", + "type": "ATTRIBUTE_MAP" + } + ] + },{ + "name": "setcoldata", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "data", + "type": "ATTRIBUTE_MAP" + } + ] + },{ + "name": "setmarketfee", + "base": "", + "fields": [{ + "name": "collection_name", + "type": "name" + },{ + "name": "market_fee", + "type": "float64" + } + ] + },{ + "name": "setversion", + "base": "", + "fields": [{ + "name": "new_version", + "type": "string" + } + ] + },{ + "name": "templates_s", + "base": "", + "fields": [{ + "name": "template_id", + "type": "int32" + },{ + "name": "schema_name", + "type": "name" + },{ + "name": "transferable", + "type": "bool" + },{ + "name": "burnable", + "type": "bool" + },{ + "name": "max_supply", + "type": "uint32" + },{ + "name": "issued_supply", + "type": "uint32" + },{ + "name": "immutable_serialized_data", + "type": "uint8[]" + } + ] + },{ + "name": "tokenconfigs_s", + "base": "", + "fields": [{ + "name": "standard", + "type": "name" + },{ + "name": "version", + "type": "string" + } + ] + },{ + "name": "transfer", + "base": "", + "fields": [{ + "name": "from", + "type": "name" + },{ + "name": "to", + "type": "name" + },{ + "name": "asset_ids", + "type": "uint64[]" + },{ + "name": "memo", + "type": "string" + } + ] + },{ + "name": "withdraw", + "base": "", + "fields": [{ + "name": "owner", + "type": "name" + },{ + "name": "token_to_withdraw", + "type": "asset" + } + ] + } + ], + "actions": [{ + "name": "acceptoffer", + "type": "acceptoffer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Accept an offer\nsummary: 'The offer with the id {{nowrap offer_id}} is accepted'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThe recipient of the offer with the id {{offer_id}} accepts the offer.\n\nThe assets from either side specified in the offer are automatically transferred to the respective other side.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the recipient of the offer.\n
" + },{ + "name": "addcolauth", + "type": "addcolauth", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Make an account authorized in a collection\nsummary: 'Add the account {{nowrap account_to_add}} to the authorized_accounts list of the collection {{nowrap collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nAdds the account {{account_to_add}} to the authorized_accounts list of the collection {{collection_name}}.\n\nThis allows {{account_to_add}} to both create and edit templates and assets of this collection.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the collection's author.\n
" + },{ + "name": "addconftoken", + "type": "addconftoken", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Add token to supported list\nsummary: 'Adds a token that can then be used to back assets'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\nDescription:\n
\nThe token with the symbol {{token_symbol}} from the token contract {{token_contract}} is added to the supported_tokens list.\n\nThis means that assets can then be backed with that specific token.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{$action.account}}.\n
" + },{ + "name": "addnotifyacc", + "type": "addnotifyacc", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Add an account to a collection's notify list\nsummary: 'Add the account {{nowrap account_to_add}} to the notify_accounts list of the collection {{nowrap collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nAdds the account {{account_to_add}} to the notify_accounts list of the collection {{collection_name}}.\n\nThis will make {{account_to_add}} get notifications directly on the blockchain when one of the following actions is performed:\n- One or more assets of the collection {{collection_name}} is transferred\n- An asset of the collection {{collection_name}} is minted\n- An asset of the collection {{collection_name}} has its mutable data changed\n- An asset of the collection {{collection_name}} is burned\n- An asset of the collection {{collection_name}} gets backed with core tokens\n- A template of the collection {{collection_name}} is created\n\n{{account_to_add}} is able to add code to their own smart contract to handle these notifications. \n
\n\nClauses:\n
\nThis action may only be called with the permission of the collection's author.\n\n{{account_to_add}} may not make any transactions throw when receiving a notification. This includes, but is not limited to, purposely blocking certain transfers by making the transaction throw.\n\nIt is the collection author's responsibility to enforce that this does not happen.\n
" + },{ + "name": "admincoledit", + "type": "admincoledit", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Extend collections schema\nsummary: 'Extends the schema to serialize collection data by one or more lines'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThe following FORMAT lines are added to the schema that is used to serialize collections data:\n{{#each collection_format_extension}}\n - name: {{this.name}} , type: {{this.type}}\n{{/each}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{$action.account}}.\n
" + },{ + "name": "announcedepo", + "type": "announcedepo", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Announces a deposit\nsummary: '{{nowrap owner}} adds the symbol {{nowrap symbol_to_announce}} to his balance table row'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThis action is used to add a zero value asset to the quantities vector of the balance row with the owner {{owner}}.\nIf there is no balance row with the owner {{owner}}, a new one is created.\nAdding something to a vector increases the RAM required, therefore this can't be done directly in the receipt of the transfer action, so using this action a zero value is added so that the RAM required doesn't change when adding the received quantity in the transfer action later.\n\nBy calling this action, {{payer}} pays for the RAM of the balance table row with the owner {{owner}}.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{payer}}.\n
" + },{ + "name": "backasset", + "type": "backasset", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Backs an asset with tokens\nsummary: '{{nowrap payer}} backs the asset with the ID {{nowrap asset_id}} with {{nowrap token_to_back}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{payer}} backs an the asset with the ID {{asset_id}} owned by {{asset_owner}} with {{token_to_back}}.\n{{payer}} must have at least as many tokens in his balance. {{token_to_back}} will be removed from {{payer}}'s balance.\nThe tokens backed to this asset can be retreived by burning the asset, in which case the owner at the time of the burn will receive the tokens.\n\n{{payer}} pays for the full RAM cost of the asset.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{payer}}.\n
" + },{ + "name": "burnasset", + "type": "burnasset", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Burn an asset\nsummary: '{{nowrap asset_owner}} burns his asset with the id {{nowrap asset_id}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{asset_owner}} burns his asset with the id {{asset_id}}.\n\nIf there previously were core tokens backed for this asset, these core tokens are transferred to {{asset_owner}}.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{asset_owner}}.\n
" + },{ + "name": "canceloffer", + "type": "canceloffer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Cancel an offer\nsummary: 'The offer with the id {{nowrap offer_id}} is cancelled'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThe creator of the offer with the id {{offer_id}} cancels this offer. The offer is deleted from the offers table.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the creator of the offer.\n
" + },{ + "name": "createcol", + "type": "createcol", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Create collection\nsummary: '{{nowrap author}} creates a new collection with the name {{collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{author}} creates a new collection with the name {{collection_name}}.\n\n{{#if authorized_accounts}}The following accounts are added to the authorized_accounts list, allowing them create and edit templates and assets within this collection:\n {{#each authorized_accounts}}\n - {{this}}\n {{/each}}\n{{else}}No accounts are added to the authorized_accounts list.\n{{/if}}\n\n{{#if notify_accounts}}The following accounts are added to the notify_accounts list, which means that they get notified on the blockchain of any actions related to assets and templates of this collection:\n {{#each notify_accounts}}\n - {{this}}\n {{/each}}\n{{else}}No accounts are added to the notify_accounts list.\n{{/if}}\n\n{{#if allow_notify}}It will be possible to add more accounts to the notify_accounts list later.\n{{else}}It will not be possible to add more accounts to the notify_accounts list later.\n{{/if}}\n\nThe market_fee for this collection will be set to {{market_fee}}. 3rd party markets are encouraged to use this value to collect fees for the collection author, but are not required to do so.\n\n{{#if data}}The collections will be initialized with the following data:\n {{#each data}}\n - name: {{this.key}} , value: {{this.value}}\n {{/each}}\n{{else}}The collection will be initialized without any data.\n{{/if}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{author}}.\n\nCreating collections with the purpose of confusing or taking advantage of others, especially by impersonating other well known brands, personalities or dapps is not allowed.\n\nIf the notify functionality is being used, the notify accounts may not make any transactions throw when receiving the notification. This includes, but is not limited to, purposely blocking certain transfers by making the transaction throw.\n\nIt is the collection author's responsibility to enforce that this does not happen.\n
" + },{ + "name": "createoffer", + "type": "createoffer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Create an offer\nsummary: '{{nowrap sender}} makes an offer to {{nowrap recipient}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{sender}} makes the following offer to {{recipient}}.\n\n{{#if sender_asset_ids}}{{sender}} gives the assets with the following ids:\n {{#each sender_asset_ids}}\n - {{this}}\n {{/each}}\n{{else}}{{sender}} does not give any assets.\n{{/if}}\n\n{{#if recipient_asset_ids}}{{recipient}} gives the assets with the following ids:\n {{#each recipient_asset_ids}}\n - {{this}}\n {{/each}}\n{{else}}{{recipient}} does not give any assets.\n{{/if}}\n\nIf {{recipient}} accepts the offer, the assets will automatically be transferred to the respective sides.\n\n{{#if memo}}There is a memo attached to the offer stating:\n {{memo}}\n{{else}}No memo is attached to the offer.\n{{/if}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{sender}}.\n\nCreating offers that do not serve any purpose other than spamming the recipient is not allowed.\n\n{{sender}} must not take advantage of the notification they receive when the offer is accepted or declined in a way that harms {{recipient}}.\n
" + },{ + "name": "createschema", + "type": "createschema", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Create a schema\nsummary: '{{nowrap authorized_creator}} creates a new schema with the name {{nowrap schema_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{authorized_creator}} creates a new schema with the name {{schema_name}}. This schema belongs to the collection {{collection_name}}\n\n{{#if schema_format}}The schema will be initialized with the following FORMAT lines that can be used to serialize template and asset data:\n {{#each schema_format}}\n - name: {{this.name}} , type: {{this.type}}\n {{/each}}\n{{else}}The schema will be initialized without any FORMAT lines.\n{{/if}}\n\nOnly authorized accounts of the {{collection_name}} collection will be able to extend the schema by adding additional FORMAT lines in the future, but they will not be able to delete previously added FORMAT lines.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{authorized_creator}}.\n\n{{authorized_creator}} has to be an authorized account in the collection {{collection_name}}.\n\nCreating schemas with the purpose of confusing or taking advantage of others, especially by impersonating other well known brands, personalities or dapps is not allowed.\n
" + },{ + "name": "createtempl", + "type": "createtempl", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Create a template\nsummary: '{{nowrap authorized_creator}} creates a new template which belongs to the {{nowrap collection_name}} collection and uses the {{nowrap schema_name}} schema'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{authorized_creator}} creates a new template which belongs to the {{collection_name}} collection.\n\nThe schema {{schema_name}} is used for the serialization of the template's data.\n\n{{#if transferable}}The assets within this template will be transferable\n{{else}}The assets within this template will not be transferable\n{{/if}}\n\n{{#if burnable}}The assets within this template will be burnable\n{{else}}The assets within this template will not be burnable\n{{/if}}\n\n{{#if max_supply}}A maximum of {{max_supply}} assets can ever be created within this template.\n{{else}}There is no maximum amount of assets that can be created within this template.\n{{/if}}\n\n{{#if immutable_data}}The immutable data of the template is set to:\n {{#each immutable_data}}\n - name: {{this.key}} , value: {{this.value}}\n {{/each}}\n{{else}}No immutable data is set for the template.\n{{/if}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{authorized_creator}}.\n\n{{authorized_creator}} has to be an authorized account in the collection {{collection_name}}.\n
" + },{ + "name": "declineoffer", + "type": "declineoffer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Decline an offer\nsummary: 'The offer with the id {{nowrap offer_id}} is declined'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThe recipient of the offer with the id {{offer_id}} declines the offer. The offer is deleted from the offers table.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the recipient of the offer.\n
" + },{ + "name": "extendschema", + "type": "extendschema", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Extend schema\nsummary: 'Extends the schema {{nowrap schema_name}} by adding one or more FORMAT lines'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThe schema {{schema_name}} belonging to the collection {{collection_name}} is extended by adding the following FORMAT lines that can be used to serialize template and asset data:\n{{#each schema_format_extension}}\n - name: {{this.name}} , type: {{this.type}}\n{{/each}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{authorized_editor}}.\n\n{{authorized_editor}} has to be an authorized account in the collection {{collection_name}}.\n
" + },{ + "name": "forbidnotify", + "type": "forbidnotify", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Disallow collection notifications\nsummary: 'Sets the allow_notify value of the collection {{nowrap collection_name}} to false'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThe allow_notify value of the collection {{collection_name}} is set to false.\nThis means that it will not be possible to add accounts to the notify_accounts list later.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the collection's author.\n
" + },{ + "name": "init", + "type": "init", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Initialize config tables\nsummary: 'Initialize the tables \"config\" and \"tokenconfig\" if they have not been initialized before'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nInitialize the tables \"config\" and \"tokenconfig\" if they have not been initialized before. If they have been initialized before, nothing will happen.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{$action.account}}.\n
" + },{ + "name": "locktemplate", + "type": "locktemplate", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Locks a template\nsummary: '{{nowrap authorized_editor}} locks the template with the id {{nowrap template_id}} belonging to the collection {{nowrap collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{authorized_editor}} locks the template with the id {{template_id}} belonging to the collection {{collection_name}}.\n\nThis sets the template's maximum supply to the template's current supply, which means that no more assets referencing this template can be minted.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{authorized_creator}}.\n\n{{authorized_creator}} has to be an authorized account in the collection {{collection_name}}.\n\nThe template's issued supply must be greater than 0.\n
" + },{ + "name": "logbackasset", + "type": "logbackasset", + "ricardian_contract": "" + },{ + "name": "logburnasset", + "type": "logburnasset", + "ricardian_contract": "" + },{ + "name": "logmint", + "type": "logmint", + "ricardian_contract": "" + },{ + "name": "lognewoffer", + "type": "lognewoffer", + "ricardian_contract": "" + },{ + "name": "lognewtempl", + "type": "lognewtempl", + "ricardian_contract": "" + },{ + "name": "logsetdata", + "type": "logsetdata", + "ricardian_contract": "" + },{ + "name": "logtransfer", + "type": "logtransfer", + "ricardian_contract": "" + },{ + "name": "mintasset", + "type": "mintasset", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Mint an asset\nsummary: '{{nowrap authorized_minter}} mints an asset which will be owned by {{nowrap new_asset_owner}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{authorized_minter}} mints an asset of the template which belongs to the {{schema_name}} schema of the {{collection_name}} collection. The asset will be owned by {{new_asset_owner}}.\n\n{{#if immutable_data}}The immutable data of the asset is set to:\n {{#each immutable_data}}\n - name: {{this.key}} , value: {{this.value}}\n {{/each}}\n{{else}}No immutable data is set for the asset.\n{{/if}}\n\n{{#if mutable_data}}The mutable data of the asset is set to:\n {{#each mutable_data}}\n - name: {{this.key}} , value: {{this.value}}\n {{/each}}\n{{else}}No mutable data is set for the asset.\n{{/if}}\n\n{{#if quantities_to_back}}The asset will be backed with the following tokens and {{authorized_minter}} needs to have at least that amount of tokens in their balance:\n {{#each quantities_to_back}}\n - {{quantities_to_back}}\n {{/each}}\n{{/if}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{authorized_minter}}.\n\n{{authorized_minter}} has to be an authorized account in the collection that the template with the id {{template_id}} belongs to.\n\nMinting assets that contain intellectual property requires the permission of the all rights holders of that intellectual property.\n\nMinting assets with the purpose of confusing or taking advantage of others, especially by impersonating other well known brands, personalities or dapps is not allowed.\n\nMinting assets with the purpose of spamming or otherwise negatively impacing {{new_owner}} is not allowed.\n
" + },{ + "name": "payofferram", + "type": "payofferram", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Pays RAM for existing offer\nsummary: '{{nowrap payer}} will pay for the RAM cost of the offer {{nowrap offer_id}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{payer}} pays for the RAM cost of the offer {{offer_id}}. The offer itself is not modified\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{payer}}.\n
" + },{ + "name": "remcolauth", + "type": "remcolauth", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Remove an account's authorization in a collection\nsummary: 'Remove the account {{nowrap account_to_remove}} from the authorized_accounts list of the collection {{nowrap collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nRemoves the account {{account_to_remove}} from the authorized_accounts list of the collection {{collection_name}}.\n\nThis removes {{account_to_remove}}'s permission to both create and edit templates and assets of this collection.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the collection's author.\n
" + },{ + "name": "remnotifyacc", + "type": "remnotifyacc", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Remove an account from a collection's notfiy list\nsummary: 'Remove the account {{nowrap account_to_remove}} from the notify_accounts list of the collection {{nowrap collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nRemoves the account {{account_to_remove}} from the notify_accounts list of the collection {{collection_name}}.\n\n{{account_to_remove}} will therefore no longer receive notifications for any of the actions related to the collection {{collection_name}}.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the collection's author.\n
" + },{ + "name": "setassetdata", + "type": "setassetdata", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set the mutable data of an asset\nsummary: '{{nowrap authorized_editor}} sets the mutable data of the asset with the id {{nowrap asset_id}} owned by {{nowrap asset_owner}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{#if new_mutable_data}}{{authorized_editor}} sets the mutable data of the asset with the id {{asset_id}} owned by {{nowrap asset_owner}} to the following:\n {{#each new_mutable_data}}\n - name: {{this.key}} , value: {{this.value}}\n {{/each}}\n{{else}}{{authorized_editor}} clears the mutable data of the asset with the id {{asset_id}} owned by {{asset_owner}}.\n{{/if}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{authorized_editor}}.\n\n{{authorized_editor}} has to be an authorized account in the collection that the asset with the id {{asset_id}} belongs to. (An asset belongs to the collection that the template it is within belongs to)\n
" + },{ + "name": "setcoldata", + "type": "setcoldata", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set collection data\nsummary: 'Sets the data of the collection {{nowrap collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{#if data}}Sets the data of the collection {{collection_name}} to the following\n {{#each data}}\n - name: {{this.key}} , value: {{this.value}}\n {{/each}}\n{{else}}Clears the data of the collection {{collection_name}}\n{{/if}}\n
\n\nClauses:\n
\nThis action may only be called with the permission of the collection's author.\n
" + },{ + "name": "setmarketfee", + "type": "setmarketfee", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set collection market fee\nsummary: 'Sets the market fee of the collection {{nowrap collection_name}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\nThe market_fee for the collection {{collection_name}} will be set to {{market_fee}}. 3rd party markets are encouraged to use this value to collect fees for the collection author, but are not required to do so.\n
\n\nClauses:\n
\nThis action may only be called with the permission of the collection's author.\n
" + },{ + "name": "setversion", + "type": "setversion", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set tokenconfig version\nsummary: 'Sets the version in the tokenconfigs table to {{nowrap new_version}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\nDescription:\n
\nThe version in the tokenconfigs table is set to {{new_version}}.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{$action.account}}.\n
" + },{ + "name": "transfer", + "type": "transfer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Transfer Assets\nsummary: 'Send one or more assets from {{nowrap from}} to {{nowrap to}}'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{from}} transfers one or more assets with the following ids to {{to}}:\n{{#each asset_ids}}\n - {{this}}\n{{/each}}\n\n{{#if memo}}There is a memo attached to the transfer stating:\n {{memo}}\n{{else}}No memo is attached to the transfer.\n{{/if}}\n\nIf {{to}} does not own any assets, {{from}} pays the RAM for the scope of {{to}} in the assets table.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{from}}.\n\nTransfers that do not serve any purpose other than spamming the recipient are not allowed.\n
" + },{ + "name": "withdraw", + "type": "withdraw", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Withdraws fungible tokens\nsummary: '{{nowrap owner}} withdraws {{token_to_withdraw}} from his balance'\nicon: https://atomicassets.io/image/logo256.png#108AEE3530F4EB368A4B0C28800894CFBABF46534F48345BF6453090554C52D5\n---\n\nDescription:\n
\n{{owner}} withdraws {{token_to_withdraw}} that they previously deposited and have not yet spent otherwise.\nThe tokens will be transferred back to {{owner}} and will be deducted from {{owner}}'s balance.\n
\n\nClauses:\n
\nThis action may only be called with the permission of {{owner}}.\n
" + } + ], + "tables": [{ + "name": "assets", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "assets_s" + },{ + "name": "balances", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "balances_s" + },{ + "name": "collections", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "collections_s" + },{ + "name": "config", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "config_s" + },{ + "name": "offers", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "offers_s" + },{ + "name": "schemas", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "schemas_s" + },{ + "name": "templates", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "templates_s" + },{ + "name": "tokenconfigs", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "tokenconfigs_s" + } + ], + "ricardian_clauses": [], + "error_messages": [], + "abi_extensions": [], + "variants": [{ + "name": "variant_int8_int16_int32_int64_uint8_uint16_uint32_uint64_float32_float64_string_INT8_VEC_INT16_VEC_INT32_VEC_INT64_VEC_UINT8_VEC_UINT16_VEC_UINT32_VEC_UINT64_VEC_FLOAT_VEC_DOUBLE_VEC_STRING_VEC", + "types": [ + "int8", + "int16", + "int32", + "int64", + "uint8", + "uint16", + "uint32", + "uint64", + "float32", + "float64", + "string", + "INT8_VEC", + "INT16_VEC", + "INT32_VEC", + "INT64_VEC", + "UINT8_VEC", + "UINT16_VEC", + "UINT32_VEC", + "UINT64_VEC", + "FLOAT_VEC", + "DOUBLE_VEC", + "STRING_VEC" + ] + } + ], + "action_results": [] +} \ No newline at end of file diff --git a/tests/atomicassets.wasm b/tests/atomicassets.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5b4661f97b1e325f6c19e3be9efa5a3815e21fcf GIT binary patch literal 244822 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

<-LkWV1S=AfID0lGG{ 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 Y>-}6G;QAofhqyk>1+q5khrrPP3k?sv8~^|S literal 0 HcmV?d00001 diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index a3585f9..b2c9c1f 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -45,9 +45,10 @@ "Applies `get-in` on the result of `get-table-rows` Scope is assumed to be the same as account" - [acc tbl vec] - (.then (eos/get-table-rows acc acc tbl) - #(get-in % vec))) + ([acc tbl vec] (get-in-rows acc acc tbl vec)) + ([acc scope tbl vec] + (.then (eos/get-table-rows acc scope tbl) + #(get-in % vec)))) (println "acc-4 " acc-4) (println "acc-5 " acc-5) @@ -62,6 +63,9 @@ (def force-vacc-id 5) +(def atomic-acc "atomicassets") +(def force-collection-name "forcecollect") + (use-fixtures :once {:before (fn [] @@ -71,6 +75,7 @@ (try ( Date: Wed, 9 Aug 2023 16:43:57 +0200 Subject: [PATCH 10/45] force: Remove old qualification system --- contracts/force/force.cpp | 58 ------------------- contracts/force/force.hpp | 76 ------------------------- tests/e2e/force.cljs | 115 -------------------------------------- 3 files changed, 249 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 65c9ada..f6e9525 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -201,64 +201,6 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si } } -void force::mkquali(content content, uint32_t account_id, eosio::name payer, vaccount::sig sig) { - mkquali_params params = {18, account_id, content}; - require_vaccount(account_id, pack(params), sig); - - quali_table quali_tbl(_self, _self.value); - uint32_t quali_id = quali_tbl.available_primary_key(); - quali_tbl.emplace(payer, - [&](auto& q) - { - q.id = quali_id; - q.content = content; - q.account_id = account_id; - }); -} - - -void force::editquali(uint32_t quali_id, content content, uint32_t account_id, - eosio::name payer, vaccount::sig sig) { - quali_table quali_table(_self, _self.value); - auto& quali = quali_table.get(quali_id, "qualification does not exist"); - - editquali_params params = {20, quali_id, content}; - std::vector msg_bytes = pack(params); - require_vaccount(account_id, pack(params), sig); - - quali_table.modify(quali, payer, [&] (auto& q) { q.content = content; }); -} - -void force::assignquali(uint32_t quali_id, uint32_t user_id, std::string value, - eosio::name payer, vaccount::sig sig) { - quali_table quali_tbl(_self, _self.value); - auto quali = quali_tbl.get(quali_id, "qualification not found"); - assignquali_params params = {19, quali_id, user_id, value}; - require_vaccount(quali.account_id, pack(params), sig); - - user_quali_table user_quali_tbl(_self, _self.value); - user_quali_tbl.emplace(payer, - [&](auto& q) - { - q.account_id = user_id; - q.quali_id = quali_id; - q.value.emplace(value); - }); -} - -void force::uassignquali(uint32_t quali_id, uint32_t user_id, eosio::name payer, vaccount::sig sig) { - quali_table quali_tbl(_self, _self.value); - auto quali = quali_tbl.get(quali_id, "qualification not found"); - rmbatch_params params = {20, quali_id, user_id}; - require_vaccount(quali.account_id, pack(params), sig); - - uint64_t user_quali_key = (uint64_t{user_id} << 32) | quali_id; - user_quali_table user_quali_tbl(_self, _self.value); - auto user_quali = user_quali_tbl.find(user_quali_key); - eosio::check(user_quali != user_quali_tbl.end(), "user does not have quali"); - user_quali_tbl.erase(user_quali); -} - void force::reservetask(uint32_t campaign_id, uint32_t account_id, std::optional> quali_assets, diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index fc56424..4f9fba3 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -143,33 +143,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { eosio::name payer, vaccount::sig sig); - [[eosio::action]] - void mkquali(content content, - uint32_t account_id, - eosio::name payer, - vaccount::sig sig); - - [[eosio::action]] - void editquali(uint32_t quali_id, - content content, - uint32_t account_id, - eosio::name payer, - vaccount::sig sig); - - - [[eosio::action]] - void assignquali(uint32_t quali_id, - uint32_t user_id, - std::string value, - eosio::name payer, - vaccount::sig sig); - - [[eosio::action]] - void uassignquali(uint32_t quali_id, - uint32_t user_id, - eosio::name payer, - vaccount::sig sig); - [[eosio::on_notify("*::vtransfer")]] void vtransfer_handler(uint64_t from_id, uint64_t to_id, @@ -256,20 +229,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { EOSLIB_SERIALIZE(rmcampaign_params, (mark)(campaign_id)); }; - struct mkquali_params { - uint8_t mark; - uint32_t account_id; - content content; - EOSLIB_SERIALIZE(mkquali_params, (mark)(account_id)(content)); - }; - - struct editquali_params { - uint8_t mark; - uint32_t quali_id; - content content; - EOSLIB_SERIALIZE(editquali_params, (mark)(quali_id)(content)); - }; - struct mkbatch_params { uint8_t mark; uint32_t id; @@ -298,20 +257,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { EOSLIB_SERIALIZE(rmbatch_params, (mark)(id)(campaign_id)); }; - struct assignquali_params { - uint8_t mark; - uint32_t id; - uint32_t campaign_id; - std::string value; - EOSLIB_SERIALIZE(assignquali_params, (mark)(id)(campaign_id)(value)); - }; - - struct joinbatch_params { - uint8_t mark; - uint64_t batch_id; - EOSLIB_SERIALIZE(joinbatch_params, (mark)(batch_id)); - }; - struct payout_params { uint8_t mark; uint32_t payment_id; @@ -422,23 +367,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { (data)(paid)(submitted_on)) }; - struct [[eosio::table]] quali { - uint32_t id; - content content; - uint32_t account_id; - - uint64_t primary_key() const { return uint64_t{id}; } - uint64_t by_account() const { return (uint64_t) account_id; } - }; - - struct [[eosio::table]] userquali { - uint32_t account_id; - uint32_t quali_id; - eosio::binary_extension value; - - uint64_t primary_key() const { return (uint64_t{account_id} << 32) | quali_id; } - }; - inline void require_vaccount(uint32_t acc_id, std::vector msg, vaccount::sig sig) { eosio::name vacc_contract = _config.get().vaccount_contract; vaccount::account_table acc_tbl(vacc_contract, vacc_contract.value); @@ -477,10 +405,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { indexed_by<"acc"_n, const_mem_fun>> payment_table; - typedef multi_index<"quali"_n, quali, - indexed_by<"acc"_n, const_mem_fun>> quali_table; - typedef multi_index<"userquali"_n, userquali> user_quali_table; - const eosio::name settings_pk = "settings"_n; struct [[eosio::table]] settings { diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index b2c9c1f..a855e4d 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -223,31 +223,6 @@ (.push 8) (.pushUint32 id) (.pushUint32 camp-id) (.push 0) (.pushString content) (.pushUint8ArrayChecked (vacc/hex->bytes root) 32)))) -(defn pack-mkquali-params [acc-id content] - (.asUint8Array - (doto (new (.-SerialBuffer Serialize)) - (.push 18) (.pushUint32 acc-id) (.push 0) (.pushString content)))) - -(defn pack-editquali-params [acc-id content] - (.asUint8Array - (doto (new (.-SerialBuffer Serialize)) - (.push 20) (.pushUint32 acc-id) (.push 0) (.pushString content)))) - -(defn pack-mkquali-params [acc-id content] - (.asUint8Array - (doto (new (.-SerialBuffer Serialize)) - (.push 18) (.pushUint32 acc-id) (.push 0) (.pushString content)))) - -(defn pack-assignquali-params [id user-id value] - (.asUint8Array - (doto (new (.-SerialBuffer Serialize)) - (.push 19) (.pushUint32 id) (.pushUint32 user-id) (.pushString value)))) - -(defn pack-uassignquali-params [id user-id] - (.asUint8Array - (doto (new (.-SerialBuffer Serialize)) - (.push 20) (.pushUint32 id) (.pushUint32 user-id)))) - (defn pack-rmbatch-params [id camp-id] (.asUint8Array (doto (new (.-SerialBuffer Serialize)) @@ -574,96 +549,6 @@ (publish-batch acc-4 3 (get-composite-key 0 5) 3 (sign-params (pack-reopenbatch-params (get-composite-key 0 5)))))))) -(async-deftest mkquali - (let [r (hex (.digest (.update (.hash ec) data)))) From ad2933d2708f1e8d2e3d6a844245f7b5495aeeb2 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 9 Aug 2023 16:53:48 +0200 Subject: [PATCH 11/45] force: Remove old config table and its migration --- contracts/force/force.cpp | 22 ++++++++++++++------ contracts/force/force.hpp | 42 +++++++-------------------------------- tests/e2e/force.cljs | 25 +++++++---------------- 3 files changed, 30 insertions(+), 59 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index f6e9525..47c61f6 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -1,12 +1,22 @@ #include "force.hpp" -void force::init(eosio::name vaccount_contract, uint32_t force_vaccount_id, - uint32_t payout_delay_sec, uint32_t release_task_delay_sec) { +void force::init(eosio::name vaccount_contract, + uint32_t force_vaccount_id, + uint32_t payout_delay_sec, + uint32_t release_task_delay_sec, + eosio::name fee_contract, + float fee_percentage) { eosio::require_auth(_self); - _config.set(config{vaccount_contract, - force_vaccount_id, - payout_delay_sec, - release_task_delay_sec}, _self); + _settings.emplace(_self, + [&](auto& s) + { + s.vaccount_contract = vaccount_contract; + s.force_vaccount_id = force_vaccount_id; + s.payout_delay_sec = payout_delay_sec; + s.release_task_delay_sec = release_task_delay_sec; + s.fee_contract = fee_contract; + s.fee_percentage = fee_percentage; + }); } void force::mkcampaign(vaccount::vaddress owner, diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 4f9fba3..29bcff6 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -52,14 +52,16 @@ class [[eosio::contract("force")]] force : public eosio::contract { }; force(eosio::name receiver, eosio::name code, eosio::datastream ds) : - eosio::contract(receiver, code, ds), _config(_self, _self.value), _settings(_self, _self.value) + eosio::contract(receiver, code, ds), _settings(_self, _self.value) {}; [[eosio::action]] void init(eosio::name vaccount_contract, uint32_t force_vaccount_id, uint32_t payout_delay_sec, - uint32_t release_task_delay_sec); + uint32_t release_task_delay_sec, + eosio::name fee_contract, + float fee_percentage); [[eosio::action]] void mkcampaign(vaccount::vaddress owner, @@ -161,26 +163,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { // cleanTable(_self, _self.value, 100); // }; - [[eosio::action]] - void migrate(eosio::name payer, eosio::name fee_contract, float fee_percentage) { - require_auth(_self); - auto c = _config.get(); - auto itr = _settings.find(settings_pk.value); - - eosio::check(itr == _settings.end(), "already migrated"); - - _settings.emplace(payer, - [&](auto& s) - { - s.vaccount_contract = c.vaccount_contract; - s.force_vaccount_id = c.force_vaccount_id; - s.payout_delay_sec = c.payout_delay_sec; - s.release_task_delay_sec = c.release_task_delay_sec; - s.fee_contract = fee_contract; - s.fee_percentage = fee_percentage; - }); - } - private: inline bool has_expired(time_point_sec base_time, uint32_t delay) { return time_point_sec(now()) >= (base_time + delay); @@ -189,8 +171,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { inline bool past_delay(time_point_sec base_time, std::string type_delay) { auto delay = NULL; - if (type_delay == "payout") delay = _config.get().payout_delay_sec; - else if (type_delay == "release_task") delay = _config.get().release_task_delay_sec; + if (type_delay == "payout") delay = get_settings().payout_delay_sec; + else if (type_delay == "release_task") delay = get_settings().release_task_delay_sec; return time_point_sec(now()) >= (base_time + delay); } @@ -368,21 +350,12 @@ class [[eosio::contract("force")]] force : public eosio::contract { }; inline void require_vaccount(uint32_t acc_id, std::vector msg, vaccount::sig sig) { - eosio::name vacc_contract = _config.get().vaccount_contract; + eosio::name vacc_contract = get_settings().vaccount_contract; vaccount::account_table acc_tbl(vacc_contract, vacc_contract.value); vaccount::account acc = acc_tbl.get((uint64_t) acc_id, "account row not found"); vaccount::require_auth(msg, acc.address, sig); }; - struct [[eosio::table]] config { - eosio::name vaccount_contract; - uint32_t force_vaccount_id; - uint32_t payout_delay_sec; - uint32_t release_task_delay_sec; - }; - - typedef singleton<"config"_n, config> config_table; - typedef multi_index<"campaign"_n, campaign> campaign_table; typedef multi_index<"batch"_n, batch> batch_table; typedef multi_index<"repsdone"_n, repsdone> repsdone_table; @@ -419,7 +392,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { typedef multi_index<"settings"_n, settings> settings_table; settings_table _settings; - config_table _config; settings get_settings() { auto itr = _settings.find(settings_pk.value); diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index a855e4d..4566113 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -165,24 +165,13 @@ (hex From 11374d24df9aae9741e4accb425d73d4d14e12e3 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 11 Aug 2023 00:11:49 +0200 Subject: [PATCH 12/45] force: Remove task merkle root for batches --- contracts/force/force.cpp | 4 +--- contracts/force/force.hpp | 5 +---- tests/e2e/force.cljs | 20 +++----------------- 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 47c61f6..9375d30 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -87,14 +87,13 @@ void force::rmcampaign(uint32_t campaign_id, vaccount::vaddress owner, vaccount: void force::mkbatch(uint32_t id, uint32_t campaign_id, content content, - checksum256 task_merkle_root, uint32_t repetitions, eosio::name payer, vaccount::sig sig) { campaign_table camp_tbl(_self, _self.value); auto& camp = camp_tbl.get(campaign_id, "campaign not found"); - mkbatch_params params = {8, id, campaign_id, content, task_merkle_root}; + mkbatch_params params = {8, id, campaign_id, content}; std::vector msg_bytes = pack(params); vaccount::require_auth(msg_bytes, camp.owner, sig); @@ -106,7 +105,6 @@ void force::mkbatch(uint32_t id, b.campaign_id = campaign_id; b.id = id; b.content = content; - b.task_merkle_root = task_merkle_root; b.balance = {0, camp.reward.get_extended_symbol()}; b.repetitions = repetitions; b.reward.emplace(camp.reward); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 29bcff6..7c6dc56 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -90,7 +90,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { void mkbatch(uint32_t id, uint32_t campaign_id, content content, - checksum256 task_merkle_root, uint32_t repetitions, eosio::name payer, vaccount::sig sig); @@ -216,8 +215,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t id; uint32_t campaign_id; content content; - checksum256 task_merkle_root; - EOSLIB_SERIALIZE(mkbatch_params, (mark)(id)(campaign_id)(content)(task_merkle_root)); + EOSLIB_SERIALIZE(mkbatch_params, (mark)(id)(campaign_id)(content)); }; struct closebatch_params { @@ -273,7 +271,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t id; uint32_t campaign_id; content content; - checksum256 task_merkle_root; eosio::extended_asset balance; uint32_t repetitions; uint32_t tasks_done; diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 4566113..4e0ad28 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -14,7 +14,6 @@ [e2e.vaccount :as vacc] ["eosjs/dist/eosjs-key-conversions" :refer [PrivateKey Signature PublicKey]] ["eosjs/dist/ripemd" :refer [RIPEMD160]] - [merkletreejs :refer [MerkleTree]] [clojure.string :as string] [elliptic :refer [ec]])) @@ -355,8 +354,6 @@ (is (nil? ( Date: Fri, 11 Aug 2023 12:22:26 +0200 Subject: [PATCH 13/45] force: Fix broken e2e test --- tests/e2e/force.cljs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 4e0ad28..f44f2cd 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -205,11 +205,10 @@ (doto (new (.-SerialBuffer Serialize)) (.push mark) (.pushNumberAsUint64 task-id)(.pushUint32 acc-id)))) -(defn pack-mkbatch-params [id camp-id content root] +(defn pack-mkbatch-params [id camp-id content] (.asUint8Array (doto (new (.-SerialBuffer Serialize)) - (.push 8) (.pushUint32 id) (.pushUint32 camp-id) (.push 0) (.pushString content) - (.pushUint8ArrayChecked (vacc/hex->bytes root) 32)))) + (.push 8) (.pushUint32 id) (.pushUint32 camp-id) (.push 0) (.pushString content)))) (defn pack-rmbatch-params [id camp-id] (.asUint8Array From 8de40701e107bd96b2abb93b924a086f753c96cf Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 11 Aug 2023 13:53:10 +0200 Subject: [PATCH 14/45] force: Allow updating of the settings table --- contracts/force/force.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 9375d30..d51c932 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -7,6 +7,8 @@ void force::init(eosio::name vaccount_contract, eosio::name fee_contract, float fee_percentage) { eosio::require_auth(_self); + auto settings = _settings.find(settings_pk.value); + _settings.erase(settings); _settings.emplace(_self, [&](auto& s) { From 951bb7eab3245567d50e6aa2cd61c7873f15d742 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 11 Aug 2023 16:16:26 +0200 Subject: [PATCH 15/45] force: Allow deleting batches and remove old task reclaiming methods only the active and the last batch of a campaign can be deleted for now. else the campaign could end up in a dead lock --- contracts/force/force.cpp | 91 +++++++++++++++------------------------ contracts/force/force.hpp | 31 ++++--------- 2 files changed, 42 insertions(+), 80 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index d51c932..0ec1834 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -41,6 +41,7 @@ void force::mkcampaign(vaccount::vaddress owner, c.id = camp_id; c.tasks_done = 0; c.active_batch = 0; + c.num_batches = 0; c.content = content; c.owner = owner; c.max_task_time = max_task_time; @@ -93,11 +94,13 @@ void force::mkbatch(uint32_t id, eosio::name payer, vaccount::sig sig) { campaign_table camp_tbl(_self, _self.value); - auto& camp = camp_tbl.get(campaign_id, "campaign not found"); + auto camp = camp_tbl.require_find(campaign_id, "campaign not found"); + + eosio::check(id == camp->num_batches, "batch id must be sequential"); mkbatch_params params = {8, id, campaign_id, content}; std::vector msg_bytes = pack(params); - vaccount::require_auth(msg_bytes, camp.owner, sig); + vaccount::require_auth(msg_bytes, camp->owner, sig); eosio::check(repetitions < force::MAX_REPETITIONS, "too many repetitions"); batch_table batch_tbl(_self, _self.value); @@ -107,11 +110,12 @@ void force::mkbatch(uint32_t id, b.campaign_id = campaign_id; b.id = id; b.content = content; - b.balance = {0, camp.reward.get_extended_symbol()}; + b.balance = {0, camp->reward.get_extended_symbol()}; b.repetitions = repetitions; - b.reward.emplace(camp.reward); + b.reward = camp->reward; b.num_tasks = 0; }); + camp_tbl.modify(camp, eosio::same_payer, [&](auto& c) { c.num_batches += 1; }); } void force::rmbatch(uint32_t id, uint32_t campaign_id, vaccount::sig sig) { @@ -120,18 +124,34 @@ void force::rmbatch(uint32_t id, uint32_t campaign_id, vaccount::sig sig) { uint64_t batch_pk =(uint64_t{campaign_id} << 32) | id; - auto& camp = camp_tbl.get(campaign_id, "campaign not found"); + auto camp = camp_tbl.require_find(campaign_id, "campaign not found"); - auto batch_itr = batch_tbl.find(batch_pk); - eosio::check(batch_itr != batch_tbl.end(), "batch does not exist"); + auto batch = batch_tbl.require_find(batch_pk, "batch does not exist"); rmbatch_params params = {12, id, campaign_id}; std::vector msg_bytes = pack(params); printhex(&msg_bytes[0], msg_bytes.size()); - vaccount::require_auth(msg_bytes, camp.owner, sig); + vaccount::require_auth(msg_bytes, camp->owner, sig); + + uint32_t batch_tasks_done = (camp->tasks_done - batch->start_task_idx); + if (batch->id > camp->active_batch) { + // if the batch has not started, we should empty it, the row + // can only be erased when the campaign caught up + eosio::check(camp->num_batches == id, "can only remove active or last batch"); + camp_tbl.modify(camp, same_payer, + [&](auto& c) { c.num_batches -= 1; c.total_tasks -= batch->num_tasks; }); + batch_tbl.erase(batch); + } else if (batch->id == camp->active_batch) { + // if its the active batch, move onward + batch_tbl.modify(batch, eosio::same_payer, [&](auto& b) { b.num_tasks = batch_tasks_done; }); + camp_tbl.modify(camp, eosio::same_payer, [&](auto& c) { c.active_batch += 1; }); + } else { + // if the batch is in the past, we can erease it + batch_tbl.erase(batch); + } - batch_tbl.erase(batch_itr); + // TODO: refund remaining balance } void force::cleartasks(uint32_t batch_id, uint32_t campaign_id) { @@ -168,10 +188,10 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si settings settings = get_settings(); - reopenbatch_params params = {17, batch_id}; + publishbatch_params params = {17, batch_id}; vaccount::require_auth(pack(params), camp.owner, sig); - eosio::extended_asset task_reward = batch.reward.value(); + eosio::extended_asset task_reward = batch.reward; eosio::extended_asset batch_fee(task_reward.quantity.amount * settings.fee_percentage * num_tasks * batch.repetitions, task_reward.get_extended_symbol()); @@ -476,7 +496,7 @@ void force::submittask(uint32_t campaign_id, submittask_params params = {5, campaign_id, task_idx, data}; require_vaccount(account_id, pack(params), sig); - if (batch.reward.value().quantity.amount > 0) { + if (batch.reward.quantity.amount > 0) { uint64_t payment_id = payment_tbl.available_primary_key(); uint128_t payment_sk = (uint128_t{res->batch_id} << 64) | (uint64_t{account_id} << 32); @@ -490,7 +510,7 @@ void force::submittask(uint32_t campaign_id, p.id = payment_id; p.account_id = account_id; p.batch_id = res->batch_id; - p.pending = batch.reward.value(); + p.pending = batch.reward; p.last_submission_time = time_point_sec(now()); }); } else { @@ -498,7 +518,7 @@ void force::submittask(uint32_t campaign_id, payer, [&](auto& p) { - p.pending += batch.reward.value(); + p.pending += batch.reward; p.last_submission_time = time_point_sec(now()); }); } @@ -538,49 +558,6 @@ void force::releasetask(uint64_t task_id, uint32_t account_id, submission_tbl.modify(sub, eosio::same_payer, [&](auto& s) { s.account_id.reset(); }); } -void force::reclaimtask(uint64_t task_id, uint32_t account_id, - eosio::name payer, vaccount::sig sig) { - submission_table submission_tbl(_self, _self.value); - batch_table batch_tbl(_self, _self.value); - - auto& sub = submission_tbl.get(task_id, "reservation not found"); - auto& batch = batch_tbl.get(sub.batch_id, "batch not found"); - - eosio::check(batch.tasks_done >= 0 && batch.num_tasks > 0, - "cannot reclaim task on paused batch."); - - task_params params = {15, task_id, account_id}; - require_vaccount(account_id, pack(params), sig); - - eosio::check(!sub.account_id.has_value(), "task already reserved"); - eosio::check(!sub.data.has_value(), "task already submitted"); - submission_tbl.modify(sub, - payer, - [&](auto& s) - { - s.account_id = account_id; - s.submitted_on = time_point_sec(now()); - }); -} - -void force::closebatch(uint64_t batch_id, vaccount::vaddress owner, vaccount::sig sig) { - batch_table batch_tbl(_self, _self.value); - campaign_table camp_tbl(_self, _self.value); - - auto& batch = batch_tbl.get(batch_id, "batch not found"); - auto& camp = camp_tbl.get(batch.campaign_id, "campaign not found"); - - closebatch_params params = {16, batch_id}; - - eosio::check(camp.owner == owner, "Only campaign owner can pause batch."); - vaccount::require_auth(pack(params), owner, sig); - eosio::check(batch.tasks_done >= 0 && batch.num_tasks > 0 && - batch.tasks_done < batch.num_tasks * batch.repetitions, - "can only pause batches with active tasks."); - - batch_tbl.modify(batch, eosio::same_payer, [&](auto& b) { b.num_tasks = 0; }); -} - void force::vtransfer_handler(uint64_t from_id, uint64_t to_id, extended_asset quantity, std::string memo, vaccount::sig sig, std::optional fee) { diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 7c6dc56..912a2c0 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -108,11 +108,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { void cleartasks(uint32_t batch_id, uint32_t campaign_id); - [[eosio::action]] - void closebatch(uint64_t batch_id, - vaccount::vaddress owner, - vaccount::sig sig); - [[eosio::action]] void reservetask(uint32_t campaign_id, uint32_t account_id, @@ -138,12 +133,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { eosio::name payer, vaccount::sig sig); - [[eosio::action]] - void reclaimtask(uint64_t task_id, - uint32_t account_id, - eosio::name payer, - vaccount::sig sig); - [[eosio::on_notify("*::vtransfer")]] void vtransfer_handler(uint64_t from_id, uint64_t to_id, @@ -218,16 +207,10 @@ class [[eosio::contract("force")]] force : public eosio::contract { EOSLIB_SERIALIZE(mkbatch_params, (mark)(id)(campaign_id)(content)); }; - struct closebatch_params { - uint8_t mark; - uint64_t id; - EOSLIB_SERIALIZE(closebatch_params, (mark)(id)); - }; - - struct reopenbatch_params { + struct publishbatch_params { uint8_t mark; - uint64_t id; - EOSLIB_SERIALIZE(reopenbatch_params, (mark)(id)); + uint64_t batch_id; + EOSLIB_SERIALIZE(publishbatch_params, (mark)(batch_id)); }; struct rmbatch_params { @@ -255,6 +238,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t tasks_done; uint32_t total_tasks; uint32_t active_batch; + uint32_t num_batches; + bool paused; vaccount::vaddress owner; content content; uint32_t max_task_time; @@ -263,8 +248,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t primary_key() const { return (uint64_t) id; } - EOSLIB_SERIALIZE(campaign, (id)(tasks_done)(total_tasks)(active_batch)(owner)(content) - (max_task_time)(reward)(qualis)) + EOSLIB_SERIALIZE(campaign, (id)(tasks_done)(total_tasks)(active_batch)(num_batches)(owner) + (content)(max_task_time)(reward)(qualis)) }; struct [[eosio::table]] batch { @@ -276,7 +261,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t tasks_done; uint32_t num_tasks; uint32_t start_task_idx; - eosio::binary_extension reward; + eosio::extended_asset reward; uint64_t primary_key() const { return (uint64_t{campaign_id} << 32) | id; } }; From 8b5cf4aa2ec3c28d9bbc4173574715b83cf211eb Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 11 Aug 2023 16:58:32 +0200 Subject: [PATCH 16/45] force: Implement pausing of campaigns --- contracts/force/force.cpp | 52 +++++++++++---------------------------- contracts/force/force.hpp | 15 +++++------ 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 0ec1834..3a83d6a 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -46,6 +46,7 @@ void force::mkcampaign(vaccount::vaddress owner, c.owner = owner; c.max_task_time = max_task_time; c.reward = reward; + c.paused = false; c.qualis = qualis; c.total_tasks = 0; }); @@ -54,6 +55,7 @@ void force::mkcampaign(vaccount::vaddress owner, void force::editcampaign(uint32_t campaign_id, vaccount::vaddress owner, content content, + bool paused, eosio::extended_asset reward, std::vector qualis, eosio::name payer, @@ -61,7 +63,7 @@ void force::editcampaign(uint32_t campaign_id, campaign_table camp_tbl(_self, _self.value); auto& camp = camp_tbl.get(campaign_id, "campaign does not exist"); - editcampaign_params params = {10, campaign_id, content}; + editcampaign_params params = {10, campaign_id, content, reward, paused, qualis}; std::vector msg_bytes = pack(params); vaccount::require_auth(msg_bytes, owner, sig); @@ -135,6 +137,7 @@ void force::rmbatch(uint32_t id, uint32_t campaign_id, vaccount::sig sig) { vaccount::require_auth(msg_bytes, camp->owner, sig); uint32_t batch_tasks_done = (camp->tasks_done - batch->start_task_idx); + if (batch->id > camp->active_batch) { // if the batch has not started, we should empty it, the row // can only be erased when the campaign caught up @@ -144,10 +147,16 @@ void force::rmbatch(uint32_t id, uint32_t campaign_id, vaccount::sig sig) { batch_tbl.erase(batch); } else if (batch->id == camp->active_batch) { // if its the active batch, move onward - batch_tbl.modify(batch, eosio::same_payer, [&](auto& b) { b.num_tasks = batch_tasks_done; }); - camp_tbl.modify(camp, eosio::same_payer, [&](auto& c) { c.active_batch += 1; }); + // batch_tbl.modify(batch, eosio::same_payer, [&](auto& b) { b.num_tasks = batch_tasks_done; }); + uint32_t batch_tasks_remaining = batch->num_tasks - batch_tasks_done; + camp_tbl.modify(camp, eosio::same_payer, + [&](auto& c) + { + c.active_batch += 1; + c.total_tasks -= batch_tasks_remaining; + }); } else { - // if the batch is in the past, we can erease it + // if the batch is in the past, we can erase it batch_tbl.erase(batch); } @@ -239,6 +248,8 @@ void force::reservetask(uint32_t campaign_id, campaign_table campaign_tbl(_self, _self.value); auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); + eosio::check(!campaign.paused, "campaign is paused"); + uint32_t batch_id = campaign.active_batch; uint64_t batch_pk = (uint64_t{campaign_id} << 32) | batch_id; batch_table batch_tbl(_self, _self.value); @@ -525,39 +536,6 @@ void force::submittask(uint32_t campaign_id, } } -void force::releasetask(uint64_t task_id, uint32_t account_id, - eosio::name payer, vaccount::sig sig) { - submission_table submission_tbl(_self, _self.value); - batch_table batch_tbl(_self, _self.value); - campaign_table campaign_tbl(_self, _self.value); - auto vacc_contract = get_settings().vaccount_contract; - - vaccount::account_table acc_tbl(vacc_contract, vacc_contract.value); - vaccount::account acc = acc_tbl.get((uint64_t) account_id, "account row not found"); - - auto& sub = submission_tbl.get(task_id, "reservation not found"); - - auto& batch = batch_tbl.get(sub.batch_id, "batch not found"); - auto& camp = campaign_tbl.get(batch.campaign_id, "campaign not found"); - - eosio::check(sub.account_id.has_value(), "cannot release already released task."); - eosio::check(!sub.data.has_value(), "cannot release task with data."); - - task_params params = {14, task_id, account_id}; - std::vector msg_bytes = pack(params); - - if (sub.account_id == account_id) { - require_vaccount(account_id, msg_bytes, sig); - } - else if (acc.address == camp.owner) { - vaccount::require_auth(msg_bytes, acc.address, sig); - } else { - eosio::check(past_delay(sub.submitted_on, "release_task"), "cannot release task before delay."); - } - - submission_tbl.modify(sub, eosio::same_payer, [&](auto& s) { s.account_id.reset(); }); -} - void force::vtransfer_handler(uint64_t from_id, uint64_t to_id, extended_asset quantity, std::string memo, vaccount::sig sig, std::optional fee) { diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 912a2c0..eb8ee60 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -76,6 +76,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { void editcampaign(uint32_t campaign_id, vaccount::vaddress owner, content content, + bool paused, eosio::extended_asset reward, std::vector qualis, eosio::name payer, @@ -127,12 +128,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { void payout(uint64_t payment_id, std::optional sig); - [[eosio::action]] - void releasetask(uint64_t task_id, - uint32_t account_id, - eosio::name payer, - vaccount::sig sig); - [[eosio::on_notify("*::vtransfer")]] void vtransfer_handler(uint64_t from_id, uint64_t to_id, @@ -189,8 +184,10 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint8_t mark; uint32_t campaign_id; content content; + eosio::extended_asset reward; + bool paused; std::vector qualis; - EOSLIB_SERIALIZE(editcampaign_params, (mark)(campaign_id)(content)); + EOSLIB_SERIALIZE(editcampaign_params, (mark)(campaign_id)(content)(reward)(paused)(qualis)); }; struct rmcampaign_params { @@ -239,8 +236,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t total_tasks; uint32_t active_batch; uint32_t num_batches; - bool paused; vaccount::vaddress owner; + bool paused; content content; uint32_t max_task_time; eosio::extended_asset reward; @@ -249,7 +246,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t primary_key() const { return (uint64_t) id; } EOSLIB_SERIALIZE(campaign, (id)(tasks_done)(total_tasks)(active_batch)(num_batches)(owner) - (content)(max_task_time)(reward)(qualis)) + (paused)(content)(max_task_time)(reward)(qualis)) }; struct [[eosio::table]] batch { From 3729a35e46c65b32de8f285d30698fed8465bcd8 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 11 Oct 2023 13:47:04 +0200 Subject: [PATCH 17/45] ci: Update guix manifest to support docker builds --- manifest.scm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manifest.scm b/manifest.scm index 3b98b14..451204f 100644 --- a/manifest.scm +++ b/manifest.scm @@ -1,6 +1,6 @@ -(use-modules (blockchain) - (gnu packages gcc) - (gnu packages node)) +(use-modules + (gnu packages gcc) + (gnu packages node)) (packages->manifest - (list (list gcc "lib") node eosio-cdt gnu-make)) + (list (list gcc "lib") node gnu-make)) From 8f80ce368ecb74d5b85807e90dce00ae798065ef Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 11 Oct 2023 13:49:40 +0200 Subject: [PATCH 18/45] ci: Update tasks deployment script to V2 --- tasks/effect.clj | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tasks/effect.clj b/tasks/effect.clj index 6cf641f..16bfe0c 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -32,7 +32,7 @@ :proposals {:account "efxproposals" :path "contracts/proposals" :hash nil} - :force {:account "efxforce1112" + :force {:account "effecttasks2" :path "contracts/force" :hash nil} :vaccount {:account "efxaccount11" @@ -473,13 +473,8 @@ (do-cleos net "push" "action" (-> deployment net :force :account) "init" - (str "[" (-> deployment net :vaccount :account) ", 0, 1800, 1800]") - "-p" - (-> deployment net :force :account)) - (do-cleos net "push" "action" - (-> deployment net :force :account) - "migrate" - (str "[" (-> deployment net :force :account) ", " (-> deployment net :feepool :account) ", 0.1]") + (str "[" (-> deployment net :vaccount :account) ", 11, 1800, 1800, " + (-> deployment net :feepool :account) ", 0.1]") "-p" (-> deployment net :force :account)) (do-cleos net "set" "account" "permission" From ffe9b4407e7b612153f4fb4f74e8c788d4b904f8 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 11 Oct 2023 13:57:37 +0200 Subject: [PATCH 19/45] force: Add helper action to clear all data on testnet --- contracts/force/force.hpp | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index eb8ee60..17dbb73 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -136,15 +136,28 @@ class [[eosio::contract("force")]] force : public eosio::contract { vaccount::sig sig, std::optional fee); - // [[eosio::action]] - // void clean() { - // require_auth(_self); - // cleanTable(_self, _self.value, 100); - // cleanTable(_self, _self.value, 100); - // cleanTable(_self, _self.value, 100); - // cleanTable(_self, _self.value, 100); - // cleanTable(_self, _self.value, 100); - // }; + template + void cleanTable(name code, uint64_t account, const uint32_t batchSize){ + T db(code, account); + uint32_t counter = 0; + auto itr = db.begin(); + while (itr != db.end() && counter++ < batchSize) { + itr = db.erase(itr); + } + } + + [[eosio::action]] + void clean() { + require_auth(_self); + cleanTable(_self, _self.value, 100); + cleanTable(_self, _self.value, 100); + cleanTable(_self, _self.value, 100); + cleanTable(_self, _self.value, 100); + cleanTable(_self, _self.value, 100); + cleanTable(_self, _self.value, 100); + cleanTable(_self, _self.value, 100); + }; + private: inline bool has_expired(time_point_sec base_time, uint32_t delay) { From eebf7ad025dd944d188b73cde270891fbc7ba170 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 11 Oct 2023 14:12:03 +0200 Subject: [PATCH 20/45] force: Fix alignment of reservation and submission IDs we are looking for a way to keep submissions in the same order as reservations. previously the code falsely assumed that `available_primary_key` would never re-use keys, but it appears EOS does (it takes highest_primary_key + 1). this was causing collisions between submissions IDs --- contracts/force/force.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 3a83d6a..bcea07a 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -342,6 +342,8 @@ void force::reservetask(uint32_t campaign_id, // reserve suitable task idx to the user uint32_t task_idx = std::max(campaign.tasks_done, user_next_task_idx); + submission_table submission_tbl(_self, _self.value); + // check if there is an earlier expired reservatoin to claim instead auto by_camp = reservation_tbl.get_index<"camp"_n>(); auto by_camp_itr = by_camp.find(campaign_id); @@ -353,7 +355,8 @@ void force::reservetask(uint32_t campaign_id, // omitting so would let him do this repetition twice. task_idx >= by_camp_itr->task_idx) { auto& res = *by_camp_itr; - uint64_t bump_id = reservation_tbl.available_primary_key(); + uint64_t bump_id = std::max(reservation_tbl.available_primary_key(), + submission_tbl.available_primary_key()); // we must re-insert the reservation in order to bump the id reservation_tbl.erase(res); @@ -429,7 +432,9 @@ void force::reservetask(uint32_t campaign_id, } } - uint64_t reservation_id = reservation_tbl.available_primary_key(); + uint64_t reservation_id = std::max(reservation_tbl.available_primary_key(), + submission_tbl.available_primary_key()); + reservation_tbl.emplace(payer, [&](auto& r) { From f7c28c77480bd8d37f856ea1455d0985485e26e7 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 11 Oct 2023 18:33:12 +0200 Subject: [PATCH 21/45] force: Fix issue where user reserves more tasks than cmpaign size it appears to be an offset-by-1 issue, so this is an attempt to remedy that --- contracts/force/force.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index bcea07a..78cdcca 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -336,7 +336,7 @@ void force::reservetask(uint32_t campaign_id, require_vaccount(account_id, pack(params), sig); - eosio::check(!user_last_task_check || campaign.total_tasks > user_last_task.value, + eosio::check(!user_last_task_check || campaign.total_tasks > user_last_task.value - 1, "no more tasks for you"); // reserve suitable task idx to the user @@ -344,7 +344,7 @@ void force::reservetask(uint32_t campaign_id, submission_table submission_tbl(_self, _self.value); - // check if there is an earlier expired reservatoin to claim instead + // check if there is an earlier expired reservation to claim instead auto by_camp = reservation_tbl.get_index<"camp"_n>(); auto by_camp_itr = by_camp.find(campaign_id); if (by_camp_itr != by_camp.end() && From 83e54a039f7493484f3f1aefc0aa76dc614f45fd Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Wed, 8 Nov 2023 18:29:59 +0100 Subject: [PATCH 22/45] force: Fix bug in task reservation indexing using acctaskidx --- contracts/force/force.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 78cdcca..eadd07b 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -317,9 +317,9 @@ void force::reservetask(uint32_t campaign_id, // find the last task idx the user completed in the campaign acctaskidx_table acctaskidx_tbl(_self, _self.value); - auto user_last_task_check = (acctaskidx_tbl.find(acccamp_pk) == acctaskidx_tbl.end()); + auto user_has_last_task = (acctaskidx_tbl.find(acccamp_pk) != acctaskidx_tbl.end()); - if (user_last_task_check) { + if (!user_has_last_task) { acctaskidx_tbl.emplace(payer, [&](auto& i) { @@ -330,13 +330,12 @@ void force::reservetask(uint32_t campaign_id, } auto& user_last_task = acctaskidx_tbl.get(acccamp_pk); - uint32_t user_next_task_idx = user_last_task_check ? 0 : user_last_task.value + 1; + uint32_t user_next_task_idx = !user_has_last_task ? 0 : user_last_task.value + 1; reservetask_params params = {6, user_next_task_idx, campaign_id}; require_vaccount(account_id, pack(params), sig); - - eosio::check(!user_last_task_check || campaign.total_tasks > user_last_task.value - 1, + eosio::check(!user_has_last_task || campaign.total_tasks > user_last_task.value, "no more tasks for you"); // reserve suitable task idx to the user From caef169dc57309aaf016130512a1175c7a873d8a Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Mon, 20 Nov 2023 18:49:20 +0100 Subject: [PATCH 23/45] task: Add script that adds n consecutive DAO cycles to the table --- bb.edn | 3 ++- tasks/effect.clj | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/bb.edn b/bb.edn index 277016c..2fb90d6 100644 --- a/bb.edn +++ b/bb.edn @@ -6,4 +6,5 @@ init effect/init process-cycle effect/process-cycle deploy-mainnet effect/msig-deploy - atp effect/make-atp}} + atp effect/make-atp + create-n-cycles effect/create-n-cycles}} diff --git a/tasks/effect.clj b/tasks/effect.clj index 16bfe0c..df8ae0d 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -162,6 +162,41 @@ json/encode (spit filename))) +(defn create-n-cycles + "Creates an action vector that adds `n` consecutive cycle." + [net n out-file-name] + (let [net (keyword net) + + last-cycle (get-last-cycle net) + + config (get-proposal-config net) + + increase-time #(-> % + java.time.LocalDateTime/parse + (.plusSeconds (:cycle_duration_sec config)) + .toString) + + cycle-start-times + (loop [i 0 + start-time (increase-time (:start_time last-cycle)) + times []] + (if (< i (Integer/parseInt n)) + (recur (inc i) + (increase-time start-time) + (conj times start-time )) + times)) + + new-cycle-actions + (map (partial create-cycle-action (keyword net)) cycle-start-times)] + (create-tx-json + (cons + {:account "effecttokens" + :name "open" + :data {:owner "x.efx" :symbol "4,EFX" :ram_payer "x.efx"} + :authorization [{:actor "x.efx" :permission "active"}]} + new-cycle-actions) + out-file-name))) + (defn process-cycle [id] (try (let [net :mainnet From 2de763fcef9eb5bfb64aaf874280bd99b27968b7 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Mon, 20 Nov 2023 18:49:51 +0100 Subject: [PATCH 24/45] task: Adjust default automatic power up quantity --- tasks/effect.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/effect.clj b/tasks/effect.clj index df8ae0d..b4605e4 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -21,7 +21,7 @@ (declare do-cleos) (def powerup {:jungle4 #(do-cleos :jungle4 "push" "action" "eosio" "powerup" - (str "[\"" payer "\", \"" %1 "\", 1, 100000000000, 100000000000, \"1.0000 EOS\"]") + (str "[\"" payer "\", \"" %1 "\", 1, 50000000000, 50000000000, \"1.0000 EOS\"]") "-p" payer)}) (def deployment From 6a291b8273c3390c4bc502621312817db78b07de Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Sat, 27 Jan 2024 23:18:32 +0100 Subject: [PATCH 25/45] force: Move from vtransfer to transfer in publishbatch --- contracts/force/force.cpp | 32 +++++++++-------- contracts/force/force.hpp | 12 +++---- tests/e2e/force.cljs | 73 +++++++++++++++++---------------------- 3 files changed, 54 insertions(+), 63 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index eadd07b..58bcb75 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -8,7 +8,7 @@ void force::init(eosio::name vaccount_contract, float fee_percentage) { eosio::require_auth(_self); auto settings = _settings.find(settings_pk.value); - _settings.erase(settings); + // _settings.erase(settings); _settings.emplace(_self, [&](auto& s) { @@ -141,7 +141,7 @@ void force::rmbatch(uint32_t id, uint32_t campaign_id, vaccount::sig sig) { if (batch->id > camp->active_batch) { // if the batch has not started, we should empty it, the row // can only be erased when the campaign caught up - eosio::check(camp->num_batches == id, "can only remove active or last batch"); + eosio::check(camp->num_batches == (id + 1), "can only remove active or last batch"); camp_tbl.modify(camp, same_payer, [&](auto& c) { c.num_batches -= 1; c.total_tasks -= batch->num_tasks; }); batch_tbl.erase(batch); @@ -228,14 +228,12 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si if (batch_fee.quantity.amount > 0) { action(permission_level{_self, "xfer"_n}, - settings.vaccount_contract, - "withdraw"_n, - std::make_tuple((uint64_t) settings.force_vaccount_id, + batch.balance.contract, + "transfer"_n, + std::make_tuple(_self, settings.fee_contract, batch_fee, - std::string("batch " + std::to_string(batch_id)), - NULL, - NULL)) + std::string("batch " + std::to_string(batch_id)))) .send(); } } @@ -540,11 +538,15 @@ void force::submittask(uint32_t campaign_id, } } -void force::vtransfer_handler(uint64_t from_id, uint64_t to_id, extended_asset quantity, - std::string memo, vaccount::sig sig, - std::optional fee) { - uint64_t batch_id = std::stoull(memo); - batch_table batch_tbl(_self, _self.value); - auto& batch = batch_tbl.get(batch_id, "batch not found"); - batch_tbl.modify(batch, eosio::same_payer, [&](auto& b) { b.balance += quantity; }); +void force::transfer_handler(eosio::name from, + eosio::name to, + eosio::asset quantity, + std::string memo) { + if (to == get_self()) { + eosio::extended_asset sym(quantity, get_first_receiver()); + uint64_t batch_id = std::stoull(memo); + batch_table batch_tbl(_self, _self.value); + auto& batch = batch_tbl.get(batch_id, "batch not found"); + batch_tbl.modify(batch, eosio::same_payer, [&](auto& b) { b.balance += sym; }); + } } diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 17dbb73..0a96077 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -128,13 +128,11 @@ class [[eosio::contract("force")]] force : public eosio::contract { void payout(uint64_t payment_id, std::optional sig); - [[eosio::on_notify("*::vtransfer")]] - void vtransfer_handler(uint64_t from_id, - uint64_t to_id, - extended_asset quantity, - std::string memo, - vaccount::sig sig, - std::optional fee); + [[eosio::on_notify("*::transfer")]] + void transfer_handler(eosio::name from_id, + eosio::name to_id, + eosio::asset quantity, + std::string memo); template void cleanTable(name code, uint64_t account, const uint32_t batchSize){ diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index f44f2cd..2c29950 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -53,6 +53,7 @@ (println "acc-5 " acc-5) (println "acc-3 " acc-3) (println "acc-2 " acc-2) +(println "token" token-acc) (def accs [["address" (vacc/pub->addr vacc/keypair-pub)] ["name" acc-3] ;; vaccount id = 1 @@ -116,6 +117,12 @@ :code vacc-acc :type "withdraw"} [{:actor force-acc :permission "active"}])) + (hex @@ -312,19 +315,10 @@ :reward {:quantity "3.0000 EFX" :contract token-acc} :qualis [] :payer acc-2 - :sig nil}))) + :sig nil + :paused false}))) - (testing "can edit campaign from pub key hash" - (let [ipfs-hash "QmPoB7nH4Q94C4YxT4rEcQDv3m76HT14wHbUL1gpEa4vWG" - params (pack-editcampaign-params 3 ipfs-hash)] - (hex (.digest (.update (.hash ec) data)))) @@ -825,6 +814,7 @@ ( Date: Sun, 28 Jan 2024 15:13:08 +0100 Subject: [PATCH 26/45] force: Fix transfering of the network fee in publishbatch --- contracts/force/force.cpp | 2 +- tasks/effect.clj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 58bcb75..97cf2a3 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -232,7 +232,7 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si "transfer"_n, std::make_tuple(_self, settings.fee_contract, - batch_fee, + batch_fee.quantity, std::string("batch " + std::to_string(batch_id)))) .send(); } diff --git a/tasks/effect.clj b/tasks/effect.clj index b4605e4..d2d1577 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -525,8 +525,8 @@ (-> deployment net :force :account)) (do-cleos net "set" "action" "permission" (-> deployment net :force :account) - (-> deployment net :vaccount :account) - "vtransfer" + (-> deployment net :token :account) + "transfer" "xfer" "-p" (-> deployment net :force :account)) From f39eccb78ac61e12f72d876ea2d12dd9094c86c6 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Thu, 29 Feb 2024 17:57:37 +0100 Subject: [PATCH 27/45] task: Add helper task to fix a misconfigured feepool cycle --- tasks/effect.clj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tasks/effect.clj b/tasks/effect.clj index d2d1577..789f55b 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -567,3 +567,20 @@ (str "'" tx "'") proposer "-p" proposer)) (catch Exception e (prn e))))) + +(defn fix-feepool [] + (let [fee-amount (* 0.3 326000) + act + [(transfer-efx-action "daoproposals" fee-amount "feepool.efx") + (transfer-efx-action "daoproposals" (* 0.7 326000) "treasury.efx") + {:account "feepool.efx" + :name "setbalance" + :data {:cycle_id 80 :amount (string/replace (str (format "%.4f" fee-amount)) #"\." "")} + :authorization [{:actor "feepool.efx" :permission "active"}]} + {:account "feepool.efx" + :name "setbalance" + :data {:cycle_id 81 :amount "0"} + :authorization [{:actor "feepool.efx" :permission "active"}]}]] + (create-tx-json + act + "fixfeepool.json"))) From 6a81e7efa382ef14316dff91e3203dc3333ce284 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Sat, 6 Apr 2024 00:39:28 +0200 Subject: [PATCH 28/45] force: Remove support for pub key vaccounts --- contracts/force/force.cpp | 61 +++++++++++++++------------------------ contracts/force/force.hpp | 31 +++++++------------- 2 files changed, 34 insertions(+), 58 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 97cf2a3..57bf26a 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -26,14 +26,11 @@ void force::mkcampaign(vaccount::vaddress owner, uint32_t max_task_time, eosio::extended_asset reward, std::vector qualis, - eosio::name payer, - vaccount::sig sig) { + eosio::name payer) { campaign_table camp_tbl(_self, _self.value); uint32_t camp_id = camp_tbl.available_primary_key(); - // TODO: add owner, reward, and qualis to the params - mkcampaign_params params = {9, content}; - std::vector msg_bytes = pack(params); - vaccount::require_auth(msg_bytes, owner, sig); + + vaccount::require_auth(std::vector(), owner, std::nullopt); camp_tbl.emplace(payer, [&](auto& c) @@ -58,14 +55,13 @@ void force::editcampaign(uint32_t campaign_id, bool paused, eosio::extended_asset reward, std::vector qualis, - eosio::name payer, - vaccount::sig sig) { + eosio::name payer) { campaign_table camp_tbl(_self, _self.value); auto& camp = camp_tbl.get(campaign_id, "campaign does not exist"); editcampaign_params params = {10, campaign_id, content, reward, paused, qualis}; - std::vector msg_bytes = pack(params); - vaccount::require_auth(msg_bytes, owner, sig); + + vaccount::require_auth(std::vector(), owner, std::nullopt); camp_tbl.modify(camp, payer, @@ -77,14 +73,12 @@ void force::editcampaign(uint32_t campaign_id, }); } -void force::rmcampaign(uint32_t campaign_id, vaccount::vaddress owner, vaccount::sig sig) { +void force::rmcampaign(uint32_t campaign_id, vaccount::vaddress owner) { campaign_table camp_tbl(_self, _self.value); auto camp_itr = camp_tbl.find(campaign_id); eosio::check(camp_itr != camp_tbl.end(), "campaign does not exist"); - rmcampaign_params params = {11, campaign_id}; - std::vector msg_bytes = pack(params); - vaccount::require_auth(msg_bytes, owner, sig); + vaccount::require_auth(std::vector(), owner, std::nullopt); camp_tbl.erase(camp_itr); } @@ -93,16 +87,13 @@ void force::mkbatch(uint32_t id, uint32_t campaign_id, content content, uint32_t repetitions, - eosio::name payer, - vaccount::sig sig) { + eosio::name payer) { campaign_table camp_tbl(_self, _self.value); auto camp = camp_tbl.require_find(campaign_id, "campaign not found"); eosio::check(id == camp->num_batches, "batch id must be sequential"); - mkbatch_params params = {8, id, campaign_id, content}; - std::vector msg_bytes = pack(params); - vaccount::require_auth(msg_bytes, camp->owner, sig); + vaccount::require_auth(std::vector(), camp->owner, std::nullopt); eosio::check(repetitions < force::MAX_REPETITIONS, "too many repetitions"); batch_table batch_tbl(_self, _self.value); @@ -120,7 +111,7 @@ void force::mkbatch(uint32_t id, camp_tbl.modify(camp, eosio::same_payer, [&](auto& c) { c.num_batches += 1; }); } -void force::rmbatch(uint32_t id, uint32_t campaign_id, vaccount::sig sig) { +void force::rmbatch(uint32_t id, uint32_t campaign_id) { batch_table batch_tbl(_self, _self.value); campaign_table camp_tbl(_self, _self.value); @@ -130,11 +121,7 @@ void force::rmbatch(uint32_t id, uint32_t campaign_id, vaccount::sig sig) { auto batch = batch_tbl.require_find(batch_pk, "batch does not exist"); - rmbatch_params params = {12, id, campaign_id}; - - std::vector msg_bytes = pack(params); - printhex(&msg_bytes[0], msg_bytes.size()); - vaccount::require_auth(msg_bytes, camp->owner, sig); + vaccount::require_auth(std::vector(), camp->owner, std::nullopt); uint32_t batch_tasks_done = (camp->tasks_done - batch->start_task_idx); @@ -187,7 +174,7 @@ void force::cleartasks(uint32_t batch_id, uint32_t campaign_id) { } } -void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig sig) { +void force::publishbatch(uint64_t batch_id, uint32_t num_tasks) { batch_table batch_tbl(_self, _self.value); auto& batch = batch_tbl.get(batch_id, "batch not found"); @@ -197,8 +184,7 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si settings settings = get_settings(); - publishbatch_params params = {17, batch_id}; - vaccount::require_auth(pack(params), camp.owner, sig); + vaccount::require_auth(std::vector(), camp.owner, std::nullopt); eosio::extended_asset task_reward = batch.reward; eosio::extended_asset batch_fee(task_reward.quantity.amount * settings.fee_percentage * @@ -241,8 +227,7 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks, vaccount::sig si void force::reservetask(uint32_t campaign_id, uint32_t account_id, std::optional> quali_assets, - name payer, - vaccount::sig sig) { + name payer) { campaign_table campaign_tbl(_self, _self.value); auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); @@ -330,8 +315,7 @@ void force::reservetask(uint32_t campaign_id, auto& user_last_task = acctaskidx_tbl.get(acccamp_pk); uint32_t user_next_task_idx = !user_has_last_task ? 0 : user_last_task.value + 1; - reservetask_params params = {6, user_next_task_idx, campaign_id}; - require_vaccount(account_id, pack(params), sig); + require_vaccount(account_id); eosio::check(!user_has_last_task || campaign.total_tasks > user_last_task.value, "no more tasks for you"); @@ -350,6 +334,9 @@ void force::reservetask(uint32_t campaign_id, // idx. if the user were to steal future indexis, bumping // acctaskidx would mean the users misses out on tasks, and // omitting so would let him do this repetition twice. + // + // NOTE: this does allow users to complete reptitions twice, + // when they expire? task_idx >= by_camp_itr->task_idx) { auto& res = *by_camp_itr; uint64_t bump_id = std::max(reservation_tbl.available_primary_key(), @@ -444,13 +431,12 @@ void force::reservetask(uint32_t campaign_id, }); } -void force::payout(uint64_t payment_id, std::optional sig) { +void force::payout(uint64_t payment_id) { payment_table payment_tbl(_self, _self.value); auto& payment = payment_tbl.get(payment_id, "payment not found"); - payout_params params = {13, payment.account_id}; - require_vaccount(payment.account_id, pack(params), sig); + require_vaccount(payment.account_id); eosio::check(past_delay(payment.last_submission_time, "payout"), "not past payout delay"); eosio::check(payment.pending.quantity.amount > 0, "nothing to payout"); @@ -474,7 +460,7 @@ void force::submittask(uint32_t campaign_id, uint32_t task_idx, std::string data, uint32_t account_id, - name payer, vaccount::sig sig) { + eosio::name payer) { uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; reservation_table reservation_tbl(_self, _self.value); auto by_acccamp = reservation_tbl.get_index<"acccamp"_n>(); @@ -506,8 +492,7 @@ void force::submittask(uint32_t campaign_id, auto& batch = batch_tbl.get(res->batch_id); - submittask_params params = {5, campaign_id, task_idx, data}; - require_vaccount(account_id, pack(params), sig); + require_vaccount(account_id); if (batch.reward.quantity.amount > 0) { uint64_t payment_id = payment_tbl.available_primary_key(); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 0a96077..215625d 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -69,8 +69,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t max_task_time, eosio::extended_asset reward, std::vector qualis, - eosio::name payer, - vaccount::sig sig); + eosio::name payer); [[eosio::action]] void editcampaign(uint32_t campaign_id, @@ -79,31 +78,26 @@ class [[eosio::contract("force")]] force : public eosio::contract { bool paused, eosio::extended_asset reward, std::vector qualis, - eosio::name payer, - vaccount::sig sig); + eosio::name payer); [[eosio::action]] void rmcampaign(uint32_t campaign_id, - vaccount::vaddress owner, - vaccount::sig sig); + vaccount::vaddress owner); [[eosio::action]] void mkbatch(uint32_t id, uint32_t campaign_id, content content, uint32_t repetitions, - eosio::name payer, - vaccount::sig sig); + eosio::name payer); [[eosio::action]] void publishbatch(uint64_t batch_id, - uint32_t num_tasks, - vaccount::sig sig); + uint32_t num_tasks); [[eosio::action]] void rmbatch(uint32_t id, - uint32_t campaign_id, - vaccount::sig sig); + uint32_t campaign_id); [[eosio::action]] void cleartasks(uint32_t batch_id, @@ -113,20 +107,17 @@ class [[eosio::contract("force")]] force : public eosio::contract { void reservetask(uint32_t campaign_id, uint32_t account_id, std::optional> quali_assets, - eosio::name payer, - vaccount::sig sig); + eosio::name payer); [[eosio::action]] void submittask(uint32_t campaign_id, uint32_t task_idx, std::string data, uint32_t account_id, - eosio::name payer, - vaccount::sig sig); + eosio::name payer); [[eosio::action]] - void payout(uint64_t payment_id, - std::optional sig); + void payout(uint64_t payment_id); [[eosio::on_notify("*::transfer")]] void transfer_handler(eosio::name from_id, @@ -339,11 +330,11 @@ class [[eosio::contract("force")]] force : public eosio::contract { (data)(paid)(submitted_on)) }; - inline void require_vaccount(uint32_t acc_id, std::vector msg, vaccount::sig sig) { + inline void require_vaccount(uint32_t acc_id) { eosio::name vacc_contract = get_settings().vaccount_contract; vaccount::account_table acc_tbl(vacc_contract, vacc_contract.value); vaccount::account acc = acc_tbl.get((uint64_t) acc_id, "account row not found"); - vaccount::require_auth(msg, acc.address, sig); + vaccount::require_auth(std::vector(), acc.address, std::nullopt); }; typedef multi_index<"campaign"_n, campaign> campaign_table; From bdabbf49e1e8adf97816f99438340628e41351f0 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Sat, 6 Apr 2024 13:10:55 +0200 Subject: [PATCH 29/45] force: Fix tests by removing pub key vaccount scenarios --- tests/e2e/force.cljs | 61 +++----------------------------------------- 1 file changed, 3 insertions(+), 58 deletions(-) diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 2c29950..0cee3bf 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -278,33 +278,7 @@ :payer acc-4 :sig nil}))) - (testing "can create campaign from pub key hash" - (let [ipfs-hash "QmPU1fL3oVZGKhGeNSMGxJgY7NsK6MQEpMyZF3CvQwRz4T" - params (pack-mkcampaign-params ipfs-hash)] - ( Date: Sat, 6 Apr 2024 13:11:32 +0200 Subject: [PATCH 30/45] dao: Fix tests to match latest nodeos version there was a change in the output of nodeos RPC calls, where maps changed from "second" to "value" to refer to their value paramter --- tests/e2e/proposals.cljs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/proposals.cljs b/tests/e2e/proposals.cljs index e6c41cd..b2eb5af 100644 --- a/tests/e2e/proposals.cljs +++ b/tests/e2e/proposals.cljs @@ -300,9 +300,9 @@ (let [rows (> rows (filter #(= (% "id") 0)) first)] - (is (= (get-in r ["vote_counts" 0 "value"]) 0)) - (is (= (get-in r ["vote_counts" 1 "value"]) 24042)) - (is (= (get-in r ["vote_counts" 2 "value"]) 37276))) + (is (= (get-in r ["vote_counts" 0 "second"]) 0)) + (is (= (get-in r ["vote_counts" 1 "second"]) 24042)) + (is (= (get-in r ["vote_counts" 2 "second"]) 37276))) ( Date: Thu, 11 Apr 2024 18:04:49 +0200 Subject: [PATCH 31/45] force: Remove deprecated method serialization structs these structs where solely needed to generate signature for BSC addresses, which has now been deprecated --- contracts/force/force.cpp | 2 -- contracts/force/force.hpp | 71 --------------------------------------- 2 files changed, 73 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 57bf26a..1085f46 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -59,8 +59,6 @@ void force::editcampaign(uint32_t campaign_id, campaign_table camp_tbl(_self, _self.value); auto& camp = camp_tbl.get(campaign_id, "campaign does not exist"); - editcampaign_params params = {10, campaign_id, content, reward, paused, qualis}; - vaccount::require_auth(std::vector(), owner, std::nullopt); camp_tbl.modify(camp, diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 215625d..0bed631 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -161,77 +161,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { return time_point_sec(now()) >= (base_time + delay); } - struct reservetask_params { - uint8_t mark; - uint32_t last_task_done; - uint32_t campaign_id; - EOSLIB_SERIALIZE(reservetask_params, (mark)(last_task_done)(campaign_id)); - }; - - struct submittask_params { - uint8_t mark; - uint32_t campaign_id; - uint32_t task_idx; - std::string data; - EOSLIB_SERIALIZE(submittask_params, (mark)(campaign_id)(task_idx)(data)); - }; - - struct mkcampaign_params { - uint8_t mark; - content content; - EOSLIB_SERIALIZE(mkcampaign_params, (mark)(content)); - }; - - struct editcampaign_params { - uint8_t mark; - uint32_t campaign_id; - content content; - eosio::extended_asset reward; - bool paused; - std::vector qualis; - EOSLIB_SERIALIZE(editcampaign_params, (mark)(campaign_id)(content)(reward)(paused)(qualis)); - }; - - struct rmcampaign_params { - uint8_t mark; - uint32_t campaign_id; - EOSLIB_SERIALIZE(rmcampaign_params, (mark)(campaign_id)); - }; - - struct mkbatch_params { - uint8_t mark; - uint32_t id; - uint32_t campaign_id; - content content; - EOSLIB_SERIALIZE(mkbatch_params, (mark)(id)(campaign_id)(content)); - }; - - struct publishbatch_params { - uint8_t mark; - uint64_t batch_id; - EOSLIB_SERIALIZE(publishbatch_params, (mark)(batch_id)); - }; - - struct rmbatch_params { - uint8_t mark; - uint32_t id; - uint32_t campaign_id; - EOSLIB_SERIALIZE(rmbatch_params, (mark)(id)(campaign_id)); - }; - - struct payout_params { - uint8_t mark; - uint32_t payment_id; - EOSLIB_SERIALIZE(payout_params, (mark)(payment_id)); - }; - - struct task_params { - uint8_t mark; - uint64_t task_id; - uint32_t account_id; - EOSLIB_SERIALIZE(task_params, (mark)(task_id)(account_id)); - }; - struct [[eosio::table]] campaign { uint32_t id; uint32_t tasks_done; From eb7e016d9f763e198f45d683007fd8220255f55f Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Thu, 11 Apr 2024 18:19:50 +0200 Subject: [PATCH 32/45] force: Remove `tasks_done` field from `batch` table it is not used anymore in V2 --- contracts/force/force.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 0bed631..823139c 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -186,7 +186,6 @@ class [[eosio::contract("force")]] force : public eosio::contract { content content; eosio::extended_asset balance; uint32_t repetitions; - uint32_t tasks_done; uint32_t num_tasks; uint32_t start_task_idx; eosio::extended_asset reward; From 48cccebf5e67ea5c8008af767b82764a7b6438f4 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Thu, 11 Apr 2024 18:21:57 +0200 Subject: [PATCH 33/45] force: Add `total_submissions` field to `campaign` table --- contracts/force/force.cpp | 32 +++++++++++++++++++------------- contracts/force/force.hpp | 16 +++++++++++++--- tests/e2e/force.cljs | 8 ++++---- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 1085f46..6bb0525 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -36,7 +36,7 @@ void force::mkcampaign(vaccount::vaddress owner, [&](auto& c) { c.id = camp_id; - c.tasks_done = 0; + c.reservations_done = 0; c.active_batch = 0; c.num_batches = 0; c.content = content; @@ -121,7 +121,7 @@ void force::rmbatch(uint32_t id, uint32_t campaign_id) { vaccount::require_auth(std::vector(), camp->owner, std::nullopt); - uint32_t batch_tasks_done = (camp->tasks_done - batch->start_task_idx); + uint32_t batch_tasks_done = (camp->reservations_done - batch->start_task_idx); if (batch->id > camp->active_batch) { // if the batch has not started, we should empty it, the row @@ -200,7 +200,7 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks) { // if this batch becomes the active batch of the // campaign track it starting index if (camp.active_batch == b.id) { - b.start_task_idx = camp.tasks_done; + b.start_task_idx = camp.reservations_done; } }); @@ -212,7 +212,7 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks) { if (batch_fee.quantity.amount > 0) { action(permission_level{_self, "xfer"_n}, - batch.balance.contract, + batch.balance.contract, "transfer"_n, std::make_tuple(_self, settings.fee_contract, @@ -236,7 +236,7 @@ void force::reservetask(uint32_t campaign_id, batch_table batch_tbl(_self, _self.value); auto& batch = batch_tbl.get(batch_pk, "no batches available"); - eosio::check(campaign.tasks_done < batch.start_task_idx + batch.num_tasks, + eosio::check(campaign.reservations_done < batch.start_task_idx + batch.num_tasks, "no more tasks in campaign"); // check qualifications @@ -319,7 +319,7 @@ void force::reservetask(uint32_t campaign_id, "no more tasks for you"); // reserve suitable task idx to the user - uint32_t task_idx = std::max(campaign.tasks_done, user_next_task_idx); + uint32_t task_idx = std::max(campaign.reservations_done, user_next_task_idx); submission_table submission_tbl(_self, _self.value); @@ -387,12 +387,13 @@ void force::reservetask(uint32_t campaign_id, if (has_reps_done_row) repsdone_tbl.erase(repetitions_done); - bool batch_done = ((campaign.tasks_done + 1) >= (batch.start_task_idx + batch.num_tasks)); + bool batch_done = ((campaign.reservations_done + 1) >= + (batch.start_task_idx + batch.num_tasks)); campaign_tbl.modify(campaign, eosio::same_payer, [&](auto& c) { - c.tasks_done += 1; + c.reservations_done += 1; if (batch_done) { c.active_batch += 1; } @@ -408,7 +409,7 @@ void force::reservetask(uint32_t campaign_id, eosio::same_payer, [&](auto &b) { - b.start_task_idx = campaign.tasks_done; + b.start_task_idx = campaign.reservations_done; }); } } @@ -458,7 +459,7 @@ void force::submittask(uint32_t campaign_id, uint32_t task_idx, std::string data, uint32_t account_id, - eosio::name payer) { + eosio::name payer) { uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; reservation_table reservation_tbl(_self, _self.value); auto by_acccamp = reservation_tbl.get_index<"acccamp"_n>(); @@ -488,6 +489,11 @@ void force::submittask(uint32_t campaign_id, s.submitted_on = time_point_sec(now()); }); + auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); + campaign_tbl.modify(campaign, + eosio::same_payer, + [&](auto& c) { c.total_submissions += 1; }); + auto& batch = batch_tbl.get(res->batch_id); require_vaccount(account_id); @@ -522,9 +528,9 @@ void force::submittask(uint32_t campaign_id, } void force::transfer_handler(eosio::name from, - eosio::name to, - eosio::asset quantity, - std::string memo) { + eosio::name to, + eosio::asset quantity, + std::string memo) { if (to == get_self()) { eosio::extended_asset sym(quantity, get_first_receiver()); uint64_t batch_id = std::stoull(memo); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 823139c..93d1374 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -163,7 +163,17 @@ class [[eosio::contract("force")]] force : public eosio::contract { struct [[eosio::table]] campaign { uint32_t id; - uint32_t tasks_done; + /** + * Counter for the reservations that have been created. This + * counter only goes up if all repetitions for a task have been + * reserved. + */ + uint32_t reservations_done; + /** + * Counter for the total submissions that exist for this campaign. + * This counter goes up for every repetition that is submitted. + */ + uint32_t total_submissions; uint32_t total_tasks; uint32_t active_batch; uint32_t num_batches; @@ -176,8 +186,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t primary_key() const { return (uint64_t) id; } - EOSLIB_SERIALIZE(campaign, (id)(tasks_done)(total_tasks)(active_batch)(num_batches)(owner) - (paused)(content)(max_task_time)(reward)(qualis)) + EOSLIB_SERIALIZE(campaign, (id)(reservations_done)(total_submissions)(total_tasks)(active_batch) + (num_batches)(owner)(paused)(content)(max_task_time)(reward)(qualis)) }; struct [[eosio::table]] batch { diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 0cee3bf..be24755 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -508,7 +508,7 @@ :payer acc-3 :quali_assets [] :sig nil})))) - (is (= ( Date: Thu, 11 Apr 2024 18:53:13 +0200 Subject: [PATCH 34/45] task: Add Make targets for running tests and deploying to testnet --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c434f36..f22fe9e 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,6 @@ all: $(WASM) %.abi %.wasm: %.cpp %.hpp $(%-shared.hpp) $(EOS_CC) -o $@ $< -.PHONY: serve-docs clean clean: rm -f $(WASM) $(ABI) @@ -21,3 +20,11 @@ serve-docs: build-docs: jekyll b -s docs + +test-contracts: + npm run lumo e2e.force + +deploy-testnet: + bb deploy jungle4 + +.PHONY: serve-docs clean test-contracts deploy-testnet From f029a15c419a1289d7f9861abfdb49444452fa66 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Sat, 20 Apr 2024 21:46:03 +0200 Subject: [PATCH 35/45] force: Add a type indicator to the submission's `data` field the field can also no longer be optional, as this is not needed with the new split between reservations and submissions. --- contracts/force/force.cpp | 4 ++-- contracts/force/force.hpp | 24 +++++++++++++++++++----- tests/e2e/force.cljs | 19 ++++++++----------- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 6bb0525..c60ec3b 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -457,7 +457,7 @@ void force::payout(uint64_t payment_id) { void force::submittask(uint32_t campaign_id, uint32_t task_idx, - std::string data, + std::pair> data, uint32_t account_id, eosio::name payer) { uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; @@ -483,7 +483,7 @@ void force::submittask(uint32_t campaign_id, s.campaign_id = campaign_id; s.task_idx = task_idx; s.account_id.emplace(account_id); - s.data.emplace(data); + s.data = data; s.batch_id = res->batch_id; s.paid = false; s.submitted_on = time_point_sec(now()); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 93d1374..3fbdc94 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -112,7 +112,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { [[eosio::action]] void submittask(uint32_t campaign_id, uint32_t task_idx, - std::string data, + std::pair> data, uint32_t account_id, eosio::name payer); @@ -174,6 +174,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { * This counter goes up for every repetition that is submitted. */ uint32_t total_submissions; + /** + * Total tasks in this campaign. + */ uint32_t total_tasks; uint32_t active_batch; uint32_t num_batches; @@ -186,8 +189,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t primary_key() const { return (uint64_t) id; } - EOSLIB_SERIALIZE(campaign, (id)(reservations_done)(total_submissions)(total_tasks)(active_batch) - (num_batches)(owner)(paused)(content)(max_task_time)(reward)(qualis)) + EOSLIB_SERIALIZE(campaign, (id)(reservations_done)(total_submissions)(total_tasks) + (active_batch)(num_batches)(owner)(paused)(content)(max_task_time)(reward) + (qualis)) }; struct [[eosio::table]] batch { @@ -257,7 +261,17 @@ class [[eosio::contract("force")]] force : public eosio::contract { std::optional account_id; std::optional content; uint64_t batch_id; - std::optional data; + /** + * The first byte of `data` indicates the type of the submission. + * + * 0 = Flag + * 1 = Raw (normally a UTF-8 encoded string) + * 2 = Ipfs hash (30 bytes, without indicator) + * 3 = ? + * + * Details on the decoding scheme can be foun in the campaign JSON. + */ + std::pair> data; bool paid; eosio::time_point_sec submitted_on; @@ -265,7 +279,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t by_batch() const { return batch_id; } EOSLIB_SERIALIZE(submission, (id)(campaign_id)(task_idx)(account_id)(content)(batch_id) - (data)(paid)(submitted_on)) + (data)(paid)(submitted_on)) }; inline void require_vaccount(uint32_t acc_id) { diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index be24755..1a3fe4c 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -680,24 +680,20 @@ (defn reserve-task-fn [campaign-id account account-id] (tx-as account force-acc "reservetask" {:campaign_id campaign-id :account_id account-id - :quali_assets nil - :payer account - :sig nil})) + :quali_assets nil})) (defn submit-task-fn [campaign-id task-idx account account-id] (tx-as account force-acc "submittask" {:campaign_id campaign-id - :data (str "test data " task-idx) + :data {:first 1 :second "001122"} :account_id account-id - :task_idx task-idx - :payer account - :sig nil})) + :task_idx task-idx})) (async-deftest submit-task (testing "can not submit for other user" (js/console.log (eos/tx-get-console ( Date: Sat, 4 May 2024 18:29:15 +0200 Subject: [PATCH 36/45] force: Remove unused payer from submittask --- contracts/force/force.cpp | 12 ++++++++---- contracts/force/force.hpp | 14 ++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index c60ec3b..c544eef 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -224,8 +224,7 @@ void force::publishbatch(uint64_t batch_id, uint32_t num_tasks) { void force::reservetask(uint32_t campaign_id, uint32_t account_id, - std::optional> quali_assets, - name payer) { + std::optional> quali_assets) { campaign_table campaign_tbl(_self, _self.value); auto& campaign = campaign_tbl.get(campaign_id, "campaign not found"); @@ -244,6 +243,7 @@ void force::reservetask(uint32_t campaign_id, auto vacc = vaccount::get_vaccount(settings.vaccount_contract, account_id); bool is_eos = vaccount::is_eos(vacc->address); eosio::name asset_owner = is_eos ? vaccount::get_name(vacc->address) : _self; + eosio::name payer = asset_owner; auto acc_assets_tbl = atomicassets::get_assets(asset_owner); auto force_assets_tbl = atomicassets::get_assets(_self); @@ -458,8 +458,7 @@ void force::payout(uint64_t payment_id) { void force::submittask(uint32_t campaign_id, uint32_t task_idx, std::pair> data, - uint32_t account_id, - eosio::name payer) { + uint32_t account_id) { uint64_t acccamp_pk = (uint64_t{account_id} << 32) | campaign_id; reservation_table reservation_tbl(_self, _self.value); auto by_acccamp = reservation_tbl.get_index<"acccamp"_n>(); @@ -475,6 +474,11 @@ void force::submittask(uint32_t campaign_id, batch_table batch_tbl(_self, _self.value); campaign_table campaign_tbl(_self, _self.value); + settings settings = get_settings(); + auto vacc = vaccount::get_vaccount(settings.vaccount_contract, account_id); + eosio::check(vaccount::is_eos(vacc->address), "wrong vaccount type"); + eosio::name payer = vaccount::get_name(vacc->address); + reservation_tbl.erase(*res); submission_tbl.emplace(payer, [&](auto& s) diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 3fbdc94..1d3f3d5 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -106,24 +106,22 @@ class [[eosio::contract("force")]] force : public eosio::contract { [[eosio::action]] void reservetask(uint32_t campaign_id, uint32_t account_id, - std::optional> quali_assets, - eosio::name payer); + std::optional> quali_assets); [[eosio::action]] void submittask(uint32_t campaign_id, uint32_t task_idx, - std::pair> data, - uint32_t account_id, - eosio::name payer); + std::pair> data, + uint32_t account_id); [[eosio::action]] void payout(uint64_t payment_id); [[eosio::on_notify("*::transfer")]] void transfer_handler(eosio::name from_id, - eosio::name to_id, - eosio::asset quantity, - std::string memo); + eosio::name to_id, + eosio::asset quantity, + std::string memo); template void cleanTable(name code, uint64_t account, const uint32_t batchSize){ From e884792188bbcdb9986a99507cee2704a0267254 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Tue, 14 May 2024 23:24:16 +0200 Subject: [PATCH 37/45] force: Allow users to work ahead of the campaigns active batch To achieve this, we have to keep track of the active batch for each user inside `acctaskidx`. This means existing `acctaskidx` data will be corrupted and must be purged. --- contracts/force/force.cpp | 45 ++++++++++++---- contracts/force/force.hpp | 1 + tests/e2e/force.cljs | 106 +++++++++++++++++++++++++++++++++----- 3 files changed, 130 insertions(+), 22 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index c544eef..205e4ee 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -230,14 +230,6 @@ void force::reservetask(uint32_t campaign_id, eosio::check(!campaign.paused, "campaign is paused"); - uint32_t batch_id = campaign.active_batch; - uint64_t batch_pk = (uint64_t{campaign_id} << 32) | batch_id; - batch_table batch_tbl(_self, _self.value); - auto& batch = batch_tbl.get(batch_pk, "no batches available"); - - eosio::check(campaign.reservations_done < batch.start_task_idx + batch.num_tasks, - "no more tasks in campaign"); - // check qualifications settings settings = get_settings(); auto vacc = vaccount::get_vaccount(settings.vaccount_contract, account_id); @@ -298,14 +290,17 @@ void force::reservetask(uint32_t campaign_id, // find the last task idx the user completed in the campaign acctaskidx_table acctaskidx_tbl(_self, _self.value); - auto user_has_last_task = (acctaskidx_tbl.find(acccamp_pk) != acctaskidx_tbl.end()); + auto our_task_idx = acctaskidx_tbl.find(acccamp_pk); + auto user_has_last_task = (our_task_idx != acctaskidx_tbl.end()); + // if this is the first task done by this user, insert the task index if (!user_has_last_task) { acctaskidx_tbl.emplace(payer, [&](auto& i) { i.campaign_id = campaign_id; i.account_id = account_id; + i.batch_idx = campaign.active_batch; i.value = 0; }); } @@ -323,6 +318,38 @@ void force::reservetask(uint32_t campaign_id, submission_table submission_tbl(_self, _self.value); + // fetch active batch information + uint32_t batch_id = (user_has_last_task && our_task_idx->batch_idx > campaign.active_batch) ? + our_task_idx->batch_idx : campaign.active_batch; + + uint64_t batch_pk = (uint64_t{campaign_id} << 32) | batch_id; + batch_table batch_tbl(_self, _self.value); + auto& batch = batch_tbl.get(batch_pk, "no batches available"); + + eosio::check(campaign.reservations_done < batch.start_task_idx + batch.num_tasks, + "no more tasks in campaign"); + + // check if this task index lays in the next batch + if (task_idx >= (batch.start_task_idx + batch.num_tasks)) { + uint64_t next_batch_pk = (uint64_t{campaign_id} << 32) | (batch_id + 1); + auto next_batch = batch_tbl.find(next_batch_pk); + batch_pk = next_batch_pk; + eosio::check(next_batch != batch_tbl.end(), "next batch not available"); + + batch_tbl.modify(*next_batch, + eosio::same_payer, + [&](auto &b) + { + b.start_task_idx = task_idx; + }); + acctaskidx_tbl.modify(*our_task_idx, + eosio::same_payer, + [&](auto &i) + { + i.batch_idx = batch_id + 1; + }); + } + // check if there is an earlier expired reservation to claim instead auto by_camp = reservation_tbl.get_index<"camp"_n>(); auto by_camp_itr = by_camp.find(campaign_id); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 1d3f3d5..6b53a01 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -208,6 +208,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { struct [[eosio::table]] acctaskidx { uint32_t account_id; uint32_t campaign_id; + uint32_t batch_idx; uint32_t value; uint64_t primary_key() const { return (uint64_t{account_id} << 32) | campaign_id; } }; diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 1a3fe4c..73aebb2 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -230,13 +230,6 @@ (.asUint8Array (doto (new (.-SerialBuffer Serialize)) (.push 13) (.pushUint32 acc-id)))) -(defn pack-reservetask-params [last-task-done camp-id] - (.asUint8Array - (doto (new (.-SerialBuffer Serialize)) - (.push 6) - (.pushUint32 last-task-done) - (.pushUint32 camp-id)))) - (defn pack-submittask-params [sub-id data] (.asUint8Array (doto (new (.-SerialBuffer Serialize)) @@ -688,6 +681,23 @@ :account_id account-id :task_idx task-idx})) +(defn make-campaign-fn [] + (tx-as acc-2 force-acc "mkcampaign" + {:owner ["name" acc-2] + :content {:field_0 0 :field_1 vacc/hash160-1} + :max_task_time 10 + :reward {:quantity "1.0000 EFX" :contract token-acc} + :qualis [] + :payer acc-2})) + +(defn make-batch-fn [campaign-id id reps] + (tx-as acc-2 force-acc "mkbatch" + {:id id + :campaign_id campaign-id + :content {:field_0 0 :field_1 vacc/hash160-1} + :repetitions reps + :payer acc-2})) + (async-deftest submit-task (testing "can not submit for other user" (js/console.log @@ -753,10 +763,82 @@ :sig nil})) ( Date: Fri, 17 May 2024 19:03:22 +0200 Subject: [PATCH 38/45] force: Remove unused `content` field from `submission` table --- contracts/force/force.hpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 6b53a01..5c71122 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -258,17 +258,15 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t campaign_id; uint32_t task_idx; std::optional account_id; - std::optional content; uint64_t batch_id; /** * The first byte of `data` indicates the type of the submission. * - * 0 = Flag - * 1 = Raw (normally a UTF-8 encoded string) - * 2 = Ipfs hash (30 bytes, without indicator) - * 3 = ? + * 0 = Empty + * 1 = Raw (IPFS hash, or a UTF-8 encoded string, or different) + * 2+ = ? * - * Details on the decoding scheme can be foun in the campaign JSON. + * Details on the decoding scheme can be found in the campaign JSON. */ std::pair> data; bool paid; @@ -277,7 +275,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t primary_key() const { return id; } uint64_t by_batch() const { return batch_id; } - EOSLIB_SERIALIZE(submission, (id)(campaign_id)(task_idx)(account_id)(content)(batch_id) + EOSLIB_SERIALIZE(submission, (id)(campaign_id)(task_idx)(account_id)(batch_id) (data)(paid)(submitted_on)) }; From 79361a47d0a0acbcda0cf521f18742f39288705a Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 17 May 2024 19:25:45 +0200 Subject: [PATCH 39/45] force: Store `batch_id` as `uint32` in submission and reservation before we were storing a uint64 (concatenation of campaign_id and batch_idx). but as campaign_id is already present, we can save space by just storing batch_idx. it is also more convenient for clients, as there is no need to disect the batch_id in order to find the batch index. --- contracts/force/force.cpp | 36 +++++++++++++++++++----------------- contracts/force/force.hpp | 9 +++++---- tests/e2e/force.cljs | 4 ++-- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/contracts/force/force.cpp b/contracts/force/force.cpp index 205e4ee..0de85cb 100644 --- a/contracts/force/force.cpp +++ b/contracts/force/force.cpp @@ -300,7 +300,7 @@ void force::reservetask(uint32_t campaign_id, { i.campaign_id = campaign_id; i.account_id = account_id; - i.batch_idx = campaign.active_batch; + i.batch_idx = campaign.active_batch; i.value = 0; }); } @@ -334,20 +334,21 @@ void force::reservetask(uint32_t campaign_id, uint64_t next_batch_pk = (uint64_t{campaign_id} << 32) | (batch_id + 1); auto next_batch = batch_tbl.find(next_batch_pk); batch_pk = next_batch_pk; + batch_id = batch_id + 1; eosio::check(next_batch != batch_tbl.end(), "next batch not available"); batch_tbl.modify(*next_batch, - eosio::same_payer, - [&](auto &b) - { - b.start_task_idx = task_idx; - }); + eosio::same_payer, + [&](auto &b) + { + b.start_task_idx = task_idx; + }); acctaskidx_tbl.modify(*our_task_idx, - eosio::same_payer, - [&](auto &i) - { - i.batch_idx = batch_id + 1; - }); + eosio::same_payer, + [&](auto &i) + { + i.batch_idx = batch_id; + }); } // check if there is an earlier expired reservation to claim instead @@ -375,7 +376,7 @@ void force::reservetask(uint32_t campaign_id, r.id = bump_id; r.task_idx = res.task_idx; r.account_id = account_id; - r.batch_id = batch_pk; + r.batch_idx = batch_id; r.reserved_on = time_point_sec(now()); r.campaign_id = campaign_id; }); @@ -451,7 +452,7 @@ void force::reservetask(uint32_t campaign_id, r.id = reservation_id; r.task_idx = task_idx; r.account_id.emplace(account_id); - r.batch_id = batch_pk; + r.batch_idx = batch_id; r.reserved_on = time_point_sec(now()); r.campaign_id = campaign_id; }); @@ -515,7 +516,7 @@ void force::submittask(uint32_t campaign_id, s.task_idx = task_idx; s.account_id.emplace(account_id); s.data = data; - s.batch_id = res->batch_id; + s.batch_idx = res->batch_idx; s.paid = false; s.submitted_on = time_point_sec(now()); }); @@ -525,14 +526,15 @@ void force::submittask(uint32_t campaign_id, eosio::same_payer, [&](auto& c) { c.total_submissions += 1; }); - auto& batch = batch_tbl.get(res->batch_id); + uint64_t batch_pk = (uint64_t{campaign_id} << 32) | res->batch_idx; + auto& batch = batch_tbl.get(batch_pk); require_vaccount(account_id); if (batch.reward.quantity.amount > 0) { uint64_t payment_id = payment_tbl.available_primary_key(); - uint128_t payment_sk = (uint128_t{res->batch_id} << 64) | (uint64_t{account_id} << 32); + uint128_t payment_sk = (uint128_t{batch_pk} << 64) | (uint64_t{account_id} << 32); auto payment_idx = payment_tbl.get_index<"accbatch"_n>(); auto payment = payment_idx.find(payment_sk); @@ -542,7 +544,7 @@ void force::submittask(uint32_t campaign_id, { p.id = payment_id; p.account_id = account_id; - p.batch_id = res->batch_id; + p.batch_id = res->batch_idx; p.pending = batch.reward; p.last_submission_time = time_point_sec(now()); }); diff --git a/contracts/force/force.hpp b/contracts/force/force.hpp index 5c71122..7fe1ab9 100644 --- a/contracts/force/force.hpp +++ b/contracts/force/force.hpp @@ -240,7 +240,7 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint64_t id; uint32_t task_idx; std::optional account_id; - uint64_t batch_id; + uint32_t batch_idx; eosio::time_point_sec reserved_on; uint32_t campaign_id; @@ -258,7 +258,8 @@ class [[eosio::contract("force")]] force : public eosio::contract { uint32_t campaign_id; uint32_t task_idx; std::optional account_id; - uint64_t batch_id; + uint32_t batch_idx; + /** * The first byte of `data` indicates the type of the submission. * @@ -273,9 +274,9 @@ class [[eosio::contract("force")]] force : public eosio::contract { eosio::time_point_sec submitted_on; uint64_t primary_key() const { return id; } - uint64_t by_batch() const { return batch_id; } + uint64_t by_batch() const { return (uint64_t{campaign_id} << 32) | batch_idx; } - EOSLIB_SERIALIZE(submission, (id)(campaign_id)(task_idx)(account_id)(batch_id) + EOSLIB_SERIALIZE(submission, (id)(campaign_id)(task_idx)(account_id)(batch_idx) (data)(paid)(submitted_on)) }; diff --git a/tests/e2e/force.cljs b/tests/e2e/force.cljs index 73aebb2..a2bf755 100644 --- a/tests/e2e/force.cljs +++ b/tests/e2e/force.cljs @@ -793,7 +793,7 @@ ( Date: Fri, 28 Jun 2024 00:47:42 +0200 Subject: [PATCH 40/45] task: Update deployment hash of tasks.efx --- tasks/effect.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/effect.clj b/tasks/effect.clj index 789f55b..07bf6ac 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -54,7 +54,7 @@ :dao {:account "theeffectdao" :path "contracts/dao" :hash "22814f2c83433da8e929533e4b46bb3be95bc8826c4e4bcc62242f05b4cd2744"} - :force {:account "force.efx" + :force {:account "tasks.efx" :path "contracts/force" :hash "17e1dab4a77306e236b6f879bb059cd162e97b204e8e530daac8c7666717313b"}}}) From ec1a19ab504f847abc263813edf3f628be3e8b20 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 28 Jun 2024 21:27:32 +0200 Subject: [PATCH 41/45] task: Update contract build instructions for antelope/cdt:3.1.0 --- contracts/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/README.md b/contracts/README.md index 3292466..ffb714c 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -20,7 +20,8 @@ It's possible to use a docker container instead of installing **eosio.cdt** locally: ```bash -EOS_CC="docker run --rm -it -v $(pwd):/app -w /app effectai/eosio-cdt:v1.5.0 eosio-cpp" ABI_CC="docker run --rm -it -v $(pwd):/app -w /app effectai/eosio-cdt:v1.5.0 eosio-abigen" make all +export EOS_CC="sudo docker run --rm -v $(pwd):/build -w /build antelope/cdt:3.1.0 cdt-cpp" +make all ``` ## 🚚 Deploying From 3befbcd554dcf9e0b681ed88eee12c759935f079 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 28 Jun 2024 21:28:47 +0200 Subject: [PATCH 42/45] task: Fix `get-last-cycle` method --- tasks/effect.clj | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tasks/effect.clj b/tasks/effect.clj index 789f55b..08e7768 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -95,12 +95,15 @@ :rows))) (defn get-last-cycle [net] - (let [prop-acc (-> deployment net :proposals :account)] - (-> (cleos net "get" "table" prop-acc prop-acc "cycle" "-l" "1" "-r") - :out - (json/decode true) - :rows - first))) + (let [prop-acc (-> deployment net :proposals :account) + cycles + (-> (cleos net "get" "table" prop-acc prop-acc "cycle" "-l" "20" "-r") + :out + (json/decode true) + :rows)] + (->> cycles + (filter #(= (:state %) 1)) + first))) (defn get-proposal-config [net] (let [prop-acc (-> deployment net :proposals :account)] From 23844916bdb108dbef6b96e82f695eb48a0c9d46 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Fri, 28 Jun 2024 23:37:50 +0200 Subject: [PATCH 43/45] task: Update cycle processing script fix detecting of the last active cycle, and rely on pre-creating cycle entries --- tasks/effect.clj | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tasks/effect.clj b/tasks/effect.clj index e15a89f..c548c57 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -100,10 +100,17 @@ (-> (cleos net "get" "table" prop-acc prop-acc "cycle" "-l" "20" "-r") :out (json/decode true) - :rows)] - (->> cycles - (filter #(= (:state %) 1)) - first))) + :rows) + id (->> cycles + (filter #(= (:state %) 1)) + first + :id + inc)] + (-> (cleos net "get" "table" prop-acc prop-acc "cycle" "-U" (str id) "-L" (str id)) + :out + (json/decode true) + :rows + first))) (defn get-proposal-config [net] (let [prop-acc (-> deployment net :proposals :account)] @@ -225,7 +232,10 @@ :name "open" :data {:owner "x.efx" :symbol "4,EFX" :ram_payer "x.efx"} :authorization [{:actor "x.efx" :permission "active"}]} - (create-cycle-action net new-cycle-start) + ;; we do not create a new cycle each + ;; time, as it's done in batches now (see + ;; `create-n-cycles`) + ;; (create-cycle-action net new-cycle-start) (transfer-efx-action "daoproposals" (* 0.3 funds-left) "feepool.efx") (transfer-efx-action "daoproposals" (* 0.7 funds-left) "treasury.efx") {:account "daoproposals" From e1331279399cf5e46e62100953599951ca082f90 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Sat, 2 Nov 2024 12:00:07 +0100 Subject: [PATCH 44/45] task: Update manifest to include babashka --- manifest.scm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manifest.scm b/manifest.scm index 451204f..aa0b689 100644 --- a/manifest.scm +++ b/manifest.scm @@ -1,6 +1,7 @@ (use-modules (gnu packages gcc) - (gnu packages node)) + (gnu packages node) + (nongnu packages clojure)) (packages->manifest - (list (list gcc "lib") node gnu-make)) + (list (list gcc "lib") node gnu-make babashka)) From 9ab3f547b83814bb6cc21d8d42757a534790b0b4 Mon Sep 17 00:00:00 2001 From: Jesse Eisses Date: Sat, 2 Nov 2024 12:00:44 +0100 Subject: [PATCH 45/45] ci: Switch deployment from jungle3 to jungle4 --- Makefile | 1 - tasks/effect.clj | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f22fe9e..bebf97e 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,6 @@ all: $(WASM) %.abi %.wasm: %.cpp %.hpp $(%-shared.hpp) $(EOS_CC) -o $@ $< - clean: rm -f $(WASM) $(ABI) diff --git a/tasks/effect.clj b/tasks/effect.clj index c548c57..87ca699 100644 --- a/tasks/effect.clj +++ b/tasks/effect.clj @@ -16,7 +16,7 @@ (def rpcs {:jungle4 "https://jungle4.cryptolions.io:443" :mainnet "https://eos.greymass.com"}) -(def wallet-pass (slurp "jungle3-password.txt")) +(def wallet-pass (slurp "jungle4-password.txt")) (declare do-cleos) @@ -140,8 +140,6 @@ (assoc :actions actions) json/encode))) - - (defn extract-quantity [quantity] (Float/parseFloat (->> quantity (re-seq #"(\d+\.\d+) EFX") first second))) @@ -274,7 +272,7 @@ (defn unlock [] (shell "cleos" "wallet" "lock_all") - (shell "cleos" "wallet" "unlock" "-n" "jungle3" "--password" wallet-pass)) + (shell "cleos" "wallet" "unlock" "-n" "jungle4" "--password" wallet-pass)) (defn get-account [net acc] (json/decode (cleos net "get" "account" acc "--json") true))