diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index b40e88c..d8efbd2 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -145,7 +145,8 @@ RETURNING repo_id, issue_id, issue_number, title, commit_sha, contract_address; -- :name update-transaction-hash :! :n -- :doc updates transaction-hash for a given issue UPDATE issues -SET transaction_hash = :transaction_hash +SET transaction_hash = :transaction_hash, + updated = timezone('utc'::text, now()) WHERE issue_id = :issue_id; @@ -241,6 +242,48 @@ WHERE pr_id = :pr_id; -- Bounties ------------------------------------------------------------------------ +-- :name unmined-tx-hashes :? :* +-- :doc hashes that haven't been mined for some time +SELECT + CASE WHEN transaction_hash is not null and contract_address is null + THEN transaction_hash + WHEN execute_hash is not null and confirm_hash is null + THEN execute_hash + WHEN watch_hash is not null + THEN watch_hash + END + AS tx_hash, + CASE WHEN transaction_hash is not null and contract_address is null + THEN 'deploy' + WHEN execute_hash is not null and confirm_hash is null + THEN 'execute' + WHEN watch_hash is not null + THEN 'watch' + END + AS type, + issue_id +FROM issues +WHERE updated < timezone('utc'::text, now()) - interval '5 minutes' +AND (transaction_hash is not null and contract_address is null + OR execute_hash is not null and confirm_hash is null + OR watch_hash is not null); + + +-- :name reset-tx-hash! :! :n +-- :doc reset tx hash if it hasn't been mined for some time +UPDATE issues + SET + --~ (when (= (:type params) "deploy") "transaction_hash") + --~ (when (= (:type params) "execute") "execute_hash") + --~ (when (= (:type params) "watch") "watch_hash") + = null + WHERE + --~ (when (= (:type params) "deploy") "transaction_hash") + --~ (when (= (:type params) "execute") "execute_hash") + --~ (when (= (:type params) "watch") "watch_hash") + = :tx-hash + + -- :name pending-contracts :? :* -- :doc bounty issues where deploy contract has failed SELECT @@ -254,7 +297,7 @@ FROM issues i, users u, repositories r WHERE r.user_id = u.id AND i.repo_id = r.repo_id -AND (i.transaction_hash IS NULL OR updated > now() - interval '1 hour') +AND i.transaction_hash IS NULL AND i.contract_address IS NULL; diff --git a/src/clj/commiteth/db/bounties.clj b/src/clj/commiteth/db/bounties.clj index ff1d14e..2e6c8f0 100644 --- a/src/clj/commiteth/db/bounties.clj +++ b/src/clj/commiteth/db/bounties.clj @@ -100,3 +100,12 @@ [] (jdbc/with-db-connection [con-db *db*] (db/bounties-activity con-db))) + +(defn unmined-tx-hashes [] + (jdbc/with-db-connection [con-db *db*] + (db/unmined-tx-hashes con-db))) + +(defn reset-tx-hash! + [tx-hash type] + (jdbc/with-db-connection [con-db *db*] + (db/reset-tx-hash! con-db {:tx-hash tx-hash :type type}))) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 7ddb49d..c9943fd 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -7,6 +7,7 @@ [taoensso.tufte :as tufte :refer (defnp p profiled profile)] [clojure.tools.logging :as log] [clojure.string :as str] + [clj-time.core :as t] [mount.core :as mount] [pandect.core :as pandect] [commiteth.util.util :refer [json-api-request]]) @@ -51,26 +52,20 @@ (throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn" {:password password :file-path file-path})))))) - -(def highest-nonce (atom nil)) -(def nonces-dropped (atom (clojure.lang.PersistentQueue/EMPTY))) - +(defrecord TxNonce [tx-hash nonce type timestamp]) +(def nonce-being-mined (atom nil)) (defn get-nonce [] - (if (seq @nonces-dropped) - (let [nonce (peek @nonces-dropped)] - (swap! nonces-dropped pop) - nonce) (let [nonce (.. (.ethGetTransactionCount @web3j-obj (env :eth-account) - DefaultBlockParameterName/LATEST) + DefaultBlockParameterName/PENDING) sendAsync get getTransactionCount)] - (if (or (nil? @highest-nonce) - (> nonce @highest-nonce)) - (reset! highest-nonce nonce) - (swap! highest-nonce (comp biginteger inc)))))) + (if (or (nil? @nonce-being-mined) + (> nonce (:nonce @nonce-being-mined))) + (:nonce (reset! nonce-being-mined (TxNonce. nil nonce nil nil))) + (throw (Exception. (str "Attempting to re-use old nonce" nonce)))))) (defn get-signed-tx [gas-price gas-limit to data nonce] "Create a sign a raw transaction. @@ -248,8 +243,13 @@ (defn get-transaction-receipt [hash] - (eth-rpc {:method "eth_getTransactionReceipt" - :params [hash]})) + (when-let [receipt (eth-rpc {:method "eth_getTransactionReceipt" + :params [hash]})] + (if (= hash (:tx-hash @nonce-being-mined)) + (reset! nonce-being-mined nil) + ;; This can happen in case of payout receipts + (log/infof "Obtained receipt for tx not currently being mined: " hash)) + receipt)) (defn format-call-params [method-id & params] @@ -263,7 +263,7 @@ :params [{:to contract :data data} "latest"]}))) (defn execute - [{:keys [from contract method-id gas-limit params internal-tx-id]}] + [{:keys [from contract method-id gas-limit params internal-tx-id type]}] {:pre [(string? method-id)]} (let [data (apply format-call-params method-id params) gas-price (gas-price) @@ -288,12 +288,16 @@ (assoc params :gas gas))] (if (offline-signing?) (try - (eth-rpc - {:method "eth_sendRawTransaction" - :params [params] - :internal-tx-id internal-tx-id}) + (let [tx-hash (eth-rpc + {:method "eth_sendRawTransaction" + :params [params] + :internal-tx-id internal-tx-id})] + (:tx-hash (swap! nonce-being-mined + assoc :tx-hash tx-hash + :type type + :timestamp (t/now)))) (catch Throwable ex - (swap! nonces-dropped conj nonce) + (reset! nonce-being-mined nil) (throw ex))) (eth-rpc {:method "personal_sendTransaction" diff --git a/src/clj/commiteth/eth/multisig_wallet.clj b/src/clj/commiteth/eth/multisig_wallet.clj index 874c113..fd1bbef 100644 --- a/src/clj/commiteth/eth/multisig_wallet.clj +++ b/src/clj/commiteth/eth/multisig_wallet.clj @@ -43,6 +43,7 @@ [{:keys [owner internal-tx-id]}] {:pre [(string? owner) (string? internal-tx-id)]} (eth/execute {:internal-tx-id internal-tx-id + :type "deploy" :from (eth/eth-account) :contract (factory-contract-addr) :method-id (:create method-ids) @@ -93,6 +94,7 @@ (:withdraw-everything method-ids) payout-address)] (eth/execute {:internal-tx-id internal-tx-id + :type "execute" :from (eth/eth-account) :contract contract :method-id (:submit-transaction method-ids) @@ -111,6 +113,7 @@ (let [token-address (get-token-address token)] (assert token-address) (eth/execute {:internal-tx-id (str "watch-token-" (System/currentTimeMillis) "-" bounty-addr) + :type "watch" :from (eth/eth-account) :contract bounty-addr :method-id (:watch method-ids) diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index 4299c49..f59e2b3 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -89,6 +89,7 @@ (p :deploy-pending-contracts (doseq [{issue-id :issue_id issue-number :issue_number + transaction-hash :transaction_hash owner :owner owner-address :owner_address repo :repo} (db-bounties/pending-contracts)] @@ -371,6 +372,25 @@ (log/error ex "issue %s: update-balances exception" issue-id))))) (log/info "Exit update-balances")) +(defn check-tx-receipts + "At all times, there should be no more than one unmined tx hash, + as we are executing txs sequentially" + [] + (log/info "In check-tx-receipts") + (doseq [{tx-hash :tx_hash + type :type + issue-id :issue_id} (db-bounties/unmined-tx-hashes)] + (log/infof "issue %s: resetting tx operation: %s for hash: %s" issue-id type tx-hash) + (db-bounties/reset-tx-hash! tx-hash type) + (when (= tx-hash (:tx-hash @eth/nonce-being-mined)) + (log/infof "issue %s: reset nonce" issue-id) + (reset! eth/nonce-being-mined nil))) + (when (and (:timestamp @eth/nonce-being-mined) + (t/before? (:timestamp @eth/nonce-being-mined) (t/minus (t/now) (t/minutes 5)))) + (log/errorf "nonce-being-mined not present in DB and unmined for 5 minutes, force reset. Tx hash: %s, type: %s" + (:tx-hash @eth/nonce-being-mined) (:type @eth/nonce-being-mined)) + (reset! eth/nonce-being-mined nil)) + (log/info "Exit check-tx-receipts")) (defn wrap-in-try-catch [func] (try @@ -394,6 +414,7 @@ update-confirm-hash update-payout-receipt update-watch-hash + check-tx-receipts self-sign-bounty ]) (log/info "run-1-min-interval-tasks done")))