Bounties view + related backend functionality & more

Changes to bounty/pull-request data model and logic
* save git sha of pull request's HEAD instead of possible merge commit
  and save it for opened, merged and closed events
* only look for bounties being accepted in pull-request closed via
  merge events (no need for compilicated logic with issue-closed event)
* pull_request now has issue_id column to make many sql queries
  simpler

Frontend
* owner-bounties app-db structure changes
* bounties view with confirm pending payout UI

Other
* removed lots of unused legacy code
* bug fixes + refactoring
* added comments + TODOs
This commit is contained in:
Teemu Patja 2017-02-25 00:15:44 +02:00
parent ec5f07a578
commit 9cb48b5183
No known key found for this signature in database
GPG Key ID: F5B7035E6580FD4C
17 changed files with 282 additions and 319 deletions

View File

@ -41,7 +41,8 @@
[crypto-equality "1.0.0"] [crypto-equality "1.0.0"]
[cheshire "5.7.0"] [cheshire "5.7.0"]
[mpg "1.3.0"] [mpg "1.3.0"]
[pandect "0.6.1"]] [pandect "0.6.1"]
[prismatic/plumbing "0.5.3"]]
:min-lein-version "2.0.0" :min-lein-version "2.0.0"
:source-paths ["src/clj" "src/cljc"] :source-paths ["src/clj" "src/cljc"]

View File

@ -0,0 +1,4 @@
ALTER TABLE "public"."pull_requests"
ADD COLUMN "updated" timestamp without time zone DEFAULT timezone('utc'::text, now()),
ADD COLUMN "issue_id" integer,
ADD UNIQUE ("pr_id");

View File

