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/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 4c061f8..75d73e4 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,6 +216,7 @@ INSERT INTO pull_requests (pr_id, VALUES(:pr_id, :repo_id, :pr_number, + :title, :issue_number, :issue_id, :commit_sha, @@ -225,6 +227,7 @@ SET state = :state, issue_number = :issue_number, issue_id = :issue_id, + title = :title, updated = timezone('utc'::text, now()), commit_sha = :commit_sha; @@ -592,7 +595,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 2be8511..a2c93c6 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -23,9 +23,10 @@ (defn deploy-contract [owner owner-address repo issue-id issue-number] (if (empty? owner-address) (log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id) - (do + (try (log/infof "issue %s: Deploying contract to %s" issue-id owner-address) - (if-let [transaction-hash (multisig/deploy-multisig owner-address)] + (if-let [transaction-hash (multisig/deploy-multisig {:owner owner-address + :internal-tx-id (str "contract-github-issue-" issue-id)})] (do (log/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id transaction-hash) (let [resp (github/post-deploying-comment owner @@ -36,7 +37,8 @@ (log/infof "issue %s: post-deploying-comment response: %s" issue-id resp) (issues/update-comment-id issue-id comment-id)) (issues/update-transaction-hash issue-id transaction-hash)) - (log/errorf "issue %s Failed to deploy contract to %s" issue-id 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 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 f70374d..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,29 +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)] - (log/infof "Signing TX: nonce: %s, gas-price: %s, gas-limit: %s, data: %s" - nonce gas-price gas-limit data) - 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 @@ -130,7 +158,8 @@ (atom 0)) (defn eth-rpc - [method params] + [{: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 @@ -140,9 +169,10 @@ :body (json/write-str body)} response @(post (eth-rpc-url) options) result (safe-read-str (:body response))] - (log/infof "eth-rpc req(%s) body: %s\neth-rpc req(%s) result: %s" - request-id body request-id 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) @@ -150,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)))) @@ -187,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) @@ -212,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] @@ -231,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] @@ -241,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) @@ -255,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] @@ -330,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/routes/services.clj b/src/clj/commiteth/routes/services.clj index ce5d426..656c804 100644 --- a/src/clj/commiteth/routes/services.clj +++ b/src/clj/commiteth/routes/services.clj @@ -109,10 +109,14 @@ renames {:user_name :display-name :user_avatar_url :avatar-url :issue_title :issue-title + :pr_title :pr-title + :pr_number :pr-number + :pr_id :pr-id :type :item-type :repo_name :repo-name :repo_owner :repo-owner :issue_number :issue-number + :issue_id :issue-id :value_usd :value-usd :claim_count :claim-count :balance_eth :balance-eth diff --git a/src/clj/commiteth/routes/webhooks.clj b/src/clj/commiteth/routes/webhooks.clj index b7fc05d..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 @@ -187,6 +187,7 @@ repo-id pr-id pr-number + pr-title head-sha merged? event-type)) diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index 1be116c..4299c49 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -126,7 +126,9 @@ tokens winner-login true)) - (let [execute-hash (multisig/send-all contract-address payout-address)] + (let [execute-hash (multisig/send-all {:contract contract-address + :payout-address payout-address + :internal-tx-id (str "payout-github-issue-" issue-id)})] (log/infof "issue %s: Payout self-signed, called sign-all(%s) tx: %s" issue-id contract-address payout-address execute-hash) (db-bounties/update-execute-hash issue-id execute-hash) (db-bounties/update-winner-login issue-id winner-login) 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 {