diff --git a/Dockerfile b/Dockerfile index 83164d0..b920d3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ WORKDIR /root/ RUN apt-get update RUN apt-get -y install xvfb RUN apt-get -y install wkhtmltopdf +RUN apt-get -y install less COPY --from=builder /usr/src/app/target/uberjar/commiteth.jar . COPY html2png.sh . diff --git a/resources/migrations/20180314124717-store-pr-title.down.sql b/resources/migrations/20180314124717-store-pr-title.down.sql new file mode 100644 index 0000000..d8d5a59 --- /dev/null +++ b/resources/migrations/20180314124717-store-pr-title.down.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."pull_requests" DROP COLUMN "title"; diff --git a/resources/migrations/20180314124717-store-pr-title.up.sql b/resources/migrations/20180314124717-store-pr-title.up.sql new file mode 100644 index 0000000..1a3e001 --- /dev/null +++ b/resources/migrations/20180314124717-store-pr-title.up.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."pull_requests" ADD COLUMN "title" character varying(256); diff --git a/resources/migrations/20180403131200-multiple-claims-in-pr.down.sql b/resources/migrations/20180403131200-multiple-claims-in-pr.down.sql new file mode 100644 index 0000000..096ced8 --- /dev/null +++ b/resources/migrations/20180403131200-multiple-claims-in-pr.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pkey; +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pr_id_key; +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_fkey; + + +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pkey PRIMARY KEY (pr_id); +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pr_id_key UNIQUE (pr_id); diff --git a/resources/migrations/20180403131200-multiple-claims-in-pr.up.sql b/resources/migrations/20180403131200-multiple-claims-in-pr.up.sql new file mode 100644 index 0000000..c7b29fb --- /dev/null +++ b/resources/migrations/20180403131200-multiple-claims-in-pr.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pkey; +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pr_id_key; + +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pkey PRIMARY KEY (pr_id, issue_id); +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pr_id_key UNIQUE (pr_id, issue_id); +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_fkey FOREIGN KEY (issue_id) REFERENCES issues(issue_id); diff --git a/resources/migrations/20180412143511-update-activity-feed-with-claims.down.sql b/resources/migrations/20180412143511-update-activity-feed-with-claims.down.sql new file mode 100644 index 0000000..2fb71ed --- /dev/null +++ b/resources/migrations/20180412143511-update-activity-feed-with-claims.down.sql @@ -0,0 +1,78 @@ +-- restore the previous version of the view +CREATE VIEW "public"."claims_view" AS SELECT i.title AS issue_title, + i.issue_number, + r.repo AS repo_name, + r.owner AS repo_owner, + COALESCE(u.name, u.login) AS user_name, + u.avatar_url AS user_avatar_url, + i.payout_receipt, + p.updated, + i.updated AS issue_updated, + i.balance_eth, + i.tokens, + i.value_usd, + p.state AS pr_state, + i.is_open AS issue_open, + (case when u.address IS NULL THEN false ELSE true END) AS user_has_address + FROM issues i, + users u, + repositories r, + pull_requests p + WHERE r.repo_id = i.repo_id + AND p.issue_id = i.issue_id + AND p.user_id = u.id + AND i.contract_address IS NOT NULL + AND i.comment_id IS NOT NULL + ORDER BY p.updated; + + +CREATE OR REPLACE VIEW "public"."activity_feed_view" AS + SELECT 'open-claim'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.updated + FROM claims_view + WHERE claims_view.pr_state = 0 + AND claims_view.payout_receipt IS NULL + AND claims_view.issue_open IS TRUE +UNION + SELECT 'claim-pending'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NULL +UNION + SELECT 'claim-payout'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.issue_number, + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NOT NULL + ORDER BY 12 DESC; diff --git a/resources/migrations/20180412143511-update-activity-feed-with-claims.up.sql b/resources/migrations/20180412143511-update-activity-feed-with-claims.up.sql new file mode 100644 index 0000000..34cdf7c --- /dev/null +++ b/resources/migrations/20180412143511-update-activity-feed-with-claims.up.sql @@ -0,0 +1,95 @@ +DROP VIEW "public"."claims_view" CASCADE; + +CREATE VIEW "public"."claims_view" AS SELECT i.title AS issue_title, + i.issue_number, + r.repo AS repo_name, + r.owner AS repo_owner, + p.title AS pr_title, -- added + p.pr_number AS pr_number, -- added + p.pr_id AS pr_id, -- added + i.issue_id AS issue_id, -- added + COALESCE(u.name, u.login) AS user_name, + u.avatar_url AS user_avatar_url, + i.payout_receipt, + p.updated, + i.updated AS issue_updated, + i.balance_eth, + i.tokens, + i.value_usd, + p.state AS pr_state, + i.is_open AS issue_open, + (case when u.address IS NULL THEN false ELSE true END) AS user_has_address + FROM issues i, + users u, + repositories r, + pull_requests p + WHERE r.repo_id = i.repo_id + AND p.issue_id = i.issue_id + AND p.user_id = u.id + AND i.contract_address IS NOT NULL + AND i.comment_id IS NOT NULL + ORDER BY p.updated; + + +CREATE VIEW "public"."activity_feed_view" AS -- altered + SELECT 'open-claim'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.pr_title, -- added + claims_view.pr_number, -- added + claims_view.pr_id, -- added + claims_view.issue_number, + claims_view.issue_id, -- added + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.updated + FROM claims_view + WHERE claims_view.pr_state = 0 + AND claims_view.payout_receipt IS NULL + AND claims_view.issue_open IS TRUE +UNION + SELECT 'claim-pending'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.pr_title, -- added + claims_view.pr_number, -- added + claims_view.pr_id, -- added + claims_view.issue_number, + claims_view.issue_id, -- added + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NULL +UNION + SELECT 'claim-payout'::text AS type, + claims_view.issue_title, + claims_view.repo_name, + claims_view.repo_owner, + claims_view.pr_title, -- added + claims_view.pr_number, -- added + claims_view.pr_id, -- added + claims_view.issue_number, + claims_view.issue_id, -- added + claims_view.user_name, + claims_view.user_avatar_url, + claims_view.balance_eth, + claims_view.tokens, + claims_view.value_usd, + claims_view.user_has_address, + claims_view.issue_updated AS updated + FROM claims_view + WHERE claims_view.pr_state = 1 + AND claims_view.payout_receipt IS NOT NULL + ORDER BY 12 DESC; diff --git a/resources/public/blue-arrow-down.png b/resources/public/blue-arrow-down.png new file mode 100644 index 0000000..962ed79 Binary files /dev/null and b/resources/public/blue-arrow-down.png differ diff --git a/resources/public/blue-arrow-up.png b/resources/public/blue-arrow-up.png new file mode 100644 index 0000000..16774b7 Binary files /dev/null and b/resources/public/blue-arrow-up.png differ diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index c17641b..3094e0d 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -207,6 +207,7 @@ AND i.transaction_hash IS NOT NULL; INSERT INTO pull_requests (pr_id, repo_id, pr_number, + title, issue_number, issue_id, commit_sha, @@ -215,16 +216,18 @@ INSERT INTO pull_requests (pr_id, VALUES(:pr_id, :repo_id, :pr_number, + :title, :issue_number, :issue_id, :commit_sha, :user_id, :state) -ON CONFLICT (pr_id) DO UPDATE +ON CONFLICT (pr_id,issue_id) DO UPDATE SET state = :state, issue_number = :issue_number, issue_id = :issue_id, + title = :title, updated = timezone('utc'::text, now()), commit_sha = :commit_sha; @@ -573,7 +576,11 @@ SELECT issue_title, repo_name, repo_owner, + pr_number, + pr_title, + pr_id, issue_number, + issue_id, user_name, user_avatar_url, balance_eth, diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj index ed74f4a..95ab1e8 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -21,34 +21,36 @@ (defn deploy-contract [owner owner-address repo issue-id issue-number] (if (empty? owner-address) - (log/error "Unable to deploy bounty contract because" - "repo owner has no Ethereum addres") - (do - (log/info "deploying contract to " owner-address) - (if-let [transaction-hash (multisig/deploy-multisig owner-address)] + (log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id) + (try + (log/infof "issue %s: Deploying contract to %s" issue-id owner-address) + (if-let [transaction-hash (multisig/deploy-multisig {:owner owner-address + :internal-tx-id (str "contract-github-issue-" issue-id)})] (do - (log/info "Contract deployed, transaction-hash:" - transaction-hash) + (log/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id transaction-hash) (github/update-comment (to-map owner repo issue-number transaction-hash)) +(log/infof "issue %s: post-deploying-comment response: %s" issue-id resp) (issues/update-transaction-hash issue-id transaction-hash)) - (log/error "Failed to deploy contract to" owner-address))))) - + (log/errorf "issue %s Failed to deploy contract to %s" issue-id owner-address)) + (catch Exception ex (log/errorf ex "issue %s: deploy-contract exception" issue-id))))) + (defn add-bounty-for-issue [repo repo-id issue] (let [{issue-id :id issue-number :number issue-title :title} issue created-issue (issues/create repo-id issue-id issue-number issue-title) {:keys [address owner]} (users/get-repo-owner repo-id)] - (log/debug "Adding bounty for issue " repo issue-number "owner address: " address) + (log/debug "issue %s: Adding bounty for issue %s/%s - owner address: %s" + issue-id repo issue-number owner-address) (if (= 1 created-issue) (deploy-contract owner address repo issue-id issue-number) - (log/debug "Issue already exists in DB, ignoring")))) + (log/debug "issue %s: Issue already exists in DB, ignoring")))) (defn maybe-add-bounty-for-issue [repo repo-id issue] (let [res (issues/get-issues-count repo-id) {count :count} res - limit-reached? (> count max-issues-limit) - _ (log/debug "*** get-issues-count" repo-id " " res " " count " " limit-reached?)] + limit-reached? (> count max-issues-limit)] + (log/debug "*** get-issues-count" repo-id " " res " " count " " limit-reached?) (if limit-reached? (log/debug "Total issues for repo limit reached " repo " " count) (add-bounty-for-issue repo repo-id issue)))) @@ -76,9 +78,7 @@ (issues/get-issue-titles)] (let [gh-issue (github/get-issue owner repo issue-number)] (if-not (= title (:title gh-issue)) - (do - (log/info "Updating changed title for issue" (:id gh-issue)) - (issues/update-issue-title (:id gh-issue) (:title gh-issue))))))) + (issues/update-issue-title (:id gh-issue) (:title gh-issue)))))) (defn assert-keys [m ks] (doseq [k ks] diff --git a/src/clj/commiteth/db/issues.clj b/src/clj/commiteth/db/issues.clj index 806828c..1ffc8e3 100644 --- a/src/clj/commiteth/db/issues.clj +++ b/src/clj/commiteth/db/issues.clj @@ -2,7 +2,8 @@ (:require [commiteth.db.core :refer [*db*] :as db] [clojure.java.jdbc :as jdbc] [commiteth.util.util :refer [to-db-map]] - [clojure.set :refer [rename-keys]])) + [clojure.set :refer [rename-keys]] + [clojure.tools.logging :as log])) (defn create "Creates issue" @@ -32,6 +33,7 @@ (defn update-issue-title [issue-id title] + (log/info "issue %s: Updating changed title \"%s\"" issue-id title) (jdbc/with-db-connection [con-db *db*] (db/update-issue-title con-db {:issue_id issue-id :title title}))) diff --git a/src/clj/commiteth/db/repositories.clj b/src/clj/commiteth/db/repositories.clj index eb06b38..9e82b7b 100644 --- a/src/clj/commiteth/db/repositories.clj +++ b/src/clj/commiteth/db/repositories.clj @@ -34,7 +34,7 @@ (defn update-repo-state [repo-id repo-state] (jdbc/with-db-connection [con-db *db*] - (db/update-repo-name con-db {:repo_id repo-id + (db/update-repo-state con-db {:repo_id repo-id :repo_state repo-state}))) (defn get-repo "Get a repo from DB given it's full name (owner/repo-name)" diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 4db60c1..93c9352 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -28,7 +28,9 @@ (defn auto-gas-price? [] (env :auto-gas-price false)) (defn offline-signing? [] (env :offline-signing true)) -(def web3j-obj (atom nil)) +(def web3j-obj + (delay (Web3j/build (HttpService. (eth-rpc-url))))) + (def creds-obj (atom nil)) (defn wallet-file-path [] @@ -37,10 +39,6 @@ (defn wallet-password [] (env :eth-password)) -(defn create-web3j [] - (or @web3j-obj - (swap! web3j-obj (constantly (Web3j/build (HttpService. (eth-rpc-url))))))) - (defn creds [] (or @creds-obj (let [password (wallet-password) @@ -53,27 +51,59 @@ (throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn" {:password password :file-path file-path})))))) -(defn get-signed-tx [gas-price gas-limit to data] - "Create a sign a raw transaction. - 'From' argument is not needed as it's already - encoded in credentials. +(defn get-web3j-nonce [web3j-instance] + (.. (.ethGetTransactionCount web3j-instance (env :eth-account) DefaultBlockParameterName/LATEST) + sendAsync + get + getTransactionCount)) + +(defprotocol INonceTracker + "The reason we need this is that we might send mutliple identical + transactions (e.g. bounty contract deploy with same owner) shortly + after another. In this case web3j's TX counting only increases once + a transaction has been confirmed and so we send multiple identical + transactions with the same nonce. + + Web3j also provides a TransactionManager that increases nonces but it + does not track nonces related to transactions and so as far as I understand + it might cause transactions to be executed twice if they are retried. + + https://github.com/web3j/web3j/blob/d19855475aa6620a7e93523bd9ede26ca50ed042/core/src/main/java/org/web3j/tx/RawTransactionManager.java" + (get-nonce [this internal-tx-id] + "Return the to be used nonce for an OpenBounty Ethereum + transaction identified by `internal-tx-id`. As these IDs are stable + we can use them to use consistent nonces for the same transaction.")) + +(defrecord NonceTracker [state] + INonceTracker + (get-nonce [this internal-tx-id] + (let [prev-nonce (get @state internal-tx-id) + web3j-nonce (get-web3j-nonce @web3j-obj) + nonces (set (vals @state)) + nonce (if (seq nonces) + (inc (apply max nonces)) + web3j-nonce)] + (when prev-nonce + (log/warnf "%s: tx will be retried (prev-nonce: %s, new-nonce: %s, web3j-nonce: %s)" + internal-tx-id prev-nonce nonce web3j-nonce)) + ;; TODO this is a memory leak since tracking state is never pruned + ;; Since we're not doing 1000s of transactions every day yet we can + ;; probably defer worrying about this until a bit later + (swap! state assoc internal-tx-id nonce) + nonce))) + +(def nonce-tracker + (->NonceTracker (atom {}))) + +(defn get-signed-tx [{:keys [gas-price gas-limit to data internal-tx-id]}] + "Create a sign a raw transaction. 'From' argument is not needed as it's already encoded in credentials. See https://web3j.readthedocs.io/en/latest/transactions.html#offline-transaction-signing" - (let [web3j (create-web3j) - nonce (.. (.ethGetTransactionCount web3j - (env :eth-account) - DefaultBlockParameterName/LATEST) - sendAsync - get - getTransactionCount) - tx (RawTransaction/createTransaction - nonce - gas-price - gas-limit - to - data) - signed (TransactionEncoder/signMessage tx (creds)) - hex-string (Numeric/toHexString signed)] - hex-string)) + (let [nonce (get-nonce nonce-tracker internal-tx-id)] + (log/infof "%s: Signing nonce: %s, gas-price: %s, gas-limit: %s" + internal-tx-id nonce gas-price gas-limit) + (-> (RawTransaction/createTransaction (biginteger nonce) gas-price gas-limit to data) + (TransactionEncoder/signMessage (creds)) + (Numeric/toHexString)))) (defn eth-gasstation-gas-price "Get max of average and average_calc from gas station and use that @@ -122,19 +152,27 @@ nil))))) +(def req-id-tracker + ;; HACK to ensure previous random-number approach doesn't lead to + ;; unintended collisions + (atom 0)) + (defn eth-rpc - [method params] - (let [request-id (rand-int 4096) - body (json/write-str {:jsonrpc "2.0" - :method method - :params params - :id request-id}) + [{:keys [method params internal-tx-id]}] + {:pre [(string? method) (some? params)]} + (let [request-id (swap! req-id-tracker inc) + body {:jsonrpc "2.0" + :method method + :params params + :id request-id} options {:headers {"content-type" "application/json"} - :body body} + :body (json/write-str body)} response @(post (eth-rpc-url) options) result (safe-read-str (:body response))] - (log/debug body "\n" result) - + (when internal-tx-id + (log/infof "%s: eth-rpc %s" internal-tx-id method)) + (log/debugf "%s: eth-rpc req(%s) body: %s" internal-tx-id request-id body) + (log/debugf "%s: eth-rpc req(%s) result: %s" internal-tx-id request-id result) (cond ;; Ignore any responses that have mismatching request ID (not= (:id result) request-id) @@ -142,7 +180,10 @@ ;; If request ID matches but contains error, throw (:error result) - (throw (ex-info "Error submitting transaction via eth-rpc" (:error result))) + (throw + (ex-info (format "%s: Error submitting transaction via eth-rpc %s" + (or internal-tx-id "(no-tx-id)") (:error result)) + (:error result))) :else (:result result)))) @@ -179,9 +220,10 @@ (defn estimate-gas [from to value & [params]] (let [geth-estimate (eth-rpc - "eth_estimateGas" [(merge params {:from from - :to to - :value value})]) + {:method "eth_estimateGas" + :params [(merge params {:from from + :to to + :value value})]}) adjusted-gas (adjust-gas-estimate geth-estimate)] (log/debug "estimated gas (geth):" geth-estimate) @@ -204,7 +246,8 @@ (defn get-balance-hex [account] - (eth-rpc "eth_getBalance" [account "latest"])) + (eth-rpc {:method "eth_getBalance" + :params [account "latest"]})) (defn get-balance-wei [account] @@ -223,7 +266,8 @@ (defn get-transaction-receipt [hash] - (eth-rpc "eth_getTransactionReceipt" [hash])) + (eth-rpc {:method "eth_getTransactionReceipt" + :params [hash]})) (defn format-call-params [method-id & params] @@ -233,10 +277,12 @@ (defn call [contract method-id & params] (let [data (apply format-call-params method-id params)] - (eth-rpc "eth_call" [{:to contract :data data} "latest"]))) + (eth-rpc {:method "eth_call" + :params [{:to contract :data data} "latest"]}))) (defn execute - [from contract method-id gas-limit & params] + [{:keys [from contract method-id gas-limit params internal-tx-id]}] + {:pre [(string? method-id)]} (let [data (apply format-call-params method-id params) gas-price (gas-price) value (format "0x%x" 0) @@ -247,21 +293,23 @@ (merge {:gasPrice (integer->hex gas-price)}) contract (merge {:to contract})) - gas (if gas-limit gas-limit - (estimate-gas from contract value params)) + gas (or gas-limit (estimate-gas from contract value params)) params (if (offline-signing?) - (get-signed-tx (biginteger gas-price) - (hex->big-integer gas) - contract - data) + (get-signed-tx {:internal-tx-id internal-tx-id + :gas-price (biginteger gas-price) + :gas-limit (hex->big-integer gas) + :to contract + :data data}) (assoc params :gas gas))] (if (offline-signing?) (eth-rpc - "eth_sendRawTransaction" - [params]) + {:method "eth_sendRawTransaction" + :params [params] + :internal-tx-id internal-tx-id}) (eth-rpc - "personal_sendTransaction" - [params (eth-password)])))) + {:method "personal_sendTransaction" + :params [params (eth-password)] + :internal-tx-id internal-tx-id})))) (defn hex-ch->num [ch] @@ -322,7 +370,6 @@ eth-core :start (do - (swap! web3j-obj (constantly nil)) (swap! creds-obj (constantly nil)) (log/info "eth/core started")) :stop diff --git a/src/clj/commiteth/eth/multisig_wallet.clj b/src/clj/commiteth/eth/multisig_wallet.clj index 941b0ca..874c113 100644 --- a/src/clj/commiteth/eth/multisig_wallet.clj +++ b/src/clj/commiteth/eth/multisig_wallet.clj @@ -1,6 +1,5 @@ (ns commiteth.eth.multisig-wallet - (:require [commiteth.eth.core :as eth - :refer [create-web3j creds]] + (:require [commiteth.eth.core :as eth] [commiteth.config :refer [env]] [clojure.tools.logging :as log] [taoensso.tufte :as tufte :refer (defnp p profiled profile)] @@ -36,24 +35,19 @@ [] (env :tokenreg-base-format :status)) -(defn create-new - [owner1 owner2 required] - (eth/execute (eth/eth-account) - (factory-contract-addr) - (:create method-ids) - (eth/integer->hex 865000) ;; gas-limit - 0x40 - 0x2 - required - owner1 - owner2)) - - (defn deploy-multisig "Deploy a new multisig contract to the blockchain with commiteth bot - and given owner as owners." - [owner] - (create-new (eth/eth-account) owner 2)) + and given owner as owners. + `internal-tx-id` is used to identify what issue this multisig is deployed + for and manage nonces at a later point in time." + [{:keys [owner internal-tx-id]}] + {:pre [(string? owner) (string? internal-tx-id)]} + (eth/execute {:internal-tx-id internal-tx-id + :from (eth/eth-account) + :contract (factory-contract-addr) + :method-id (:create method-ids) + :gas-limit (eth/integer->hex 865000) + :params [0x40 0x2 2 (eth/eth-account) owner]})) (defn find-event-in-tx-receipt [tx-receipt topic-id] (let [logs (:logs tx-receipt) @@ -92,20 +86,18 @@ (defn send-all - [contract to] - (log/debug "multisig/send-all " contract to) + [{:keys [contract payout-address internal-tx-id]}] + {:pre [(string? contract) (string? payout-address) (string? internal-tx-id)]} + (log/debug "multisig/send-all " contract payout-address internal-tx-id) (let [params (eth/format-call-params (:withdraw-everything method-ids) - to)] - (eth/execute (eth/eth-account) - contract - (:submit-transaction method-ids) - nil - contract - 0 - "0x60" ;; TODO: document these - "0x24" ;; or refactor out - params))) + payout-address)] + (eth/execute {:internal-tx-id internal-tx-id + :from (eth/eth-account) + :contract contract + :method-id (:submit-transaction method-ids) + :gas-limit nil + :params [contract 0 "0x60" "0x24" params]}))) (defn get-token-address [token] @@ -118,17 +110,17 @@ (log/debug "multisig/watch-token" bounty-addr token) (let [token-address (get-token-address token)] (assert token-address) - (eth/execute (eth/eth-account) - bounty-addr - (:watch method-ids) - nil - token-address))) - + (eth/execute {:internal-tx-id (str "watch-token-" (System/currentTimeMillis) "-" bounty-addr) + :from (eth/eth-account) + :contract bounty-addr + :method-id (:watch method-ids) + :gas-limit nil + :params [token-address]}))) (defn load-bounty-contract [addr] (MultiSigTokenWallet/load addr - (create-web3j) - (creds) + @eth/web3j-obj + (eth/creds) (eth/gas-price) (BigInteger/valueOf 500000))) diff --git a/src/clj/commiteth/eth/token_registry.clj b/src/clj/commiteth/eth/token_registry.clj index 4d1ddea..c7aeec7 100644 --- a/src/clj/commiteth/eth/token_registry.clj +++ b/src/clj/commiteth/eth/token_registry.clj @@ -1,6 +1,5 @@ (ns commiteth.eth.token-registry - (:require [commiteth.eth.core :as eth - :refer [create-web3j creds]] + (:require [commiteth.eth.core :as eth] [commiteth.config :refer [env]] [clojure.tools.logging :as log]) (:import [org.web3j @@ -23,8 +22,8 @@ (defn- load-tokenreg-contract [addr] (TokenReg/load addr - (create-web3j) - (creds) + @eth/web3j-obj + (eth/creds) (eth/gas-price) (BigInteger/valueOf 21000))) @@ -59,9 +58,8 @@ (defn deploy-parity-tokenreg "Deploy an instance of parity token-registry to current network" [] - (let [web3j (create-web3j)] - (TokenReg/deploy web3j - (creds) - (eth/gas-price) - (BigInteger/valueOf 4000000) ;; gas limit - BigInteger/ZERO))) + (TokenReg/deploy @eth/web3j-obj + (eth/creds) + (eth/gas-price) + (BigInteger/valueOf 4000000) ;; gas limit + BigInteger/ZERO)) diff --git a/src/clj/commiteth/github/core.clj b/src/clj/commiteth/github/core.clj index 9e293d9..9a248d8 100644 --- a/src/clj/commiteth/github/core.clj +++ b/src/clj/commiteth/github/core.clj @@ -341,6 +341,8 @@ tokens winner-login) )] + (when (and (= state :merged) (empty? winner-address)) + (log/warn "issue %s: Cannot sign pending bounty - winner has no payout address" issue-id)) (when (= :paid state) (db-bounties/update-payout-receipt issue-id payout-receipt)) diff --git a/src/clj/commiteth/routes/webhooks.clj b/src/clj/commiteth/routes/webhooks.clj index a3728a7..88b374d 100644 --- a/src/clj/commiteth/routes/webhooks.clj +++ b/src/clj/commiteth/routes/webhooks.clj @@ -115,18 +115,18 @@ (defn handle-claim - [issue user-id login name avatar_url owner repo repo-id pr-id pr-number head-sha merged? event-type] + [issue user-id login name avatar_url owner repo repo-id pr-id pr-number pr-title head-sha merged? event-type] (users/create-user user-id login name nil avatar_url) (let [open-or-edit? (contains? #{:opened :edited} event-type) close? (= :closed event-type) pr-data {:repo_id repo-id :pr_id pr-id :pr_number pr-number + :title pr-title :user_id user-id :issue_number (:issue_number issue) :issue_id (:issue_id issue) :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 @@ -174,22 +174,24 @@ pr-body :body pr-title :title} :pull_request}] (log/info "handle-pull-request-event" event-type owner repo repo-id login pr-body pr-title) - (if-let [issue (some #(issues/get-issue repo-id %1) (extract-issue-number owner repo pr-body pr-title))] - (if-not (:commit_sha issue) ; no PR has been merged yet referencing this issue - (do - (log/info "Referenced bounty issue found" owner repo (:issue_number issue)) - (handle-claim issue - user-id - login name - avatar_url - owner repo - repo-id - pr-id - pr-number - head-sha - merged? - event-type)) - (log/info "PR for issue already merged")) + (if-let [issues (remove nil? (map #(issues/get-issue repo-id %1) (extract-issue-number owner repo pr-body pr-title)))] + (doseq [issue issues] + (if-not (:commit_sha issue) ; no PR has been merged yet referencing this issue + (do + (log/info "Referenced bounty issue found" owner repo (:issue_number issue)) + (handle-claim issue + user-id + login name + avatar_url + owner repo + repo-id + pr-id + pr-number + pr-title + head-sha + merged? + event-type)) + (log/info "PR for issue already merged"))) (when (= :edited event-type) ; Remove PR if it does not reference any issue (pull-requests/remove pr-id)))) @@ -383,7 +385,7 @@ (log/debug "webhook-app POST, headers" headers) (let [raw-payload (slurp body) payload (json/parse-string raw-payload true)] - (log/info "webhook-app POST, payload:" (pr-str payload)) + (log/debug "webhook-app POST, payload:" (pr-str payload)) (if (validate-secret-one-hook payload raw-payload (get headers "x-hub-signature")) (do (log/debug "Github secret validation OK app") diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index 04d8136..c38d613 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -38,25 +38,26 @@ (update-balances))) ) + (defn update-issue-contract-address "For each pending deployment: gets transaction receipt, updates db state (contract-address, comment-id) and posts github comment" [] (log/info "In update-issue-contract-address") (p :update-issue-contract-address +<<<<<<< HEAD (doseq [{:keys [issue-id transaction-hash]} (issues/list-pending-deployments)] - (log/info "pending deployment:" transaction-hash) + (log/infof "issue %s: pending deployment: %s" issue-id transaction-hash) (try (when-let [receipt (eth/get-transaction-receipt transaction-hash)] - (log/info "update-issue-contract-address: transaction receipt for issue #" - issue-id ": " receipt) + (log/infof "issue %s: update-issue-contract-address: tx receipt: %s" issue-id receipt) (if-let [contract-address (multisig/find-created-multisig-address receipt)] (let [{:keys [owner repo comment-id issue-number] :as issue} (issues/update-contract-address issue-id contract-address) balance-eth-str (eth/get-balance-eth contract-address 6) balance-eth (read-string balance-eth-str) tokens {}] - (log/info "Updating comment") + (log/infof "issue %s: Updating comment" issue-id) (github/update-comment (to-map issue-id owner repo @@ -66,10 +67,9 @@ balance-eth balance-eth-str tokens))) - (log/error "Failed to find contract address in tx logs"))) + (log/errorf "issue %s: Failed to find contract address in tx logs" issue-id))) (catch Throwable ex - (do (log/error "update-issue-contract-address exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) + (log/errorf ex "issue %s: update-issue-contract-address exception:" issue-id))))) (log/info "Exit update-issue-contract-address")) @@ -79,26 +79,37 @@ (p :deploy-pending-contracts (doseq [{:keys [issue-id issue-number owner owner-address repo]} (db-bounties/pending-contracts)] - (log/debug "Trying to re-deploy failed bounty contract deployment, issue-id:" issue-id) - (bounties/deploy-contract owner owner-address repo issue-id issue-number)))) + (log/infof "issue %s: Trying to re-deploy failed bounty contract deployment" issue-id) + (try + (bounties/deploy-contract owner owner-address repo issue-id issue-number) + (doseq [{issue-id :issue_id + issue-number :issue_number + owner :owner + owner-address :owner_address + repo :repo} (db-bounties/pending-contracts)] + (log/infof "issue %s: Trying to re-deploy failed bounty contract deployment" issue-id) + (try + (bounties/deploy-contract owner owner-address repo issue-id issue-number) + (catch Throwable t + (log/errorf t "issue %s: deploy-pending-contracts exception: %s" issue-id (ex-data t))))))) (defn self-sign-bounty "Walks through all issues eligible for bounty payout and signs corresponding transaction" [] (log/info "In self-sign-bounty") (p :self-sign-bounty +<<<<<<< HEAD (doseq [{:keys [contract-address winner-address issue-id winner-login] :as issue} (db-bounties/pending-bounties)] (try (let [value (eth/get-balance-hex contract-address)] (when-not (empty? winner-address) (let [execute-hash (multisig/send-all contract-address winner-address)] - (log/info "Payout self-signed, called sign-all(" contract-address winner-address ") tx:" execute-hash) + (log/infof "issue %s: Payout self-signed, called sign-all(%s) tx: %s" issue-id contract-address winner-address execute-hash) (db-bounties/update-execute-hash-and-winner-login issue-id execute-hash winner-login))) (github/update-comment issue)) (catch Throwable ex - (do (log/error "self-sign-bounty exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) + (log/error ex "issue %s: self-sign-bounty exception" issue-id))))) (log/info "Exit self-sign-bounty")) (defn update-confirm-hash @@ -107,13 +118,16 @@ (log/info "In update-confirm-hash") (p :update-confirm-hash (doseq [{:keys [issue-id execute-hash]} (db-bounties/pending-payouts)] - (log/info "pending payout:" execute-hash) - (when-let [receipt (eth/get-transaction-receipt execute-hash)] - (log/info "execution receipt for issue #" issue-id ": " receipt) - (when-let [confirm-hash (multisig/find-confirmation-tx-id receipt)] - (log/info "confirm hash:" confirm-hash) - (db-bounties/update-confirm-hash issue-id confirm-hash))))) - (log/info "Exit update-confirm-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) + (db-bounties/update-confirm-hash issue-id confirm-hash))) + (catch Throwable ex + (log/errorf ex "issue %s: update-confirm-hash exception:" issue-id))) ) + (log/info "Exit update-confirm-hash"))) (defn update-watch-hash @@ -121,9 +135,13 @@ [] (p :update-watch-hash (doseq [{:keys [issue-id watch-hash]} (db-bounties/pending-watch-calls)] - (log/info "pending watch call" watch-hash) - (when-let [receipt (eth/get-transaction-receipt watch-hash)] - (db-bounties/update-watch-hash issue-id nil))))) + (log/infof "issue %s: pending watch call %s" issue-id watch-hash) + (try + (when-let [receipt (eth/get-transaction-receipt watch-hash)] + (db-bounties/update-watch-hash issue-id nil)) + (catch Throwable ex + (log/errorf ex "issue %s: update-watch-hash exception:" issue-id)) + )))) (defn older-than-3h? @@ -141,7 +159,7 @@ (p :update-payout-receipt (doseq [{:keys [payout-hash contract-address confirm-hash issue-id updated] :as issue} (db-bounties/confirmed-payouts)] - (log/debug "confirmed payout:" payout-hash) + (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) @@ -150,23 +168,21 @@ (some #(> (second %) 0.0) contract-tokens) (> contract-eth-balance 0)) (do - (log/info "Contract still has funds") + (log/infof "issue %s: Contract (%s) still has funds" issue-id contract-address) (when (multisig/is-confirmed? contract-address confirm-hash) - (log/info "Detected bounty with funds and confirmed payout, calling executeTransaction") + (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-hash)] - (log/info "execute tx:" execute-tx-hash)))) + (log/infof "issue %s: execute tx: %s" issue-id execute-tx-hash)))) (do - (log/info "Payout has succeeded, saving payout receipt for issue #" issue-id ": " receipt) + (log/infof "issue %s: Payout has succeeded, payout receipt %s" issue-id receipt) (github/update-comment (assoc issue :payout-receipt receipt))))) (when (older-than-3h? updated) - (log/info "Resetting payout hash for issue" issue-id "as it has not been mined in 3h") + (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 - (do (log/error "update-payout-receipt exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) - (log/info "Exit update-payout-receipt") - ) + (catch Throwable ex + (log/error ex "issue %s: update-payout-receipt exception" issue-id))))) + (log/info "Exit update-payout-receipt")) (defn abs "(abs n) is the absolute value of n" @@ -179,26 +195,27 @@ (defn update-bounty-token-balances - "Helper function for updating internal ERC20 token balances to token multisig contract. Will be called periodically for all open bounty contracts." + "Helper function for updating internal ERC20 token balances to token + multisig contract. Will be called periodically for all open bounty + contracts." [issue-id bounty-addr watch-hash] - #_(log/info "In update-bounty-token-balances for issue" issue-id) + (log/info "In update-bounty-token-balances for issue" issue-id) (doseq [[tla token-data] (token-data/as-map)] (try (let [balance (multisig/token-balance bounty-addr tla)] (when (> balance 0) (do - (log/info "bounty at" bounty-addr "has" balance "of token" tla) + (log/infof "bounty %s: has %s of token %s" bounty-addr balance tla) (let [internal-balance (multisig/token-balance-in-bounty bounty-addr tla)] (when (and (nil? watch-hash) (not= balance internal-balance)) - (log/info "balances not in sync, calling watch") + (log/infof "bounty %s: balances not in sync, calling watch" bounty-addr) (let [hash (multisig/watch-token bounty-addr tla)] (db-bounties/update-watch-hash issue-id hash))))))) - (catch Throwable ex - (do (log/error "update-bounty-token-balances exception:" ex) - (clojure.stacktrace/print-stack-trace ex))))) - #_(log/info "Exit update-bounty-token-balances")) - + (catch Throwable ex + (log/error ex "bounty %s: update-bounty-token-balances exception" bounty-addr)))) + (log/info "Exit update-bounty-token-balances")) + (defn update-contract-internal-balances "It is required in our current smart contract to manually update it's internal balance when some tokens have been added." @@ -256,8 +273,7 @@ (merge token-balances {:ETH current-balance-eth}))) (github/update-comment issue)))) (catch Throwable ex - (do (log/error "update-balances exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) + (log/error ex "issue %s: update-balances exception" issue-id))))) (log/info "Exit update-balances")) @@ -265,7 +281,7 @@ (try (func) (catch Throwable t - (log/error t)))) + (log/error t (.getMessage t) (ex-data t))))) (defn run-tasks [tasks] (doall diff --git a/src/cljs/commiteth/bounties.cljs b/src/cljs/commiteth/bounties.cljs index f1fa460..bdcd41e 100644 --- a/src/cljs/commiteth/bounties.cljs +++ b/src/cljs/commiteth/bounties.cljs @@ -4,51 +4,95 @@ [commiteth.common :refer [human-time display-data-page items-per-page - issue-url]] + issue-url + pull-request-url]] [commiteth.handlers :as handlers] [commiteth.db :as db] [commiteth.ui-model :as ui-model] [commiteth.subscriptions :as subs] [commiteth.util :as util])) +(defn display-bounty-claims [claims] + [:div.bounty-claims-container.ph4.pv3 + (for [claim claims] + (let [{:keys [avatar-url + pr-title + pr-id + updated + repo-owner + repo-name + pr-number]} claim] + ^{:key pr-id} + [:div.bounty-claims-row.open-bounty-item-content.flex + [:div.bounty-claims-icon.pl2 + [:div.ui.tiny.circular.image + [:img {:src avatar-url}]]] + [:span.bounty-claims-text.pt2.pl2 + [:a.fw5 + {:href (pull-request-url repo-owner repo-name pr-number)} + (if pr-title + pr-title + (str "#" pr-number " by " repo-owner " in " repo-name))] + [:span.time.pl2 (human-time updated)]]]))]) + +(defn blue-arrow-box [image-src] + "generates the appropriate container for a blue arrow" + [:span.blue-arrow-box.pa1 + [:img.blue-arrow-image.v-mid {:src image-src}]]) (defn bounty-item [bounty] - (let [{avatar-url :repo_owner_avatar_url - owner :repo-owner - repo-name :repo-name - issue-title :issue-title - issue-number :issue-number - updated :updated - tokens :tokens - balance-eth :balance-eth - value-usd :value-usd - claim-count :claim-count} bounty - full-repo (str owner "/" repo-name) - repo-url (str "https://github.com/" full-repo) - repo-link [:a {:href repo-url} full-repo] - issue-link [:a - {:href (issue-url owner repo-name issue-number)} - issue-title]] - [:div.open-bounty-item - [:div.open-bounty-item-content - [:div.header issue-link] - [:div.bounty-item-row - [:div.time (human-time updated)] - [:span.bounty-repo-label repo-link]] - - [:div.footer-row - [:div.balance-badge "ETH " balance-eth] - (for [[tla balance] tokens] - ^{:key (random-uuid)} - [:div.balance-badge.token - (str (subs (str tla) 1) " " balance)]) - [:span.usd-value-label "Value "] [:span.usd-balance-label (str "$" value-usd)] - (when (> claim-count 0) - [:span.open-claims-label (str claim-count " open claim" - (when (> claim-count 1) "s"))])]] - [:div.open-bounty-item-icon - [:div.ui.tiny.circular.image - [:img {:src avatar-url}]]]])) + (let [open-bounty-claims (rf/subscribe [::subs/open-bounty-claims])] + (fn [bounty] + (let [{avatar-url :repo_owner_avatar_url + owner :repo-owner + repo-name :repo-name + issue-title :issue-title + issue-number :issue-number + issue-id :issue-id + updated :updated + tokens :tokens + balance-eth :balance-eth + value-usd :value-usd + claim-count :claim-count + claims :claims} bounty + full-repo (str owner "/" repo-name) + repo-url (str "https://github.com/" full-repo) + repo-link [:a {:href repo-url} full-repo] + issue-link [:a + {:href (issue-url owner repo-name issue-number)} + issue-title] + open-claims-click #(rf/dispatch [::handlers/open-bounty-claim issue-id]) + close-claims-click #(rf/dispatch [::handlers/close-bounty-claim issue-id]) + matches-current-issue? (some #{issue-id} @open-bounty-claims)] + [:div + [:div.open-bounty-item.ph4 + [:div.open-bounty-item-content + [:div.header issue-link] + [:div.bounty-item-row + [:div.time (human-time updated)] + [:span.bounty-repo-label repo-link]] + [:div.footer-row + [:div.balance-badge "ETH " balance-eth] + (for [[tla balance] tokens] + ^{:key (random-uuid)} + [:div.balance-badge.token + (str (subs (str tla) 1) " " balance)]) + [:span.usd-value-label "Value "] [:span.usd-balance-label (str "$" value-usd)] + (when (> claim-count 0) + [:span.open-claims-label + {:on-click (if matches-current-issue? + #(close-claims-click) + #(open-claims-click))} + (str claim-count " open claim" + (when (> claim-count 1) "s")) + (if matches-current-issue? + [blue-arrow-box "blue-arrow-up.png"] + [blue-arrow-box "blue-arrow-down.png"])])]] + [:div.open-bounty-item-icon + [:div.ui.tiny.circular.image + [:img {:src avatar-url}]]]] + (when matches-current-issue? + [display-bounty-claims claims])])))) (defn bounties-filter-tooltip-value-input-view [label tooltip-open? opts] [:div.open-bounties-filter-element-tooltip-value-input-container @@ -210,7 +254,7 @@ [:div (let [left (inc (* (dec page-number) items-per-page)) right (dec (+ left item-count))] - [:div.item-counts-label-and-sorting-container + [:div.item-counts-label-and-sorting-container.ph4 [:div.item-counts-label [:span (str "Showing " left "-" right " of " total-count)]] [bounties-sort-view]]) @@ -227,7 +271,7 @@ [:div.ui.text.loader.view-loading-label "Loading"]]] [:div.ui.container.open-bounties-container {:ref #(reset! container-element %1)} - [:div.open-bounties-header "Bounties"] - [:div.open-bounties-filter-and-sort + [:div.open-bounties-header.ph4.pt4 "Bounties"] + [:div.open-bounties-filter-and-sort.ph4 [bounty-filters-view]] [bounties-list @bounty-page-data container-element]])))) diff --git a/src/cljs/commiteth/common.cljs b/src/cljs/commiteth/common.cljs index 7b87894..194e75e 100644 --- a/src/cljs/commiteth/common.cljs +++ b/src/cljs/commiteth/common.cljs @@ -48,6 +48,9 @@ (defn issue-url [owner repo number] (str "https://github.com/" owner "/" repo "/issues/" number)) +(defn pull-request-url [owner repo number] + (str "https://github.com/" owner "/" repo "/pull/" number)) + (def items-per-page 15) (defn draw-page-numbers [page-number page-count container-element] @@ -132,7 +135,7 @@ :else [:div [draw-items] - [:div.page-nav-container + [:div.page-nav-container.ph4.pb4 [:div.page-direction-container [draw-rect :backward] [draw-rect :forward]] diff --git a/src/cljs/commiteth/db.cljs b/src/cljs/commiteth/db.cljs index c92b2a1..295969f 100644 --- a/src/cljs/commiteth/db.cljs +++ b/src/cljs/commiteth/db.cljs @@ -4,7 +4,7 @@ (def default-db {:page :bounties :user nil - :user-profile-loaded? false + :user-profile-loaded? false :repos-loading? false :repos {} :activity-feed-loading? false @@ -18,6 +18,7 @@ ::ui-model/bounty-filter-type|currency nil ::ui-model/bounty-filter-type|date nil ::ui-model/bounty-filter-type|owner nil} + ::open-bounty-claims #{} :owner-bounties {} :top-hunters [] :activity-feed []}) diff --git a/src/cljs/commiteth/handlers.cljs b/src/cljs/commiteth/handlers.cljs index 61447c1..e5579fa 100644 --- a/src/cljs/commiteth/handlers.cljs +++ b/src/cljs/commiteth/handlers.cljs @@ -484,6 +484,16 @@ (.removeEventListener js/window "click" close-dropdown) (assoc db :user-dropdown-open? false))) +(reg-event-db + ::open-bounty-claim + (fn [db [_ opening-issue-id]] + (update db ::db/open-bounty-claims #(conj % opening-issue-id)))) + +(reg-event-db + ::close-bounty-claim + (fn [db [_ closing-issue-id]] + (update db ::db/open-bounty-claims #(disj % closing-issue-id)))) + (reg-event-db ::set-open-bounties-sorting-type (fn [db [_ sorting-type]] diff --git a/src/cljs/commiteth/subscriptions.cljs b/src/cljs/commiteth/subscriptions.cljs index 3ecd6e2..c6aaf57 100644 --- a/src/cljs/commiteth/subscriptions.cljs +++ b/src/cljs/commiteth/subscriptions.cljs @@ -53,17 +53,24 @@ :open-bounties-page :<- [::filtered-and-sorted-open-bounties] :<- [:page-number] - (fn [[open-bounties page-number] _] + :<- [:activity-feed] + (fn [[open-bounties page-number activity-feed] _] (let [total-count (count open-bounties) - start (* (dec page-number) items-per-page) - end (min total-count (+ items-per-page start)) - items (subvec open-bounties start end)] - {:items items - :item-count (count items) + start (* (dec page-number) items-per-page) + end (min total-count (+ items-per-page start)) + items (->> (subvec open-bounties start end) + (map (fn [bounty] + (let [matching-claims (filter + (fn [claim] + (= (:issue-id claim) + (:issue-id bounty))) + activity-feed)] + (assoc bounty :claims matching-claims)))))] + {:items items + :item-count (count items) :total-count total-count :page-number page-number - :page-count (Math/ceil (/ total-count items-per-page))}))) - + :page-count (Math/ceil (/ total-count items-per-page))}))) (reg-sub :owner-bounties @@ -146,6 +153,11 @@ (fn [db _] (:user-dropdown-open? db))) +(reg-sub + ::open-bounty-claims + (fn [db _] + (::db/open-bounty-claims db))) + (reg-sub ::open-bounties-sorting-type (fn [db _] diff --git a/src/less/style.less b/src/less/style.less index 0c3d7c7..46e4c0e 100644 --- a/src/less/style.less +++ b/src/less/style.less @@ -415,7 +415,6 @@ label[for="input-hidden"] { background-color: #fff; border-radius: 10px; transform: translateY(-45px); - padding: 24px; .open-bounties-header { font-family: "PostGrotesk-Medium"; font-size: 21px; @@ -714,6 +713,17 @@ label[for="input-hidden"] { padding: 4px; text-align: center; } + + .blue-arrow-box { + width: 24px; + height: 24px; + } + + .blue-arrow-image { + width: 13.5px; + height: 6px; + } + } .open-bounty-item:first-child { @@ -748,8 +758,6 @@ label[for="input-hidden"] { font-size: 15px; } - border-bottom: #eaecee 1px solid !important; - .open-bounty-item-content { width: 80%; padding: .7em 0 1em; @@ -780,8 +788,28 @@ label[for="input-hidden"] { flex-direction: row !important; justify-content: space-between; width: 80%; - } + +.bounty-claims-row { + padding-top: 6px; + padding-bottom: 6px; +} + +.bounty-claims-container { + background-color: #FBFBFB; +} + +.bounty-claims-icon { + width: 40px; + } + +.bounty-claims-text { + a { + text-decoration: none; + color: #42505c; + } +} + .bounty-repo-label a { margin: auto; font-family: "PostGrotesk-Book"; @@ -804,6 +832,7 @@ label[for="input-hidden"] { padding-left: 15px; font-size: 15px; color: #57a7ed; + cursor: pointer; } .activity-item-container {