@ -121,8 +121,8 @@ INSERT INTO issues (repo_id, issue_id, issue_number, title)
FROM issues FROM issues
WHERE repo_id = :repo_id AND issue_id = :issue_id); WHERE repo_id = :repo_id AND issue_id = :issue_id);
-- :name close-issue! :<! :1 -- :name update-commit-id :<! :1
-- :doc updates issue with commit id -- :doc updates issue with commit_id
UPDATE issues UPDATE issues
SET commit_id = :commit_id SET commit_id = :commit_id
WHERE issue_id = :issue_id WHERE issue_id = :issue_id
@ -171,7 +171,7 @@ SELECT
transaction_hash transaction_hash
FROM issues FROM issues
WHERE contract_address IS NULL WHERE contract_address IS NULL
AND issues.transaction_hash IS NOT NULL; AND issues.transaction_hash IS NOT NULL;
-- Pull Requests ------------------------------------------------------------------- -- Pull Requests -------------------------------------------------------------------
@ -181,6 +181,7 @@ INSERT INTO pull_requests (pr_id,
repo_id, repo_id,
pr_number, pr_number,
issue_number, issue_number,
issue_id,
commit_id, commit_id,
user_id, user_id,
state) state)
@ -188,12 +189,14 @@ VALUES(:pr_id,
:repo_id, :repo_id,
:pr_number, :pr_number,
:issue_number, :issue_number,
:issue_id,
:commit_id, :commit_id,
:user_id, :user_id,
:state) :state)
ON CONFLICT (pr_id) DO UPDATE ON CONFLICT (pr_id) DO UPDATE
SET SET
state = :state, state = :state,
updated = timezone('utc'::text, now()),
commit_id = :commit_id; commit_id = :commit_id;
-- Bounties ------------------------------------------------------------------------ -- Bounties ------------------------------------------------------------------------
@ -206,7 +209,7 @@ SELECT
u.address AS payout_address u.address AS payout_address
FROM issues i, pull_requests p, users u FROM issues i, pull_requests p, users u
WHERE WHERE
(p.commit_id = i.commit_id OR coalesce(p.issue_number, -1) = i.issue_number) p.issue_id = i.issue_id
AND p.repo_id = i.repo_id AND p.repo_id = i.repo_id
AND u.id = p.user_id AND u.id = p.user_id
AND i.execute_hash IS NULL; AND i.execute_hash IS NULL;
@ -216,10 +219,11 @@ AND i.execute_hash IS NULL;
SELECT SELECT
i.contract_address AS contract_address, i.contract_address AS contract_address,
i.issue_id AS issue_id, i.issue_id AS issue_id,
u.address AS payout_address u.address AS payout_address,
i.execute_hash AS execute_hash
FROM issues i, pull_requests p, users u FROM issues i, pull_requests p, users u
WHERE WHERE
(p.commit_id = i.commit_id OR coalesce(p.issue_number, -1) = i.issue_number) p.issue_id = i.issue_id
AND p.repo_id = i.repo_id AND p.repo_id = i.repo_id
AND u.id = p.user_id AND u.id = p.user_id
AND i.confirm_hash IS NULL AND i.confirm_hash IS NULL
@ -230,10 +234,11 @@ AND i.execute_hash IS NOT NULL;
SELECT SELECT
i.contract_address AS contract_address, i.contract_address AS contract_address,
i.issue_id AS issue_id, i.issue_id AS issue_id,
u.address AS payout_address u.address AS payout_address,
i.payout_hash AS payout_hash
FROM issues i, pull_requests p, users u FROM issues i, pull_requests p, users u
WHERE WHERE
(p.commit_id = i.commit_id OR coalesce(p.issue_number, -1) = i.issue_number) p.issue_id = i.issue_id
AND p.repo_id = i.repo_id AND p.repo_id = i.repo_id
AND u.id = p.user_id AND u.id = p.user_id
AND i.payout_receipt IS NULL AND i.payout_receipt IS NULL
@ -280,7 +285,7 @@ r.repo_id = i.repo_id
AND i.commit_id IS NULL; AND i.commit_id IS NULL;
-- :name owner-bounties-list :? :* -- :name owner-bounties-list :? :*
-- :doc lists fixed issues (bounties awaiting maintainer confirmation) -- :doc all bounty issues for given owner
SELECT SELECT
i.contract_address AS contract_address, i.contract_address AS contract_address,
i.issue_id AS issue_id, i.issue_id AS issue_id,
@ -291,24 +296,50 @@ SELECT
i.confirm_hash AS confirm_hash, i.confirm_hash AS confirm_hash,
i.payout_hash AS payout_hash, i.payout_hash AS payout_hash,
i.payout_receipt AS payout_receipt, i.payout_receipt AS payout_receipt,
r.repo AS repo_name,
o.address AS owner_address
FROM issues i, users o, repositories r
WHERE
r.repo_id = i.repo_id
AND r.user_id = o.id
AND r.user_id = :owner_id;
-- AND i.confirm_hash IS NOT NULL;
-- :name bounty-claims :? :*
-- :doc open, merged and closed PRs referencing given bounty issue
SELECT
i.contract_address AS contract_address,
i.issue_id AS issue_id,
i.issue_number AS issue_number,
i.title AS issue_title,
i.repo_id AS repo_id,
i.balance AS balance,
i.confirm_hash AS confirm_hash,
i.payout_hash AS payout_hash,
i.payout_receipt AS payout_receipt,
p.state AS pr_state,
p.pr_id AS pr_id, p.pr_id AS pr_id,
p.user_id AS user_id, p.user_id AS user_id,
p.pr_number AS pr_number, p.pr_number AS pr_number,
u.address AS payout_address, u.address AS payout_address,
u.login AS user_login, u.login AS user_login,
u.name AS user_name, u.name AS user_name,
r.login AS owner_name, u.avatar_url AS user_avatar_url,
r.login AS owner_login,
r.repo AS repo_name, r.repo AS repo_name,
o.address AS owner_address o.address AS owner_address
FROM issues i, pull_requests p, users u, users o, repositories r FROM issues i, pull_requests p, users u, users o, repositories r
WHERE WHERE
(p.commit_id = i.commit_id OR coalesce(p.issue_number, -1) = i.issue_number) p.issue_id = i.issue_id
AND p.repo_id = i.repo_id AND p.repo_id = i.repo_id
AND u.id = p.user_id AND u.id = p.user_id
AND r.repo_id = i.repo_id AND r.repo_id = i.repo_id
AND r.user_id = o.id AND r.user_id = o.id
AND r.user_id = :owner_id AND i.issue_id = :issue_id;
AND i.confirm_hash IS NOT NULL; --AND i.confirm_hash IS NOT NULL;
-- :name owner-issues-list :? :* -- :name owner-issues-list :? :*
-- :doc owner's bounty issues with no merged PR -- :doc owner's bounty issues with no merged PR
@ -353,8 +384,7 @@ SELECT
i.issue_number AS issue_number, i.issue_number AS issue_number,
i.balance AS balance, i.balance AS balance,
r.login AS login, r.login AS login,
r.repo AS repo, r.repo AS repo
i.state AS state
FROM issues i, repositories r FROM issues i, repositories r
WHERE i.issue_number = :issue_number WHERE i.issue_number = :issue_number
AND r.repo_id = i.repo_id AND r.repo_id = i.repo_id

View File

