include "List.aes" include "Pair.aes" namespace Miner = type package_code = string record package = { daily_cap : int, price : int} record aggregated_package = { daily_cap : int, count : int} record worker = { daily_cap : int, can_withdraw_payout : bool, packages : map(package_code, aggregated_package), joined_pool_tmst : int} record transfer_packs = { worker : address, packages_to_move : list(string * int), new_address : address } datatype approvable_action = Transfer(transfer_packs) function new_package(price : int, cap : int) : package = {daily_cap = cap, price = price} function claim(ps : list(package_code * (package * int)), joined_tmst : int) : worker = let daily_cap = daily_cap_from_packs_list(ps) let packs : map(package_code, aggregated_package) = List.foldl( (accum, t) => let pack_id = Pair.fst(t) let (pack, cnt) = Pair.snd(t) let val = switch(Map.lookup(pack_id, accum)) None => {daily_cap = pack.daily_cap, count = cnt} Some(v) => v{count = v.count + cnt} accum{[pack_id] = val}, {}, ps) {daily_cap = daily_cap, can_withdraw_payout = false, packages = packs, joined_pool_tmst = joined_tmst} function split_packages(w : worker, split : transfer_packs) : worker * worker = let (packages_left, packages_collected) = List.foldl( (accum, p) => let accum_left = Pair.fst(accum) let accum_collected = Pair.snd(accum) let code = Pair.fst(p) let count = Pair.snd(p) switch(Map.lookup(code, accum_left)) None => abort("Does not own enough packages") Some(owned_packs) => let left_packs = owned_packs.count - count require(left_packs > -1, "Does not own enough packages") (accum_left{[code] = owned_packs{count = left_packs}}, (code, owned_packs{count = count}) :: accum_collected), (w.packages, []), split.packages_to_move) let daily_cap_delta = List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, packages_collected)) let new_w = {daily_cap = daily_cap_delta, can_withdraw_payout = false, packages = Map.from_list(packages_collected), joined_pool_tmst = Chain.block_height} require(Map.size(new_w.packages) == List.length(packages_collected), "Do not split counts of the same package code") (w{daily_cap = w.daily_cap - daily_cap_delta, packages = packages_left}, new_w) function merge_workers(w1 : worker, w2: worker) = let packages = List.foldl( (accum, t) => let code = Pair.fst(t) let aggr_pack = Pair.snd(t) let updated_pack = switch(Map.lookup(code, accum)) None => aggr_pack Some(p) => p{count = p.count + aggr_pack.count} accum{[code] = updated_pack}, w1.packages, Map.to_list(w2.packages)) // if one of them is allowed to withdraw, so is the resulting new account let oldest_tmst = switch(w1.joined_pool_tmst < w2.joined_pool_tmst) true => w1.joined_pool_tmst false => w2.joined_pool_tmst let can_withdraw_payout = w1.can_withdraw_payout || w2.can_withdraw_payout let daily_cap = daily_cap_from_packages(packages) {daily_cap = daily_cap, can_withdraw_payout = can_withdraw_payout, packages = packages, joined_pool_tmst = oldest_tmst} function daily_cap_from_packs_list(ps : list(package_code * (package * int))) = List.sum(List.map((t) => Pair.fst(Pair.snd(t)).daily_cap * Pair.snd(Pair.snd(t)), ps)) function daily_cap_from_packages(ps : map(package_code, aggregated_package)) = List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, Map.to_list(ps))) contract interface Data = stateful entrypoint set_pool : (address) => unit stateful entrypoint add : (address, Miner.worker) => unit stateful entrypoint remove : (address) => unit payable stateful entrypoint give_rewards : (list(address * int)) => unit entrypoint balance : (address) => int entrypoint assert_is_payable : (address) => unit stateful entrypoint payout : () => unit stateful entrypoint payout_without_payable_check : (address) => unit entrypoint all_balances : () => list(address * int) entrypoint all_daily_caps : () => list(address * int) entrypoint all : () => list(address) entrypoint all_detailed : () => map(address, Miner.worker) entrypoint member : (address) => bool entrypoint get : (address) => Miner.worker stateful entrypoint rename : (address, address) => unit stateful entrypoint make_payable : (address) => unit stateful entrypoint make_non_payable : (address) => unit stateful entrypoint split_packages : (Miner.transfer_packs) => unit entrypoint all_balances_and_daily_caps : () => list(address * int * int) contract interface Pool = entrypoint leader : () => address stateful entrypoint enroll : (address, Miner.worker) => unit entrypoint member : (address) => bool stateful entrypoint remove : (address) => unit entrypoint get : (address) => Miner.worker stateful entrypoint set_locked : (bool) => unit stateful entrypoint set_leader : (address) => unit entrypoint can_be_destroyed : () => bool entrypoint info : () => address * string * address * string * string * string * string * list(string) stateful entrypoint make_payable : (address) => unit stateful entrypoint make_non_payable : (address) => unit entrypoint assert_worker_is_payable : (address) => unit stateful entrypoint force_payout : (address) => unit stateful entrypoint change_worker_address : (address, address) => unit entrypoint balance : (address) => int stateful entrypoint set_data_ct : (Data) => unit stateful entrypoint move_data_and_coins_to_new_pool : (Pool) => Data payable entrypoint receive_coins : () => unit stateful entrypoint evacuate_coins : (int, address) => unit stateful entrypoint split_packages : (Miner.transfer_packs) => unit include "Set.aes" include "List.aes" include "Pair.aes" main contract PoolInstance:Pool = datatype pool_status = OPEN | LOCKED | MIGRATED datatype event = Enroll(address) | Remove(address) record state = { status : pool_status, leader : address, main_contract : address, connect_addresses : Set.set(string), // IP addresses and ports leader_name : string, leader_url: string, leader_avatar_url : string, leader_description : string, data_ct : Data } entrypoint init(main_contract : address, leader : address, data_ct : Data) = {status = OPEN, leader = leader, main_contract = main_contract, connect_addresses = Set.new(), leader_name = "", leader_url = "", leader_avatar_url = "", leader_description = "", data_ct = data_ct} entrypoint status() = switch(state.status) OPEN => "open" LOCKED => "locked" MIGRATED => "migrated" entrypoint info() = (Contract.address, status(), state.leader, state.leader_name, state.leader_url, state.leader_avatar_url, state.leader_description, Set.to_list(state.connect_addresses)) entrypoint data_contract() = state.data_ct entrypoint main_contract() = state.main_contract entrypoint can_be_destroyed() : bool = (empty() && state.status == LOCKED && Contract.balance == 0) || state.status == MIGRATED entrypoint leader() = state.leader entrypoint miners() = state.data_ct.all() entrypoint miners_detailed() = state.data_ct.all_detailed() entrypoint miner_balances() : list(address * int) = state.data_ct.all_balances() entrypoint miner_daily_caps() : list(address * int) = state.data_ct.all_daily_caps() entrypoint miner_balances_and_caps() : list(address * int * int) = state.data_ct.all_balances_and_daily_caps() entrypoint empty() : bool = List.is_empty(miners()) stateful entrypoint enroll(worker_address : address, worker : Miner.worker) = assert_caller_is_main_contract() require(state.status == OPEN, "Pool is locked, can not join it") Chain.event(Enroll(worker_address)) add_worker(worker_address, worker) entrypoint member(worker_address : address) : bool = state.data_ct.member(worker_address) /* deletes a worker from the pool if present. Currently the accumulated coins remain in pool. */ stateful entrypoint remove(worker_address : address) = assert_caller_is_main_contract() Chain.event(Remove(worker_address)) state.data_ct.remove(worker_address) entrypoint get(worker_address : address) = state.data_ct.get(worker_address) // this can overwrite a MIGRATED state stateful entrypoint set_locked(val : bool) = assert_caller_is_main_contract() let s = switch(val) true => LOCKED false => OPEN put(state{status = s}) stateful entrypoint set_leader(new_leader : address) = assert_caller_is_main_contract() put(state{leader = new_leader}) stateful entrypoint add_connect_address(conn_address : string) = assert_leader() put(state{connect_addresses = Set.insert(conn_address, state.connect_addresses)}) stateful entrypoint rm_connect_address(conn_address : string) = assert_leader() put(state{connect_addresses = Set.delete(conn_address, state.connect_addresses)}) stateful entrypoint set_name(name : string) = assert_leader() put(state{leader_name = name}) stateful entrypoint set_url(url : string) = assert_leader() put(state{leader_url = url}) stateful entrypoint set_avatar_url(avatar_url : string) = assert_leader() put(state{leader_avatar_url = avatar_url}) stateful entrypoint set_description(description : string) = assert_leader() put(state{leader_description = description}) /* NB: does not take into account one's daily limits! */ stateful entrypoint simply_reward_work(amounts : list(address * int), _ : string) = assert_leader() let total_reward = List.sum(List.map((t) => Pair.snd(t), amounts)) require(Contract.balance >= total_reward, "Not enough GAJU for that reward") state.data_ct.give_rewards(amounts, value = total_reward) payable entrypoint receive_coins() = () // for Eureka entrypoint balance(addr : address) = state.data_ct.balance(addr) entrypoint daily_cap(addr : address) = let worker = get(addr) worker.daily_cap stateful entrypoint payout() : unit = state.data_ct.payout() stateful entrypoint force_payout(worker_addr : address) = assert_caller_is_main_contract() state.data_ct.payout_without_payable_check(worker_addr) stateful entrypoint change_worker_address(old_addr : address, new_addr : address) = assert_caller_is_main_contract() state.data_ct.rename(old_addr, new_addr) stateful entrypoint evacuate_coins(amount : int, safeheaven : address) = assert_caller_is_main_contract() Chain.spend(safeheaven, amount) stateful entrypoint make_payable(worker_address : address) = assert_caller_is_main_contract() state.data_ct.make_payable(worker_address) stateful entrypoint make_non_payable(worker_address : address) = assert_caller_is_main_contract() state.data_ct.make_non_payable(worker_address) stateful entrypoint set_data_ct(data_ct : Data) = assert_caller_is_main_contract() put(state{data_ct = data_ct}) stateful entrypoint move_data_and_coins_to_new_pool(new_pool : Pool) = assert_caller_is_main_contract() state.data_ct.set_pool(new_pool.address) new_pool.receive_coins(value = Contract.balance) put(state{status = MIGRATED}) state.data_ct entrypoint assert_worker_is_payable(worker_address : address) = state.data_ct.assert_is_payable(worker_address) stateful entrypoint split_packages(split : Miner.transfer_packs) = assert_caller_is_main_contract() state.data_ct.split_packages(split) // private functions function assert_caller_is_main_contract() = require(Call.caller == state.main_contract, "Call it through the main pool contract") function assert_leader() = require(Call.origin == state.leader, "Must be called by the leader") stateful function add_worker(worker_address, worker) = state.data_ct.add(worker_address, worker)