diff --git a/README.md b/README.md index b1df819..0e58aaf 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,9 @@ lein less auto ``` ### Solidity compilation -Invoke `build-contracts` Leiningen task to compile Solidity files into Java classes: +Compile Solidity files into Java classes with: ``` -lein build-contracts +cd contracts && ./build.sh ``` ### Clojure app without REPL diff --git a/contracts/README.md b/contracts/README.md index f92dc0e..59015c9 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -2,8 +2,8 @@ This directory contains all the underlying smart contracts used by the OpenBounty platform. -- A script `contracts/build.sh` is part of this repository and can be used to -compile the contracts and copy Java interfaces into `src/java/`. +-- A script `build.sh` is part of this directory and can be used to +-compile the contracts and copy Java interfaces into `src/java/`. In order to run the script the following dependencies have to be met: diff --git a/contracts/build.sh b/contracts/build.sh index 7b00d8c..99e4991 100755 --- a/contracts/build.sh +++ b/contracts/build.sh @@ -1,12 +1,18 @@ #!/bin/bash -eu -SOLC=$(which solc) -WEB3J=$(which web3j) +function print_dependency_message +{ + echo "error: " $1 "must already be installed!" +} + +SOLC=$(which solc) || print_dependency_message "solc" + +WEB3J=$(which web3j) || print_dependency_message "web3" rm -f resources/contracts/*.{abi,bin} # compile contracts -for f in contracts/{TokenReg,MultiSigTokenWallet*}.sol; do +for f in {TokenReg,MultiSigTokenWallet*}.sol; do $SOLC $f --overwrite --bin --abi --optimize -o resources/contracts done diff --git a/resources/public/ic-check-circle-black-24dp-1x.png b/resources/public/ic-check-circle-black-24dp-1x.png new file mode 100644 index 0000000..a2caa18 Binary files /dev/null and b/resources/public/ic-check-circle-black-24dp-1x.png differ diff --git a/resources/public/ic-check-circle-black-24dp-2x.png b/resources/public/ic-check-circle-black-24dp-2x.png new file mode 100644 index 0000000..86bf38e Binary files /dev/null and b/resources/public/ic-check-circle-black-24dp-2x.png differ diff --git a/resources/public/ic-more-vert-black-24dp-1x.png b/resources/public/ic-more-vert-black-24dp-1x.png new file mode 100644 index 0000000..0e4f2f6 Binary files /dev/null and b/resources/public/ic-more-vert-black-24dp-1x.png differ diff --git a/resources/public/ic-more-vert-black-24dp-2x.png b/resources/public/ic-more-vert-black-24dp-2x.png new file mode 100644 index 0000000..9f10aa2 Binary files /dev/null and b/resources/public/ic-more-vert-black-24dp-2x.png differ diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 8c75ac1..8a518ff 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -313,11 +313,9 @@ SELECT i.issue_id AS issue_id, u.address AS payout_address, i.execute_hash AS execute_hash -FROM issues i, pull_requests p, users u -WHERE -p.issue_id = i.issue_id -AND p.repo_id = i.repo_id -AND u.id = p.user_id +FROM issues i, repositories r, users u +WHERE i.repo_id = r.repo_id +AND u.id = r.user_id AND i.confirm_hash IS NULL AND i.execute_hash IS NOT NULL; @@ -348,6 +346,29 @@ AND u.id = p.user_id AND i.payout_receipt IS NULL AND i.payout_hash IS NOT NULL; +-- :name confirmed-revocation-payouts :? :* +-- :doc lists all recently confirmed bounty revocations +SELECT + i.contract_address AS contract_address, + r.owner AS owner, + r.repo AS repo, + i.comment_id AS comment_id, + i.issue_number AS issue_number, + i.issue_id AS issue_id, + i.balance_eth AS balance_eth, + i.tokens AS tokens, + i.value_usd AS value_usd, + u.address AS payout_address, + u.login AS payee_login, + i.confirm_hash AS confirm_hash, + i.payout_hash AS payout_hash, + i.updated AS updated +FROM issues i, users u, repositories r +WHERE r.repo_id = i.repo_id +AND u.id = r.user_id +AND i.payout_receipt IS NULL +AND i.payout_hash IS NOT NULL; + -- :name update-winner-login :! :n UPDATE issues SET winner_login = :winner_login @@ -407,6 +428,12 @@ UPDATE issues SET is_open = :is_open WHERE issue_id = :issue_id; +-- :name reset-bot-confirmation :! :n +-- :doc updates issue's execute and confirm hash +UPDATE issues +SET execute_hash = NULL, +confirm_hash = NULL +WHERE issue_id = :issue_id; -- :name issue-exists :1 -- :doc returns true if given issue exists @@ -429,13 +456,17 @@ SELECT i.is_open AS is_open, i.winner_login AS winner_login, i.commit_sha AS commit_sha, + u.address AS owner_address, + i.contract_address AS contract_address, + i.confirm_hash AS confirm_hash, i.title AS title, i.comment_id AS comment_id, i.repo_id AS repo_id, r.owner AS owner, r.repo AS repo -FROM issues i, repositories r +FROM issues i, repositories r, users u WHERE r.repo_id = i.repo_id +AND r.user_id = u.id AND i.issue_id = :issue-id @@ -480,6 +511,7 @@ SELECT i.balance_eth AS balance_eth, i.tokens AS tokens, i.value_usd AS value_usd, + i.execute_hash AS execute_hash, i.confirm_hash AS confirm_hash, i.payout_hash AS payout_hash, i.payout_receipt AS payout_receipt, @@ -489,6 +521,7 @@ SELECT r.owner AS repo_owner, r.owner_avatar_url AS repo_owner_avatar_url, o.address AS owner_address, + o.login AS owner_login, u.address AS payout_address FROM users o, repositories r, issues i LEFT OUTER JOIN users u ON u.login = i.winner_login WHERE diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj index 2473a2e..2898adc 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -125,9 +125,9 @@ (let [open-claims (fn open-claims [bounty] (filter bnt/open? (:claims bounty)))] (if-let [merged-or-paid? (or (:winner_login bounty) - (:payout_hash bounty))] + (:payout_receipt bounty))] (cond - (:payout_hash bounty) :paid + (:payout_receipt bounty) :paid (nil? (:payout_address bounty)) :pending-contributor-address ;; `confirm_hash` is set by us as soon as a PR is merged and the ;; contributor address is known. Usually end users should not need diff --git a/src/clj/commiteth/db/bounties.clj b/src/clj/commiteth/db/bounties.clj index 3f83b84..2a6dacb 100644 --- a/src/clj/commiteth/db/bounties.clj +++ b/src/clj/commiteth/db/bounties.clj @@ -40,6 +40,10 @@ (jdbc/with-db-connection [con-db *db*] (db/confirmed-payouts con-db))) +(defn confirmed-revocation-payouts + [] + (jdbc/with-db-connection [con-db *db*] + (db/confirmed-revocation-payouts con-db))) (defn update-winner-login [issue-id login] @@ -61,6 +65,20 @@ (jdbc/with-db-connection [con-db *db*] (db/reset-payout-hash con-db {:issue_id issue-id}))) +(def payout-receipt-keys + [:issue_id + :payout_hash + :contract_address + :repo + :owner + :comment_id + :issue_number + :balance_eth + :tokens + :confirm_hash + :payee_login + :updated]) + (defn update-payout-receipt [issue-id payout-receipt] (jdbc/with-db-connection [con-db *db*] diff --git a/src/clj/commiteth/db/issues.clj b/src/clj/commiteth/db/issues.clj index d103093..91e33b8 100644 --- a/src/clj/commiteth/db/issues.clj +++ b/src/clj/commiteth/db/issues.clj @@ -95,6 +95,12 @@ (db/update-issue-open con-db {:issue_id issue-id :is_open is-open}))) +(defn reset-bot-confirmation + "resets execute and confirm hash to null for given issue id" + [issue-id] + (jdbc/with-db-connection [con-db *db*] + (db/reset-bot-confirmation con-db {:issue_id issue-id}))) + (defn is-bounty-issue? [issue-id] (let [res (jdbc/with-db-connection [con-db *db*] diff --git a/src/clj/commiteth/routes/services.clj b/src/clj/commiteth/routes/services.clj index 916ce0c..ea4ca0f 100644 --- a/src/clj/commiteth/routes/services.clj +++ b/src/clj/commiteth/routes/services.clj @@ -1,6 +1,7 @@ (ns commiteth.routes.services (:require [ring.util.http-response :refer :all] [compojure.api.sweet :refer :all] + [compojure.api.exception :as ex] [schema.core :as s] [compojure.api.meta :refer [restructure-param]] [buddy.auth.accessrules :refer [restrict]] @@ -10,8 +11,10 @@ [commiteth.db.usage-metrics :as usage-metrics] [commiteth.db.repositories :as repositories] [commiteth.db.bounties :as bounties-db] + [commiteth.db.issues :as issues] [commiteth.bounties :as bounties] [commiteth.eth.core :as eth] + [commiteth.eth.tracker :as tracker] [commiteth.github.core :as github] [clojure.tools.logging :as log] [commiteth.config :refer [env]] @@ -19,7 +22,8 @@ eth-decimal->str]] [crypto.random :as random] [clojure.set :refer [rename-keys]] - [clojure.string :as str])) + [clojure.string :as str] + [commiteth.eth.multisig-wallet :as multisig])) (defn add-bounties-for-existing-issues? [] (env :add-bounties-for-existing-issues false)) @@ -72,6 +76,7 @@ :type :item-type :repo_name :repo-name :repo_owner :repo-owner + :owner_login :owner-login :issue_number :issue-number :issue_id :issue-id :value_usd :value-usd @@ -155,13 +160,28 @@ (let [whitelist (env :user-whitelist #{})] (whitelist user))) +(defn execute-revocation [issue-id contract-address payout-address] + (log/info (str "executing revocation for " issue-id "at" contract-address)) + (try + (let [tx-info (multisig/send-all {:contract contract-address + :payout-address payout-address + :internal-tx-id [:execute issue-id]})] + (tracker/track-tx! tx-info) + {:execute-hash (:tx-hash tx-info)}) + (catch Throwable ex + (log/errorf ex "error revoking funds for %s" issue-id)))) + + (defapi service-routes (when (:dev env) - {:swagger {:ui "/swagger-ui" - :spec "/swagger.json" - :data {:info {:version "0.1" - :title "commitETH API" - :description "commitETH API"}}}}) + {:swagger {:ui "/swagger-ui" + :spec "/swagger.json" + :data {:info {:version "0.1" + :title "commitETH API" + :description "commitETH API"}}} + :exceptions {:handlers + {::ex/request-parsing (ex/with-logging ex/request-parsing-handler :info) + ::ex/response-validation (ex/with-logging ex/response-validation-handler :error)}}}) (context "/api" [] (GET "/top-hunters" [] @@ -191,11 +211,11 @@ (POST "/" [] :auth-rules authenticated? :current-user user - :body [body {:address s/Str + :body [body {:address s/Str :is_hidden_in_hunters s/Bool}] :summary "Updates user's fields." - (let [user-id (:id user) + (let [user-id (:id user) {:keys [address]} body] (when-not (eth/valid-address? address) @@ -226,9 +246,9 @@ (log/debug "/bounty/X/payout" params) (let [{issue :issue payout-hash :payout-hash} params - result (bounties-db/update-payout-hash - (Integer/parseInt issue) - payout-hash)] + result (bounties-db/update-payout-hash + (Integer/parseInt issue) + payout-hash)] (log/debug "result" result) (if (= 1 result) (ok) @@ -237,4 +257,22 @@ :auth-rules authenticated? :current-user user (log/debug "/user/bounties") - (ok (user-bounties user)))))) + (ok (user-bounties user))) + (POST "/revoke" {{issue-id :issue-id} :params} + :auth-rules authenticated? + :current-user user + (let [{contract-address :contract_address owner-address :owner_address} (issues/get-issue-by-id issue-id)] + (do (log/infof "calling revoke-initiate for %s with %s %s" issue-id contract-address owner-address) + (if-let [{:keys [execute-hash]} (execute-revocation issue-id contract-address owner-address)] + (ok {:issue-id issue-id + :execute-hash execute-hash + :contract-address contract-address}) + (bad-request (str "Unable to withdraw funds from " contract-address)))))) + (POST "/remove-bot-confirmation" {{issue-id :issue-id} :params} + :auth-rules authenticated? + :current-user user + (do (log/infof "calling remove-bot-confirmation for %s " issue-id) + ;; if this resulted in updating a row, return success + (if (pos? (issues/reset-bot-confirmation issue-id)) + (ok (str "Updated execute and confirm hash for " issue-id)) + (bad-request (str "Unable to update execute and confirm hash for " issue-id)))))))) diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index 4329bd4..27d5412 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -9,7 +9,7 @@ [commiteth.db.bounties :as db-bounties] [commiteth.bounties :as bounties] [commiteth.util.crypto-fiat-value :as fiat-util] - [commiteth.util.util :refer [eth-decimal->str]] + [commiteth.util.util :as util] [clojure.tools.logging :as log] [mount.core :as mount] [clj-time.core :as t] @@ -129,7 +129,7 @@ repo comment-id contract-address - (eth-decimal->str balance-eth) + (util/eth-decimal->str balance-eth) tokens winner-login true)) @@ -142,35 +142,39 @@ repo comment-id contract-address - (eth-decimal->str balance-eth) + (util/eth-decimal->str balance-eth) tokens winner-login false)))) (catch Throwable ex - (log/error ex "issue %s: self-sign-bounty exception" issue-id))))) + (log/errorf ex "issue %s: self-sign-bounty exception" issue-id))))) (log/info "Exit self-sign-bounty")) (defn update-confirm-hash - "Gets transaction receipt for each pending payout and updates DB confirm_hash with tranaction ID of commiteth bot account's confirmation." - [] - (log/info "In update-confirm-hash") - (p :update-confirm-hash - (doseq [{issue-id :issue_id - execute-hash :execute_hash} (db-bounties/pending-payouts)] - (log/infof "issue %s: pending payout: %s" issue-id execute-hash) + [issue-id execute-hash] + (log/infof "issue %s: pending payout: %s" issue-id execute-hash) (try (when-let [receipt (eth/get-transaction-receipt execute-hash)] (log/infof "issue %s: execution receipt for issue " issue-id receipt) (when-let [confirm-hash (multisig/find-confirmation-tx-id receipt)] - (log/infof "issue %s: confirm hash:" issue-id confirm-hash) + (log/infof "issue %s: confirm hash:%s" issue-id confirm-hash) (tracker/untrack-tx! {:issue-id issue-id :tx-hash execute-hash :result confirm-hash :type :execute}))) (catch Throwable ex - (log/errorf ex "issue %s: update-confirm-hash exception:" issue-id))))) - (log/info "Exit update-confirm-hash")) + (log/errorf ex "issue %s: update-confirm-hash exception:" issue-id)))) +(defn update-confirm-hashes + "Gets transaction receipt for each pending payout and updates DB confirm_hash with tranaction ID of commiteth bot account's confirmation." + [] + (log/info "In update-confirm-hashes") + (p :update-confirm-hash + (doseq [{issue-id :issue_id + execute-hash :execute_hash} (db-bounties/pending-payouts)] + + (update-confirm-hash issue-id execute-hash))) + (log/info "Exit update-confirm-hashes")) (defn update-watch-hash "Sets watch-hash to NULL for bounties where watch tx has been mined. Used to avoid unneeded watch transactions in update-bounty-token-balances" @@ -197,36 +201,33 @@ (println "hour diff:" diff) (> diff 3))) -(defn update-payout-receipt - "Gets transaction receipt for each confirmed payout and updates payout_hash" - [] - (log/info "In update-payout-receipt") - (p :update-payout-receipt - (doseq [{issue-id :issue_id - payout-hash :payout_hash - contract-address :contract_address - repo :repo - owner :owner - comment-id :comment_id - issue-number :issue_number - balance-eth :balance_eth - tokens :tokens - confirm-id :confirm_hash - payee-login :payee_login - updated :updated} (db-bounties/confirmed-payouts)] +(defn update-payout-receipt [bounty] + {:pre [(util/contains-all-keys bounty db-bounties/payout-receipt-keys)]} + (let [{issue-id :issue_id + payout-hash :payout_hash + contract-address :contract_address + repo :repo + owner :owner + comment-id :comment_id + issue-number :issue_number + balance-eth :balance_eth + tokens :tokens + confirm-hash :confirm_hash + payee-login :payee_login + updated :updated} bounty] (log/infof "issue %s: confirmed payout: %s" issue-id payout-hash) (try (if-let [receipt (eth/get-transaction-receipt payout-hash)] - (let [contract-tokens (multisig/token-balances contract-address) + (let [contract-tokens (multisig/token-balances contract-address) contract-eth-balance (eth/get-balance-wei contract-address)] (if (or - (some #(> (second %) 0.0) contract-tokens) - (> contract-eth-balance 0)) + (some #(> (second %) 0.0) contract-tokens) + (> contract-eth-balance 0)) (do (log/infof "issue %s: Contract (%s) still has funds" issue-id contract-address) - (when (multisig/is-confirmed? contract-address confirm-id) + (when (multisig/is-confirmed? contract-address confirm-hash) (log/infof "issue %s: Detected bounty with funds and confirmed payout, calling executeTransaction" issue-id) - (let [execute-tx-hash (multisig/execute-tx contract-address confirm-id)] + (let [execute-tx-hash (multisig/execute-tx contract-address confirm-hash)] (log/infof "issue %s: execute tx: %s" issue-id execute-tx-hash)))) (do @@ -236,15 +237,33 @@ repo comment-id contract-address - (eth-decimal->str balance-eth) + (util/eth-decimal->str balance-eth) tokens payee-login)))) (when (older-than-3h? updated) (log/warn "issue %s: Resetting payout hash for issue as it has not been mined in 3h" issue-id) (db-bounties/reset-payout-hash issue-id))) (catch Throwable ex - (log/error ex "issue %s: update-payout-receipt exception" issue-id))))) - (log/info "Exit update-payout-receipt")) + (log/error ex "issue %s: update-payout-receipt exception" issue-id))))) + +(defn update-payout-receipts + "Gets transaction receipt for each confirmed payout and updates payout_hash" + [] + (log/info "In update-payout-receipts") + (p :update-payout-receipts + (doseq [bounty (db-bounties/confirmed-payouts)] + (update-payout-receipt bounty)) + (log/info "Exit update-payout-receipts"))) + +(defn update-revoked-payout-receipts + "Gets transaction receipt for each confirmed revocation and updates payout_hash" + [] + (log/info "In update-revoked-payout-receipts") + (p :update-revoked-payout-receipts + ;; todo see if confirmed-payouts & confirmed-revocation-payouts can be combined + (doseq [bounty (db-bounties/confirmed-revocation-payouts)] + (update-payout-receipt bounty)) + (log/info "Exit update-revoked-payout-receipts"))) (defn abs "(abs n) is the absolute value of n" @@ -412,8 +431,9 @@ (run-tasks [deploy-pending-contracts update-issue-contract-address - update-confirm-hash - update-payout-receipt + update-confirm-hashes + update-payout-receipts + update-revoked-payout-receipts update-watch-hash check-tx-receipts self-sign-bounty diff --git a/src/clj/commiteth/util/util.clj b/src/clj/commiteth/util/util.clj index f85211e..e4f3f6d 100644 --- a/src/clj/commiteth/util/util.clj +++ b/src/clj/commiteth/util/util.clj @@ -14,3 +14,8 @@ (->> (http/get url) (:body) (json/read-str))) + +(defn contains-all-keys [m ks] + {:pre [(map? m) [(vector? ks)]]} + (every? + #(contains? m %) ks)) diff --git a/src/cljc/commiteth/ui/balances.cljc b/src/cljc/commiteth/ui/balances.cljc index 35c65e0..2a611d6 100644 --- a/src/cljc/commiteth/ui/balances.cljc +++ b/src/cljc/commiteth/ui/balances.cljc @@ -5,6 +5,14 @@ {:pre [(string? tla)]} (get {"ETH" "#57a7ed"} tla "#4360df")) +(defn pending-badge [] + "static component for pending badge" + [:div.dib.ph2.pv1.relative + {:style {:color "#CCAC00"}} + [:div.absolute.top-0.left-0.right-0.bottom-0.o-30.br2 + {:style {:background-color "#FFD700"}}] + [:span.pg-med "Refund pending"]]) + (defn balance-badge [tla balance] {:pre [(keyword? tla)]} diff --git a/src/cljs/commiteth/core.cljs b/src/cljs/commiteth/core.cljs index 5dc1854..693403b 100644 --- a/src/cljs/commiteth/core.cljs +++ b/src/cljs/commiteth/core.cljs @@ -7,6 +7,7 @@ [commiteth.routes] [commiteth.handlers] [commiteth.subscriptions] + [commiteth.interceptors] [commiteth.activity :refer [activity-page]] [commiteth.bounties :refer [bounties-page]] [commiteth.repos :refer [repos-page]] diff --git a/src/cljs/commiteth/db.cljs b/src/cljs/commiteth/db.cljs index 295969f..76dab88 100644 --- a/src/cljs/commiteth/db.cljs +++ b/src/cljs/commiteth/db.cljs @@ -19,6 +19,7 @@ ::ui-model/bounty-filter-type|date nil ::ui-model/bounty-filter-type|owner nil} ::open-bounty-claims #{} + ::pending-revocations {} :owner-bounties {} :top-hunters [] :activity-feed []}) diff --git a/src/cljs/commiteth/handlers.cljs b/src/cljs/commiteth/handlers.cljs index 63b3fda..56bef55 100644 --- a/src/cljs/commiteth/handlers.cljs +++ b/src/cljs/commiteth/handlers.cljs @@ -1,6 +1,7 @@ (ns commiteth.handlers (:require [commiteth.db :as db] - [re-frame.core :refer [dispatch + [re-frame.core :refer [debug + dispatch reg-event-db reg-event-fx reg-fx @@ -14,12 +15,16 @@ :refer [reg-co-fx!]] [commiteth.ui-model :as ui-model] [commiteth.common :as common] - [commiteth.routes :as routes])) + [commiteth.routes :as routes] + [commiteth.interceptors])) (rf-storage/reg-co-fx! :commiteth-sob {:fx :store :cofx :store}) +;; https://github.com/Day8/re-frame/blob/master/docs/Debugging-Event-Handlers.md +(def interceptors [(when ^boolean goog.DEBUG debug)]) + (reg-fx :http (fn [{:keys [method url on-success on-error finally params]}] @@ -110,6 +115,16 @@ (fn [db _] (dissoc db :flash-message))) +(reg-event-db + :set-revoke-modal + (fn [db [_ bounty]] + (assoc db :revoke-modal-bounty bounty))) + +(reg-event-db + :clear-revoke-modal + (fn [db [_ bounty]] + (dissoc db :revoke-modal-bounty bounty))) + (defn assoc-in-if-not-empty [m path val] (if (seq val) (assoc-in m path val) @@ -217,6 +232,8 @@ (reg-event-db :set-owner-bounties + [commiteth.interceptors/watch-confirm-hash + commiteth.interceptors/watch-payout-receipt] (fn [db [_ issues]] (assoc db :owner-bounties issues @@ -376,6 +393,7 @@ (reg-event-fx :save-payout-hash + interceptors (fn [{:keys [db]} [_ issue-id payout-hash]] {:db db :http {:method POST @@ -386,13 +404,15 @@ (defn send-transaction-callback - [issue-id] + [issue-id pending-revocations] (fn [error payout-hash] (println "send-transaction-callback" error payout-hash) (when error - (dispatch [:set-flash-message - :error - (str "Error sending transaction: " error)]) + (if (empty? pending-revocations) + (dispatch [:set-flash-message + :error + (str "Error sending transaction: " error)]) + (dispatch [:remove-bot-confirmation issue-id])) (dispatch [:payout-confirm-failed issue-id])) (when payout-hash (dispatch [:save-payout-hash issue-id payout-hash])))) @@ -405,14 +425,72 @@ (defn strip-0x [x] (str/replace x #"^0x" "")) +(defn set-pending-revocation [location issue-id confirming-account] + (assoc-in location [::db/pending-revocations issue-id] + {:confirming-account confirming-account})) + +(reg-event-fx + :set-pending-revocation + [interceptors (inject-cofx :store)] + (fn [{:keys [db store]} [_ issue-id confirming-account]] + {:db (set-pending-revocation db issue-id confirming-account) + :store (set-pending-revocation store issue-id confirming-account)})) + +(reg-event-fx + :remove-pending-revocation + [interceptors (inject-cofx :store)] + (fn [{:keys [db store]} [_ issue-id]] + {:db (dissoc-in db [::db/pending-revocations issue-id]) + :store (dissoc-in store [::db/pending-revocations issue-id])})) + +(reg-event-fx + :remove-bot-confirmation + interceptors + (fn [{:keys [db]} [_ issue-id]] + {:http {:method POST + :url "/api/user/remove-bot-confirmation" + :params {:token (get-admin-token db) + :issue-id issue-id} + :on-success #(dispatch [:remove-pending-revocation issue-id]) + :on-error #(println "error removing bot confirmation for " issue-id)}})) + +(reg-event-fx + :revoke-bounty-success + (fn [{:keys [db]} [_ {:keys [issue-id owner-address contract-address confirm-hash]}]] + {:dispatch [:set-pending-revocation issue-id :commiteth]})) + +(reg-event-fx + :revoke-bounty-error + interceptors + (fn [{:keys [db]} [_ issue-id response]] + {:dispatch [:set-flash-message + :error (if (= 400 (:status response)) + (:response response) + (str "Failed to initiate revocation for: " issue-id + (:status-text response)))]})) + +(reg-event-fx + :revoke-bounty + interceptors + (fn [{:keys [db]} [_ issue-id]] + {:http {:method POST + :url "/api/user/revoke" + :on-success #(dispatch [:revoke-bounty-success %]) + :on-error #(dispatch [:revoke-bounty-error %]) + :params {:token (get-admin-token db) + :issue-id issue-id}} + :dispatch [:clear-revoke-modal]})) + (reg-event-fx :confirm-payout + interceptors (fn [{:keys [db]} [_ {issue-id :issue_id owner-address :owner_address contract-address :contract_address confirm-hash :confirm_hash} issue]] (println (:web3 db)) (let [w3 (:web3 db) + pending-revocations (::db/pending-revocations db) confirm-method-id (sig->method-id w3 "confirmTransaction(uint256)") confirm-id (strip-0x confirm-hash) data (str confirm-method-id @@ -426,7 +504,7 @@ (println "data:" data) (try (web3-eth/send-transaction! w3 payload - (send-transaction-callback issue-id)) + (send-transaction-callback issue-id pending-revocations)) {:db (assoc-in db [:owner-bounties issue-id :confirming?] true)} (catch js/Error e {:db (assoc-in db [:owner-bounties issue-id :confirm-failed?] true) @@ -437,6 +515,7 @@ (reg-event-fx :payout-confirmed + interceptors (fn [{:keys [db]} [_ issue-id]] {:dispatch [:load-owner-bounties] :db (-> db @@ -490,6 +569,21 @@ (.removeEventListener js/window "click" close-dropdown) (assoc db :user-dropdown-open? false))) +(defn close-three-dots [] + (dispatch [:three-dots-close])) + +(reg-event-db + :three-dots-open + (fn [db [_ issue-id]] + (.addEventListener js/window "click" close-three-dots) + (update db ::db/unclaimed-options (fnil conj #{}) issue-id))) + +(reg-event-db + :three-dots-close + (fn [db [_ issue-id]] + (.removeEventListener js/window "click" close-three-dots) + (assoc db ::db/unclaimed-options #{}))) + (reg-event-db ::open-bounty-claim (fn [db [_ opening-issue-id]] diff --git a/src/cljs/commiteth/interceptors.cljs b/src/cljs/commiteth/interceptors.cljs new file mode 100644 index 0000000..9016329 --- /dev/null +++ b/src/cljs/commiteth/interceptors.cljs @@ -0,0 +1,79 @@ +(ns commiteth.interceptors + (:require [commiteth.db :as db] + [re-frame.core :as rf] + [clojure.data :as data])) + +(defn get-confirming-issue-id [owner pending-revocations] + "returns the issue id for the current revocation matching the desired owner type" + (some (fn [[issue-id revocation]] + (when (= owner (:confirming-account revocation)) + issue-id)) + pending-revocations)) + +(defn dispatch-confirm-payout [bounty] + "dispatches a bounty via reframe dispatch" + (rf/dispatch [:confirm-payout {:issue_id (:issue-id bounty) + :owner_address (:owner_address bounty) + :contract_address (:contract_address bounty) + :confirm_hash (:confirm_hash bounty)}])) + +(defn dispatch-set-pending-revocation [bounty] + "update the currently confirming account to owner" + (rf/dispatch [:set-pending-revocation (:issue-id bounty) :owner])) + +(defn dispatch-remove-pending-revocation [bounty] + "dispatches a bounty via reframe dispatch" + (rf/dispatch [:remove-pending-revocation (:issue-id bounty)])) + +(def watch-confirm-hash + "revocations move through 2 states, confirmation by commiteth and then the repo owner + if a commiteth revocation is detected, check to see if its confirm hash is set, and, if it is + dispatch a confirm payout event and update the confirming account to owner + + *Warning* this inteceptor is only intended for use with the + `:load-owner-bounties` event + + More information on re-frame interceptors can be found here: + https://github.com/Day8/re-frame/blob/master/docs/Interceptors.md" + + (rf/->interceptor + :id :watch-confirm-hash + :after (fn confirm-hash-update-after + [context] + (println "watch confirm hash interceptor...") + (let [pending-revocations (get-in context [:effects :db ::db/pending-revocations]) + updated-bounties (get-in context [:effects :db :owner-bounties]) + confirming-issue-id (get-confirming-issue-id :commiteth pending-revocations)] + (when-let [revoking-bounty (get updated-bounties confirming-issue-id)] + (if (:confirm_hash revoking-bounty) + (do (dispatch-confirm-payout revoking-bounty) + (dispatch-set-pending-revocation revoking-bounty)) + (println (str "currently revoking " confirming-issue-id " but confirm hash has not been set yet.")))) + ;; interceptor must return context + context)))) + + +(def watch-payout-receipt + "examine pending revocations with their currently confirming account set to owner + when one of them has its payout_receipt set, dispatch `remove-pending-revocation` + + *Warning* this inteceptor is only intended for use with the + `:load-owner-bounties` event + + More information on re-frame interceptors can be found here: + https://github.com/Day8/re-frame/blob/master/docs/Interceptors.md" + + (rf/->interceptor + :id :watch-payout-receipt + :after (fn payout-receipt-update-after + [context] + (println "watch payout receipt interceptor...") + (let [pending-revocations (get-in context [:effects :db ::db/pending-revocations]) + updated-bounties (get-in context [:effects :db :owner-bounties]) + confirming-issue-id (get-confirming-issue-id :owner pending-revocations)] + (when-let [revoking-bounty (get updated-bounties confirming-issue-id)] + (if (:payout_receipt revoking-bounty) + (dispatch-remove-pending-revocation revoking-bounty) + (println (str "currently revoking " confirming-issue-id " but payout receipt has not been set yet.")))) + ;; interceptor must return context + context)))) diff --git a/src/cljs/commiteth/manage_payouts.cljs b/src/cljs/commiteth/manage_payouts.cljs index c2f0f44..a9e48ac 100644 --- a/src/cljs/commiteth/manage_payouts.cljs +++ b/src/cljs/commiteth/manage_payouts.cljs @@ -6,6 +6,7 @@ [commiteth.routes :as routes] [commiteth.model.bounty :as bnt] [commiteth.ui.balances :as ui-balances] + [commiteth.config :as config] [commiteth.common :as common :refer [human-time]])) (defn pr-url [{owner :repo_owner @@ -13,6 +14,16 @@ repo :repo_name}] (str "https://github.com/" owner "/" repo "/pull/" pr-number)) +(defn etherscan-tx-url [tx-id] + (str "https://" + (when (config/on-testnet?) "ropsten.") + "etherscan.io/tx/" tx-id)) + +(defn etherscan-address-url [address] + (str "https://" + (when (config/on-testnet?) "ropsten.") + "etherscan.io/address/" address)) + (def primary-button-button :button.f7.ttu.tracked.outline-0.bg-sob-blue.white.pv3.ph4.pg-med.br3.bn.pointer.shadow-7) (def primary-button-link :a.dib.tc.f7.ttu.tracked.bg-sob-blue.white.pv2.ph3.pg-med.br2.pointer.hover-white.shadow-7) @@ -57,6 +68,8 @@ "Signed off" "Confirm Payment")]))) + + (defn confirm-row [bounty claim] (let [payout-address-available? (:payout_address bounty)] [:div @@ -231,16 +244,80 @@ [:div bottom]]]) (defn small-card-balances [bounty] - [:div.f6 - [ui-balances/token-balances (bnt/crypto-balances bounty) :label] + (let [pending-revocations (rf/subscribe [:pending-revocations])] + (fn [bounty] + [:div.f6.fl.w-80 + [ui-balances/token-balances (bnt/crypto-balances bounty) :label] + [:div + [ui-balances/usd-value-label (:value-usd bounty)]] + (when (some #(= (:issue-id %) + (:issue-id bounty)) @pending-revocations) + [:div.pt1 + [ui-balances/pending-badge]])]))) + +(defn three-dots-box [image-src] + "generates the appropriate container for menu dots" + [:span.pt2.pointer + [:img.o-50.pl3.pt2 {:src image-src}]]) + +(defn check-box [image-src] + "generates the appropriate container for a blue arrow" + [:span.pr2 + [:img.w1.v-mid.o-50 {:src image-src}]]) + +(defn three-dots [issue-id] + [:div [:div - [ui-balances/usd-value-label (:value-usd bounty)]]]) + {:on-click #(rf/dispatch [:three-dots-open issue-id])} + [three-dots-box "ic-more-vert-black-24dp-1x.png"]]]) + +(defn revoke-modal [] + (let [bounty @(rf/subscribe [:revoke-modal-bounty])] + (fn [] + (when bounty + (let [owner-address (:owner_address bounty)] + ;; width requires a deliberate override of semantic.min.css + [:div.ui.active.modal.br3 {:style {:top 100 + :width 650}} + [:div.pa4 + [:h3.dark-gray "Are you sure you want to request a refund?"] + [:p.silver "This will set your bounty" + [:span.pg-med " value to $0."] + " Don't worry, your issue will still be accessible to the community. You can check the status of your refund at the top of the dashboard."] + [:div.bg-sob-tint.br3.pa3 + [:p.fw4 (:issue-title bounty)] + [ui-balances/token-balances (bnt/crypto-balances bounty) :label] + [:p [ui-balances/usd-value-label (:value-usd bounty)]] + [:p.silver "To be refunded to: " owner-address]] + [:div.pt3 + [primary-button-button + {:on-click #(rf/dispatch [:revoke-bounty (:issue-id bounty)])} + "REQUEST REFUND"] + [:span.dark-gray.pointer.fw4.f7.ml3 + {:role "button" + :on-click #(rf/dispatch [:clear-revoke-modal])} + "CANCEL"]]]]))))) + +(defn revoke-dropdown [bounty] + (let [menu (if (contains? @(rf/subscribe [:three-dots-open?]) (:issue-id bounty)) + [:div.ui.menu.revoke.transition {:tab-index -1}] + [:div.ui.menu.transition.hidden])] + [:div.fl.w-20 + (if (empty? @(rf/subscribe [:pending-revocations])) + [three-dots (:issue-id bounty)]) + (into menu [[:div + [:a.pa2 + + {:on-click #(rf/dispatch [:set-revoke-modal bounty])} + "Revoke"]]])])) (defn unclaimed-bounty [bounty] [:div.w-third-l.fl-l.pa2 [square-card [bounty-title-link bounty {:show-date? true :max-length 60}] - [small-card-balances bounty]]]) + [:div [small-card-balances bounty] + (when (pos? (:value-usd bounty)) + [revoke-dropdown bounty])]]]) (defn paid-bounty [bounty] [:div.w-third-l.fl-l.pa2 @@ -248,7 +325,10 @@ [:div [bounty-title-link bounty {:show-date? false :max-length 60}] [:div.f6.mt1.gray - "Paid out to " [:span.pg-med.fw5 "@" (:winner_login bounty)]]] + "Paid out to " [:span.pg-med.fw5 "@" (or (:winner_login bounty) + ;; use repo owner for revoked bounties + ;; where no winner login is set + (:owner-login bounty))]]] [small-card-balances bounty]]]) (defn expandable-bounty-list [bounty-component bounties] @@ -272,9 +352,27 @@ (defn count-pill [n] [:span.v-top.ml3.ph3.pv1.bg-black-05.gray.br3.f7 n]) -(defn salute [name] +(defn pending-banner [] + (let [banner-info (rf/subscribe [:pending-revocations])] + (fn pending-banner-render [] + (when @banner-info + (into [:div] + (for [revoking-bounty @banner-info] + ^{:key (:contract_address revoking-bounty)} + [:div.relative.pa3.pr4.bg-sob-green.br3.nt1 + [:div + (case (:confirming-account revoking-bounty) + :commiteth [:p.v-mid [check-box "ic-check-circle-black-24dp-2x.png"] + [:span.pg-med "Transaction sent."] " Your refund requires two confirmations. After the first one " + [:a.sob-blue.pg-med {:href (etherscan-address-url (:contract_address revoking-bounty)) :target "_blank"} " completes "] + "you'll be prompted to sign the second via metamask."] + :owner [:p.v-mid [check-box "ic-check-circle-black-24dp-2x.png"] + [:span.pg-med "Transaction sent."] " Once your metamask transaction is confirmed your revocation will be complete. Follow the final step " + [:a.sob-blue.pg-med {:href (etherscan-address-url (:contract_address revoking-bounty)) :target "_blank"} " here. "]])]])))))) + +(defn salute [] (let [msg-info (rf/subscribe [:dashboard/banner-msg])] - (fn salute-render [name] + (fn salute-render [] (when @msg-info [:div.relative.pa3.pr4.bg-sob-blue-o-20.br3.nt1 [:div.f3.dark-gray.absolute.top-0.right-0.pa3.b.pointer @@ -326,7 +424,8 @@ user (rf/subscribe [:user]) owner-bounties (rf/subscribe [:owner-bounties]) bounty-stats-data (rf/subscribe [:owner-bounties-stats]) - owner-bounties-loading? (rf/subscribe [:get-in [:owner-bounties-loading?]])] + owner-bounties-loading? (rf/subscribe [:get-in [:owner-bounties-loading?]]) + revoke-modal-bounty (rf/subscribe [:revoke-modal-bounty])] (fn manage-payouts-page-render [] (cond (nil? @user) @@ -348,13 +447,16 @@ (get grouped :pending-contributor-address))] [:div.center.mw9.pa2.pa0-l [manage-bounties-title] - [salute "Andy"] + [salute] + [pending-banner] [:div.dn-l.db-ns.mt4 [bounty-stats-new @bounty-stats-data]] (when (nil? (common/web3)) [:div.ui.warning.message [:i.warning.icon] "To sign off claims, please view Status Open Bounty in Status, Mist or Metamask"]) + (when @revoke-modal-bounty + [revoke-modal]) [manage-bounties-nav route-id] [:div.cf [:div.fr.w-third.pl4.mb3.dn.db-l diff --git a/src/cljs/commiteth/subscriptions.cljs b/src/cljs/commiteth/subscriptions.cljs index d8160b0..2047f23 100644 --- a/src/cljs/commiteth/subscriptions.cljs +++ b/src/cljs/commiteth/subscriptions.cljs @@ -41,6 +41,11 @@ (fn [db _] (:flash-message db))) +(reg-sub + :revoke-modal-bounty + (fn [db _] + (:revoke-modal-bounty db))) + (reg-sub :open-bounties (fn [db _] @@ -82,7 +87,10 @@ ;; special prefix or namespace for derived properties that ;; are added to domain records like this ;; e.g. `derived/paid?` - [id (assoc bounty :paid? (boolean (:payout_hash bounty)))]) + [id (assoc bounty :paid? (boolean (and (:payout_receipt bounty) + ;; bounties with winner logins + ;; were not revoked + (:winner_login bounty))))]) (into {})))) (reg-sub @@ -179,6 +187,19 @@ (fn [db _] (:user-dropdown-open? db))) +(reg-sub + :three-dots-open? + (fn [db _] + (::db/unclaimed-options db))) + +(reg-sub + :pending-revocations + (fn [db _] + (map (fn [[issue-id revocations]] + (merge revocations + (get-in db [:owner-bounties issue-id]))) + (::db/pending-revocations db)))) + (reg-sub ::open-bounty-claims (fn [db _] diff --git a/src/less/style.less b/src/less/style.less index 49eac56..c1b965f 100644 --- a/src/less/style.less +++ b/src/less/style.less @@ -250,9 +250,29 @@ label[for="input-hidden"] { a { color: #fff; pointer: default; + display:block; } } +.ui.menu.revoke.transition { + font-family: "PostGrotesk-Book"; + font-size: 1em; + border-radius: 8px; + border: none; + padding: .5em; + background-color: #fff; + a { + color: #8d99a4;; + cursor: pointer; + display:block; + } + min-width:80px; + position:absolute; + z-index: 800000; + right: 22px; + bottom:60px; +} + .logout-link { color: #fff!important; } @@ -1266,4 +1286,4 @@ body { } -@import (inline) "tachyons-utils.css"; \ No newline at end of file +@import (inline) "tachyons-utils.css"; diff --git a/src/less/tachyons-utils.css b/src/less/tachyons-utils.css index 42e1d64..92a495b 100644 --- a/src/less/tachyons-utils.css +++ b/src/less/tachyons-utils.css @@ -29,6 +29,12 @@ .hover-sob-sky:hover, .hover-sob-sky:focus { color: #f2f5f8; } .hover-bg-sob-sky:hover, .hover-bg-sob-sky:focus { background-color: #f2f5f8; } +.sob-green { color: #d1ead8; } +.bg-sob-green { background-color: #d1ead8; } +.b--sob-green { border-color: #d1ead8; } +.hover-sob-green:hover, .hover-sob-green:focus { color: #d1ead8; } +.hover-bg-sob-green:hover, .hover-bg-sob-green:focus { background-color: #d1ead8; } + .sob-tint { color: #f7f9fa; } .bg-sob-tint { background-color: #f7f9fa; } .b--sob-tint { border-color: #f7f9fa; }