@ -14,6 +14,12 @@
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/owner-bounties-list con-db {:owner_id owner}))) (db/owner-bounties-list con-db {:owner_id owner})))
(defn bounty-claims
[issue-id]
(jdbc/with-db-connection [con-db *db*]
(db/bounty-claims con-db {
:issue_id issue-id})))
(defn list-not-fixed-issues (defn list-not-fixed-issues
[owner-id] [owner-id]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]

View File

@ -12,11 +12,12 @@
:issue_number issue-number :issue_number issue-number
:title issue-title}))) :title issue-title})))
(defn close (defn update-commit-id
"Updates issue with commit_id" "Updates issue with commit_id"
[commit-id issue-id] [commit-id issue-id]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/close-issue! con-db {:issue_id issue-id :commit_id commit-id}))) (db/update-commit-id con-db {:issue_id issue-id
:commit_id commit-id})))
(defn update-transaction-hash (defn update-transaction-hash
"Updates issue with transaction-hash" "Updates issue with transaction-hash"

View File

@ -11,7 +11,8 @@
:opened 0 :opened 0
:merged 1 :merged 1
:closed 2)] :closed 2)]
(log/debug (assoc pull-request :state state))
(log/debug "save pr" (assoc pull-request :state state))
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/save-pull-request! con-db (db/save-pull-request! con-db
(assoc pull-request :state state))))) (assoc pull-request :state state)))))

View File

@ -90,6 +90,19 @@
github-repos)))) github-repos))))
(defn user-bounties [user]
(let [owner-bounties (bounties-db/list-owner-bounties (:id user))]
owner-bounties
(into {}
(for [b owner-bounties]
[(:issue_id b)
(conj b
(let [claims (bounties-db/bounty-claims (:issue_id b))
balance (:balance b)]
{:balance-eth (eth/hex->eth balance 6)
:claims claims}))]))))
(defapi service-routes (defapi service-routes
{:swagger {:ui "/swagger-ui" {:swagger {:ui "/swagger-ui"
:spec "/swagger.json" :spec "/swagger.json"
@ -134,21 +147,22 @@
(POST "/bounty/:issue{[0-9]{1,9}}/payout" {:keys [params]} (POST "/bounty/:issue{[0-9]{1,9}}/payout" {:keys [params]}
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user
(let [{issue :issue (do
payout-hash :payout-hash} params (log/debug "/bounty/X/payout" params)
result (bounties-db/update-payout-hash (let [{issue :issue
(Integer/parseInt issue) payout-hash :payout-hash} params
payout-hash)] result (bounties-db/update-payout-hash
(if (= 1 result) (Integer/parseInt issue)
(ok) payout-hash)]
(internal-server-error)))) (log/debug "result" result)
(if (= 1 result)
(ok)
(internal-server-error)))))
(GET "/bounties" [] (GET "/bounties" []
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user
(log/debug "/user/bounties") (log/debug "/user/bounties")
(ok (map #(conj % (let [balance (:balance %)] (ok (user-bounties user)))
{:balance-eth (eth/hex->eth balance 6)}))
(bounties-db/list-owner-bounties (:id user)))))
(POST "/repository/toggle" {:keys [params]} (POST "/repository/toggle" {:keys [params]}
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user

View File

@ -12,7 +12,8 @@
[commiteth.util.digest :refer [hex-hmac-sha1]] [commiteth.util.digest :refer [hex-hmac-sha1]]
[compojure.core :refer [defroutes POST]] [compojure.core :refer [defroutes POST]]
[crypto.equality :as crypto] [crypto.equality :as crypto]
[ring.util.http-response :refer [ok]]) [ring.util.http-response :refer [ok]]
[commiteth.db.bounties :as bounties-db])
(:import java.lang.Integer)) (:import java.lang.Integer))
(defn find-issue-event (defn find-issue-event
@ -51,13 +52,20 @@
(defn handle-issue-closed (defn handle-issue-closed
;; TODO: does not work in case the issue is closed on github web ui ;; TODO: does not work in case the issue is closed on github web ui
[{{{user :login} :owner repo :name} :repository [{{{owner :login} :owner repo :name} :repository
{issue-id :id issue-number :number} :issue}] {issue-id :id issue-number :number} :issue}]
(log/debug "handle-issue-closed") (log/debug "handle-issue-closed" owner repo issue-number issue-id)
(future (when-let [commit-id (find-commit-id owner repo issue-number ["referenced" "closed"])]
(when-let [commit-id (find-commit-id user repo issue-number ["referenced" "closed"])] (log/debug (format "Issue %s/%s/%s closed with commit %s" owner repo issue-number commit-id))
(log/debug (format "Issue %s/%s/%s closed with commit %s" user repo issue-number commit-id)) (log/info "NOT considering event as bounty winner")
(issues/close commit-id issue-id)))) ;; TODO: disabled for now since the system is meant to be used
;; exclusively via pull requests. issue closed event without a PR
;; closed via merge first means that the referencing commit was
;; pushed directly to master and thus never accepted by the
;; maintainer (could be that the bounty hunter had write access
;; to master, but that scenario should be very rare and better
;; not to support it)
#_(issues/close commit-id issue-id)))
(def ^:const keywords (def ^:const keywords
[#"(?i)close:?\s+#(\d+)" [#"(?i)close:?\s+#(\d+)"
@ -109,6 +117,8 @@
avatar_url :avatar_url avatar_url :avatar_url
name :name} :user name :name} :user
id :id id :id
merged? :merged
{head-sha :sha} :head
pr-number :number pr-number :number
pr-body :body pr-body :body
pr-title :title} :pull_request}] pr-title :title} :pull_request}]
@ -118,34 +128,39 @@
(extract-issue-number pr-body pr-title) (extract-issue-number pr-body pr-title)
(first) (first)
(ensure-bounty-issue owner repo))] (ensure-bounty-issue owner repo))]
(log/debug "Referenced bounty issue found" bounty-issue-number) (log/debug "Referenced bounty issue found" repo bounty-issue-number)
(users/create-user user-id login name nil avatar_url nil) (users/create-user user-id login name nil avatar_url nil)
(let [pr-data {:repo_id repo-id (let [issue (github/get-issue owner repo bounty-issue-number)
pr-data {:repo_id repo-id
:pr_id id :pr_id id
:pr_number pr-number :pr_number pr-number
:user_id user-id :user_id user-id
:issue_number bounty-issue-number :issue_number bounty-issue-number
:issue_id (:id issue)
:state event-type}] :state event-type}]
;; TODO: in the opened case if the submitting user has no
;; Ethereum address stored, we could post a comment to the
;; Github PR explaining that payout is not possible if the PR is
;; merged
(case event-type (case event-type
:opened (do :opened (do
(log/info "PR with reference to bounty issue" (log/info "PR with reference to bounty issue"
bounty-issue-number "opened") bounty-issue-number "opened")
(pull-requests/save (merge pr-data {:state :opened (pull-requests/save (merge pr-data {:state :opened
:commit_id nil}))) :commit_id head-sha})))
:closed (if-let [commit-id (find-commit-id owner :closed (if merged?
repo
pr-number
["merged"])]
(do (log/info "PR with reference to bounty issue" (do (log/info "PR with reference to bounty issue"
bounty-issue-number "merged") bounty-issue-number "merged")
(pull-requests/save (pull-requests/save
(merge pr-data {:state :merged (merge pr-data {:state :merged
:commit_id commit-id}))) :commit_id head-sha}))
(issues/update-commit-id head-sha (:id issue)))
(do (log/info "PR with reference to bounty issue" (do (log/info "PR with reference to bounty issue"
bounty-issue-number "closed with no merge") bounty-issue-number "closed with no merge")
(pull-requests/save (pull-requests/save
(merge pr-data {:state :closed (merge pr-data {:state :closed
:commit_id nil})))))))) :commit_id head-sha}))))))))
(defn handle-issue (defn handle-issue

View File

@ -60,7 +60,7 @@
(when-let [confirm-hash (wallet/find-confirmation-hash receipt)] (when-let [confirm-hash (wallet/find-confirmation-hash receipt)]
(db-bounties/update-confirm-hash issue-id confirm-hash))))) (db-bounties/update-confirm-hash issue-id confirm-hash)))))
(defn update-payout-hash (defn update-payout-receipt
"Gets transaction receipt for each confirmed payout and updates payout_hash" "Gets transaction receipt for each confirmed payout and updates payout_hash"
[] []
(doseq [{issue-id :issue_id (doseq [{issue-id :issue_id
@ -68,7 +68,9 @@
(log/debug "confirmed payout:" payout-hash) (log/debug "confirmed payout:" payout-hash)
(when-let [receipt (eth/get-transaction-receipt payout-hash)] (when-let [receipt (eth/get-transaction-receipt payout-hash)]
(log/info "payout receipt for issue #" issue-id ": " receipt) (log/info "payout receipt for issue #" issue-id ": " receipt)
(db-bounties/update-payout-receipt issue-id receipt)))) ;;TODO: not sure if saving the transaction-receipt clojure map as
;; a string is a good idea
(db-bounties/update-payout-receipt issue-id (str receipt)))))
(defn update-balance (defn update-balance
[] []
@ -137,7 +139,7 @@
:start (restart-scheduler 60000 :start (restart-scheduler 60000
[update-issue-contract-address [update-issue-contract-address
update-confirm-hash update-confirm-hash
update-payout-hash update-payout-receipt
self-sign-bounty self-sign-bounty
update-balance]) update-balance])
:stop (stop-scheduler)) :stop (stop-scheduler))

View File

@ -1,7 +1,63 @@
(ns commiteth.bounties (ns commiteth.bounties
(:require [re-frame.core :as rf])) (:require [re-frame.core :as rf]))
(defn pr-url [{owner :owner_login
pr-number :pr_number
repo :repo_name}]
(str "https://github.com/" owner "/" repo "/pull/" pr-number))
(defn claim-card [claim]
(let [{pr-state :pr_state
user-name :user_name
avatar-url :user_avatar_url
issue-id :issue_id
issue-title :issue_title} claim
merged? (= 1 (:pr_state claim))
paid? ((comp not nil?) (:payout_hash claim))]
(println "paid?" paid? "merged?" merged? (and merged? (comp not) paid?))
[:div.activity-item
[:div.ui.grid.container
[:div.left-column
[:div.ui.circular.image
[:img {:src avatar-url}]]]
[:div.content
[:div.header user-name]
[:div.description "Submitted a claim for " [:a {:href (pr-url claim)}
issue-title]]
[:div.description (if paid?
"(paid)"
(str "(" (if merged? "merged" "open") ")"))]
[:div.time "1 day ago"] ;; TODO: claim timestamp
[:button.ui.button
(merge (if (and merged? (not paid?))
{}
{:disabled true})
{:on-click #(rf/dispatch [:confirm-payout claim])})
(if paid?
"Signed off"
"Confirm")]]]]))
(defn claim-list [bounties]
(into [:div.activity-item-container]
(for [b (keys bounties)
claim (:claims (get bounties b))]
[claim-card claim])))
(defn bounties-page [] (defn bounties-page []
(fn [] (let [owner-bounties (rf/subscribe [:owner-bounties])]
[:div.ui.container (fn []
[:div.ui.text "Bounties view coming soon"]])) (let [paid-out? #(nil? (:payout_hash %))
paid-bounties (into {} (filter paid-out?
@owner-bounties))
unpaid-bounties (into {} (filter (comp not paid-out?)
@owner-bounties))]
(println "unpaid-bounties" unpaid-bounties)
[:div.ui.container
[:h3 "New claims"]
[claim-list unpaid-bounties]
[:h3 "Old claims"]
[claim-list paid-bounties]]))))

View File

@ -175,11 +175,7 @@
:token js/token}])) :token js/token}]))
(reset! active-user nil))) (reset! active-user nil)))
(defn load-issues []
(rf/dispatch [:load-bounties]))
(defn load-data [] (defn load-data []
(load-issues)
(load-user)) (load-user))
(defonce timer-id (r/atom nil)) (defonce timer-id (r/atom nil))

View File

@ -5,12 +5,7 @@
:user nil :user nil
:repos-loading? false :repos-loading? false
:repos {} :repos {}
:all-bounties [] :owner-bounties {}
:owner-bounties []
:error nil
:pagination {}
:pagination-props {:page-size 10
:pages-max 10}
:top-hunters [{:profile-image-url "https://randomuser.me/api/portraits/men/4.jpg" :top-hunters [{:profile-image-url "https://randomuser.me/api/portraits/men/4.jpg"
:display-name "Place Holder" :display-name "Place Holder"
:eth-earned "11 000.00"} :eth-earned "11 000.00"}

View File

@ -2,7 +2,8 @@
(:require [commiteth.db :as db] (:require [commiteth.db :as db]
[re-frame.core :refer [dispatch reg-event-db reg-event-fx reg-fx]] [re-frame.core :refer [dispatch reg-event-db reg-event-fx reg-fx]]
[ajax.core :refer [GET POST]] [ajax.core :refer [GET POST]]
[cuerdas.core :as str])) [cuerdas.core :as str]
[plumbing.core :refer [dissoc-in]]))
(reg-fx (reg-fx
:http :http
@ -59,14 +60,6 @@
(fn [db [_ table page]] (fn [db [_ table page]]
(assoc-in db [:pagination table :page] page))) (assoc-in db [:pagination table :page] page)))
(reg-event-db
:init-pagination
(fn [db [_ bounties]]
(let [{page-size :page-size} (:pagination-props db)]
(assoc-in db [:pagination :all-bounties]
{:page 0
:pages (Math/ceil (/ (count bounties) page-size))}))))
(reg-event-fx (reg-event-fx
:set-active-user :set-active-user
(fn [{:keys [db]} [_ user]] (fn [{:keys [db]} [_ user]]
@ -79,13 +72,6 @@
{:db (assoc db :user nil) {:db (assoc db :user nil)
:redirect {:path "/logout"}})) :redirect {:path "/logout"}}))
(reg-event-fx
:load-bounties
(fn [{:keys [db]} [_]]
{:db db
:http {:method GET
:url "/api/bounties/all"
:on-success #(dispatch [:set-bounties %])}}))
(reg-event-fx (reg-event-fx
:save-payout-hash :save-payout-hash
@ -93,14 +79,10 @@
{:db db {:db db
:http {:method POST :http {:method POST
:url (str/format "/api/user/bounty/%s/payout" issue-id) :url (str/format "/api/user/bounty/%s/payout" issue-id)
:on-success #(println %) :on-success #(dispatch [:payout-confirmed issue-id])
:on-error #(dispatch [:payout-confirm-failed issue-id])
:params {:payout-hash payout-hash}}})) :params {:payout-hash payout-hash}}}))
(reg-event-fx
:set-bounties
(fn [{:keys [db]} [_ bounties]]
{:db (assoc db :all-bounties bounties)
:dispatch [:init-pagination bounties]}))
(reg-event-fx (reg-event-fx
:load-owner-bounties :load-owner-bounties
@ -221,3 +203,54 @@
:clear-updating-address :clear-updating-address
(fn [db _] (fn [db _]
(dissoc db :updating-address))) (dissoc db :updating-address)))
(defn send-transaction-callback
[issue-id]
(println "send-transaction-callback")
(fn [error payout-hash]
(println "send-transaction-callback fn")
(when error
(dispatch [:set-flash-message
:error
(str "Error sending transaction: " error)]))
(when payout-hash
(dispatch [:save-payout-hash issue-id payout-hash]))))
(reg-event-fx
:confirm-payout
(fn [{:keys [db]} [_ {issue-id :issue_id
owner-address :owner_address
contract-address :contract_address
confirm-hash :confirm_hash} issue]]
(let [send-transaction-fn (aget js/web3 "eth" "sendTransaction")
payload {:from owner-address
:to contract-address
:value 1
:data (str "0x797af627" confirm-hash)}]
(println "confirm-payout" owner-address to)
(try
(apply send-transaction-fn [(clj->js payload)
(send-transaction-callback issue-id)])
{: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)
:dispatch [:set-flash-message
:error
(str "Failed to send transaction" e)]})))))
(reg-event-db
:payout-confirmed
(fn [db [_ issue-id]]
(-> db
(dissoc-in [:owner-bounties (:issue_id issue) :confirming?] false)
(assoc-in [:owner-bounties (:issue_id issue) :confirmed?] true))))
(reg-event-db
:payout-confirm-failed
(fn [db [_ issue-id]]
(-> db
(dissoc-in [:owner-bounties (:issue_id issue) :confirming?] false)
(assoc-in [:owner-bounties (:issue_id issue) :confirm-failed?] true))))

View File

@ -1,104 +0,0 @@
(ns commiteth.issues
(:require [reagent.core :as r]
[re-frame.core :as rf]))
(defn- page-btn
[table selected-page current-page]
^{:key current-page}
[:a {:class (when (= selected-page current-page) "current")
:on-click #(rf/dispatch [:set-page table current-page])}
(str (inc current-page))])
(defn pagination [table]
(fn []
(let [{page :page
pages :pages} @(rf/subscribe [:pagination table])
{pages-max :pages-max} @(rf/subscribe [:get-in [:pagination-props]])]
(when (> pages 1)
(let [last-page (dec pages)
start-page (max 1 (min (- pages pages-max) (max 1 (- page (quot pages-max 2)))))
end-page (min (dec pages) (+ pages-max start-page))
gap-left (> start-page 1)
gap-right (< end-page last-page)]
[:div.paginate-container
[:div.pagination
[:span.previous-page
(let [disabled (= page 0)]
{:class (when disabled "disabled")
:on-click (when-not disabled
#(rf/dispatch [:set-page table (dec page)]))})
"Previous"]
(page-btn table page 0)
(when gap-left [:span.gap "…"])
(map #(page-btn table page %) (range start-page end-page))
(when gap-right [:span.gap "…"])
(page-btn table page last-page)
[:span.next-page
(let [disabled (= page last-page)]
{:class (when disabled "disabled")
:on-click (when-not disabled
#(rf/dispatch [:set-page table (inc page)]))})
"Next"]]])))))
(defn repo-link
[owner repo]
(let [coordinates (str owner "/" repo)
url (str "https://github.com/" coordinates)]
[:span
[:svg.octicon.octicon-repo {:height "16"
:version "1.1"
:viewBox "0 0 16 16"
:width "16"}
[:path {:d "M4 9H3V8h1v1zm0-3H3v1h1V6zm0-2H3v1h1V4zm0-2H3v1h1V2zm8-1v12c0 .55-.45 1-1 1H6v2l-1.5-1.5L3 16v-2H1c-.55 0-1-.45-1-1V1c0-.55.45-1 1-1h10c.55 0 1 .45 1 1zm-1 10H1v2h2v-1h3v1h5v-2zm0-10H2v9h9V1z"}]]
[:a.text-gray {:href url} coordinates]]))
(defn issue-url
[owner repo issue-number]
(str "https://github.com/" owner "/" repo "/issues/" issue-number))
(defn issue-row [{title :issue_title
issue-id :issue_id
issue-number :issue_number
owner :owner_name
repo :repo_name}]
^{:key issue-id}
[:li.issue
[:div.d-table.table-fixed.width-full
[:div.float-left.pt-3.pl-3
[:span.tooltipped.tooltipped-n
{:aria-label "Open issue"}
[:svg.octicon.octicon-issue-opened.open
{:aria-hidden "true",
:height "16",
:version "1.1",
:viewBox "0 0 14 16",
:width "14"}
[:path
{:d
"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"}]]]]
[:div.float-left.col-9.p-3.lh-condensed
[:a.box-row-link.h4
{:href (issue-url owner repo issue-number)} title]
[:span.gh-header-number
(str " #" issue-number)]
[:div.mt-1.text-small.text-gray
(repo-link owner repo)]]]])
(defn issues-list-table
[issues-path issue-row-fn]
(fn []
(let [{page :page} @(rf/subscribe (into [:pagination] issues-path))
{page-size :page-size} @(rf/subscribe [:get-in [:pagination-props]])
issues (rf/subscribe issues-path)
issues-page (->> @issues
(drop (* page page-size))
(take page-size))]
[:div.issues-list-table
[:ul.issues-list
(map issue-row-fn issues-page)]])))
(defn issues-page []
(fn []
[:div.container
[(issues-list-table [:all-bounties] issue-row)]
[(pagination :all-bounties)]]))

View File

@ -1,127 +0,0 @@
(ns commiteth.manage
(:require [re-frame.core :as rf]
[commiteth.common :refer [input]]
[commiteth.issues :refer [issues-list-table issue-url]]
[clojure.set :refer [rename-keys]]))
(defn repository-row [repo]
(let [{repo-id :id
url :html_url
name :full_name
description :description} repo]
^{:key repo-id}
[:div.d-table.width-full
[:div.d-table.col-12.width-full.py-4.border-bottom.issue
#_[checkbox {:value-path [:enabled-repos repo-id]
:style {:width 32 :margin-left 10}
:on-change #(rf/dispatch [:toggle-repo repo])}]
[:div.d-table-cell.col-11.pr-3.v-align-top
[:h4.f4
[:a {:href url} name]]
[:p.text-gray.mt-1 description]]]]))
(defn repos-list []
(let [repos-loading? (rf/subscribe [:repos-loading?])
repos (rf/subscribe [:repos])]
(fn []
[:div
(if @repos-loading?
[:i.fa.fa-spinner.fa-spin]
(map repository-row @repos))])))
(defn enable-disable-button
[button-id disable]
(let [button (.getElementById js/document button-id)]
(set! (.-disabled button) disable)))
(defn lock-button
[issue-id]
(enable-disable-button (str "payout" issue-id) "true"))
(defn unlock-button
[issue-id]
(enable-disable-button (str "payout" issue-id) nil))
(defn send-transaction-callback
[issue-id]
(fn [error payout-hash]
(when error
(unlock-button issue-id)
(rf/dispatch [:set-error (str "Error sending transaction: " error)]))
(when payout-hash
(rf/dispatch [:save-payout-hash issue-id payout-hash]))))
(defn send-transaction
[issue]
(fn []
(let [{issue-id :issue_id
owner-address :owner_address
contract-address :contract_address
confirm-hash :confirm_hash} issue
send-transaction-fn (aget js/web3 "eth" "sendTransaction")
payload {:from owner-address
:to contract-address
:value 1
:data (str "0x797af627" confirm-hash)}]
(println "sending transaction" payload)
(lock-button issue-id)
(try
(apply send-transaction-fn [(clj->js payload) (send-transaction-callback issue-id)])
(catch js/Error e (do
(unlock-button issue-id)
(rf/dispatch [:set-error e])))))))
(defn issue-row [{title :issue_title
issue-id :issue_id
issue-number :issue_number
owner :owner_name
repo :repo_name
balance :balance-eth
payout-hash :payout_hash
payout-receipt :payout_receipt
:as issue}]
^{:key issue-id}
[:li.issue
[:div.d-table.table-fixed.width-full
[:div.float-left.pt-3.pl-3
[:span.tooltipped.tooltipped-n
{:aria-label "Closed issue"}
[:svg.octicon.octicon-issue-closed.closed
{:aria-hidden "true",
:height "16",
:version "1.1",
:viewBox "0 0 14 16",
:width "14"}
[:path
{:d
"M7 10h2v2H7v-2zm2-6H7v5h2V4zm1.5 1.5l-1 1L12 9l4-4.5-1-1L12 7l-1.5-1.5zM8 13.7A5.71 5.71 0 0 1 2.3 8c0-3.14 2.56-5.7 5.7-5.7 1.83 0 3.45.88 4.5 2.2l.92-.92A6.947 6.947 0 0 0 8 1C4.14 1 1 4.14 1 8s3.14 7 7 7 7-3.14 7-7l-1.52 1.52c-.66 2.41-2.86 4.19-5.48 4.19v-.01z"}]]]]
(if payout-receipt
[:span.btn-sm.float-right {:disabled true
:style {:margin-top "7px"
:margin-right "7px"}}
(str "Sent " balance " ETH")]
[:button.btn.btn-sm.btn-danger.float-right
{:id (str "payout" issue-id)
:type "submit"
:style {:margin-top "7px"
:margin-right "7px"}
:disabled (or payout-hash (not (exists? js/web3)))
:on-click (send-transaction issue)}
(str "Send " balance " ETH")])
[:div.float-left.col-9.p-3.lh-condensed
[:span.mr-2.float-right.text-gray.text-small
(str " #" issue-number)]
[:a.box-row-link.h4
{:href (issue-url owner repo issue-number)} title]]]])
(defn manage-page []
(fn []
[:div.container
[:h3 "Bounties"]
[(issues-list-table [:owner-bounties] issue-row)]
[:h3 "Repositories"]
[repos-list]]))

View File

@ -23,6 +23,11 @@
&:active,&:focus { &:active,&:focus {
background-color: #97ebe7; background-color: #97ebe7;
} }
&:disabled,&:disabled:active {
background-color: #fff;
color: #2f3f44;
border: #e7e7e7 solid 0.1em!important;
}
} }
.ui.small.button { .ui.small.button {
font-size: 15px!important; font-size: 15px!important;
@ -59,8 +64,8 @@
} }
.ui.mini.circular.image { .ui.mini.circular.image {
height: 35px; height: 32px;
width: 35px!important; width: 32px!important;
} }
.ui.header { .ui.header {
@ -193,15 +198,50 @@ span.dropdown.icon {
cursor: default; cursor: default;
} }
.activity-item-container {
padding: 1em;
}
.activity-item { .activity-item {
.left-column {
margin: 0;
padding-left: 12px;
padding-top: 12px;
width: 100px!important;
.ui.image {
position: relative;
width: 48px;
height: 48px;
margin: 0;
padding: 0;
top: 0;
left: 0;
}
}
border: #e7e7e7 solid 0.1em!important; border: #e7e7e7 solid 0.1em!important;
box-shadow: none!important; box-shadow: none!important;
border-radius: 0.3em!important; border-radius: 0.3em!important;
padding: 0.8em 1em 1.1em!important; padding: 0.8em 1em 1.1em!important;
flex-direction: row!important; flex-direction: row!important;
margin: 1em;
.content {
padding: .7em 0 1em;
font-family: 'postgrotesk';
line-height: 2em;
min-width: 300px!important;
.header {
font-size: 1.2em;
font-weight: bold;
color: #474951;
}
}
} }
.ui.cards>.card { .ui.cards>.card {
border: #e7e7e7 solid 0.1em; border: #e7e7e7 solid 0.1em;
box-shadow: none; box-shadow: none;