Merge pull request #427 from status-im/fix/nonce-increment

Send txs sequentially; add thread to check for unmined txs
This commit is contained in:
Vitaliy Vlasov 2018-05-02 16:11:06 +03:00 committed by GitHub
commit 5e4a4bfbb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 399 additions and 264 deletions

View File

@ -145,35 +145,11 @@ 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;
-- TODO: this is terrible
-- :name update-contract-address :<! :1
-- :doc updates contract-address for a given issue
WITH t AS (
SELECT
i.issue_id AS issue_id,
i.issue_number AS issue_number,
i.title AS title,
i.transaction_hash AS transaction_hash,
i.contract_address AS contract_address,
i.comment_id AS comment_id,
i.repo_id AS repo_id,
r.owner AS owner,
r.repo AS repo
FROM issues i, repositories r
WHERE r.repo_id = i.repo_id
AND i.issue_id = :issue_id
)
UPDATE issues i
SET contract_address = :contract_address,
updated = timezone('utc'::text, now())
FROM t
WHERE i.issue_id = :issue_id
RETURNING t.issue_id, t.issue_number, t.title, t.transaction_hash, t.comment_id, i.contract_address, t.owner, t.repo, t.repo_id;
-- :name update-comment-id :! :n
-- :doc updates comment-id for a given issue
UPDATE issues
@ -241,11 +217,61 @@ WHERE pr_id = :pr_id;
-- Bounties ------------------------------------------------------------------------
-- :name unmined-txs :? :*
-- :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 '10 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 save-tx-info! :! :n
-- :doc save tx hash from receipt
UPDATE issues
SET
--~ (when (= (:type params) "deploy") "transaction_hash")
--~ (when (= (:type params) "execute") "execute_hash")
--~ (when (= (:type params) "watch") "watch_hash")
= :tx-hash,
updated = timezone('utc'::text, now())
WHERE issue_id=:issue-id
-- :name save-tx-result! :! :n
-- :doc save tx hash from receipt
UPDATE issues
SET
--~ (when (= (:type params) "deploy") "contract_address")
--~ (when (= (:type params) "execute") "confirm_hash")
--~ (when (= (:type params) "watch") "watch_hash")
= :result,
updated = timezone('utc'::text, now())
WHERE issue_id=:issue-id
-- :name pending-contracts :? :*
-- :doc bounty issues where deploy contract has failed
SELECT
i.issue_id AS issue_id,
i.issue_number AS issue_number,
i.transaction_hash AS transaction_hash,
r.owner AS owner,
u.address AS owner_address,
r.repo AS repo
@ -322,31 +348,11 @@ AND u.id = p.user_id
AND i.payout_receipt IS NULL
AND i.payout_hash IS NOT NULL;
-- :name update-confirm-hash :! :n
-- :doc updates issue with confirmation hash
UPDATE issues
SET confirm_hash = :confirm_hash,
updated = timezone('utc'::text, now())
WHERE issue_id = :issue_id;
-- :name update-execute-hash :! :n
-- :doc updates issue with execute transaction hash
UPDATE issues
SET execute_hash = :execute_hash,
updated = timezone('utc'::text, now())
WHERE issue_id = :issue_id;
-- :name update-winner-login :! :n
UPDATE issues
SET winner_login = :winner_login
WHERE issue_id = :issue_id;
-- :name update-watch-hash :! :n
-- :doc updates issue with watch transaction hash
UPDATE issues
SET watch_hash = :watch_hash
WHERE issue_id = :issue_id;
-- :name pending-watch-calls :? :*
-- :doc issues with a pending watch transaction
SELECT
@ -415,6 +421,23 @@ FROM issues
WHERE repo_id = :repo_id
AND issue_number = :issue_number;
-- :name get-issue-by-id :? :1
-- :doc get issue from DB by issue-id
SELECT
i.issue_id AS issue_id,
i.issue_number AS issue_number,
i.is_open AS is_open,
i.winner_login AS winner_login,
i.commit_sha AS commit_sha,
i.title AS title,
i.comment_id AS comment_id,
i.repo_id AS repo_id,
r.owner AS owner,
r.repo AS repo
FROM issues i, repositories r
WHERE r.repo_id = i.repo_id
AND i.issue_id = :issue-id
-- :name open-bounties :? :*

View File

@ -4,6 +4,7 @@
[commiteth.db.repositories :as repos]
[commiteth.db.comment-images :as comment-images]
[commiteth.eth.core :as eth]
[commiteth.eth.tracker :as tracker]
[commiteth.github.core :as github]
[commiteth.eth.multisig-wallet :as multisig]
[commiteth.model.bounty :as bnt]
@ -21,23 +22,18 @@
(let [labels (:labels issue)]
(some #(= label-name (:name %)) labels)))
(defn deploy-contract [owner owner-address repo issue-id issue-number]
(defn deploy-contract [owner-address issue-id]
(if (empty? 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)})]
(if-let [tx-info (multisig/deploy-multisig {:owner owner-address
:internal-tx-id [:deploy issue-id]})]
(do
(log/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id transaction-hash)
(let [resp (github/post-deploying-comment owner
repo
issue-number
transaction-hash)
comment-id (:id resp)]
(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/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id (:tx-hash tx-info))
(github/post-deploying-comment issue-id
(:tx-hash tx-info))
(tracker/track-tx! tx-info))
(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)))))
@ -51,7 +47,7 @@
(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 owner-address repo issue-id issue-number)
(deploy-contract owner-address issue-id)
(log/debug "issue %s: Issue already exists in DB, ignoring"))))
(defn maybe-add-bounty-for-issue [repo repo-id issue]

View File

@ -40,26 +40,12 @@
(jdbc/with-db-connection [con-db *db*]
(db/confirmed-payouts con-db)))
(defn update-confirm-hash
[issue-id confirm-hash]
(jdbc/with-db-connection [con-db *db*]
(db/update-confirm-hash con-db {:issue_id issue-id :confirm_hash confirm-hash})))
(defn update-execute-hash
[issue-id execute-hash]
(jdbc/with-db-connection [con-db *db*]
(db/update-execute-hash con-db {:issue_id issue-id :execute_hash execute-hash})))
(defn update-winner-login
[issue-id login]
(jdbc/with-db-connection [con-db *db*]
(db/update-winner-login con-db {:issue_id issue-id :winner_login login})))
(defn update-watch-hash
[issue-id watch-hash]
(jdbc/with-db-connection [con-db *db*]
(db/update-watch-hash con-db {:issue_id issue-id :watch_hash watch-hash})))
(defn pending-watch-calls
[]
(jdbc/with-db-connection [con-db *db*]
@ -100,3 +86,5 @@
[]
(jdbc/with-db-connection [con-db *db*]
(db/bounties-activity con-db)))

View File

@ -38,19 +38,25 @@
:title title})))
(defn update-transaction-hash
"Updates issue with transaction-hash"
[issue-id transaction-hash]
(defn save-tx-info!
"Set transaction_hash, execute_hash or watch_hash depending on operation"
[issue-id tx-hash type-kw]
(jdbc/with-db-connection [con-db *db*]
(db/update-transaction-hash con-db {:issue_id issue-id
:transaction_hash transaction-hash})))
(db/save-tx-info! con-db {:issue-id issue-id
:tx-hash tx-hash
:type (name type-kw)})))
(defn update-contract-address
"Updates issue with contract-address"
[issue-id contract-address]
(defn save-tx-result!
"Set contract_address, confirm_hash or watch_hash depending on operation"
[issue-id result type-kw]
(jdbc/with-db-connection [con-db *db*]
(db/update-contract-address con-db {:issue_id issue-id
:contract_address contract-address})))
(db/save-tx-result! con-db {:issue-id issue-id
:result result
:type (name type-kw)})))
(defn unmined-txs []
(jdbc/with-db-connection [con-db *db*]
(db/unmined-txs con-db)))
(defn update-comment-id
"Updates issue with comment id"
@ -103,3 +109,8 @@
(jdbc/with-db-connection [con-db *db*]
(db/get-issue con-db {:repo_id repo-id
:issue_number issue-number})))
(defn get-issue-by-id
[issue-id]
(jdbc/with-db-connection [con-db *db*]
(db/get-issue-by-id con-db {:issue-id issue-id})))

View File

@ -3,23 +3,15 @@
[org.httpkit.client :refer [post]]
[clojure.java.io :as io]
[commiteth.config :refer [env]]
[commiteth.eth.web3j :refer [get-signed-tx]]
[clojure.string :refer [join]]
[taoensso.tufte :as tufte :refer (defnp p profiled profile)]
[clojure.tools.logging :as log]
[commiteth.eth.tracker :as tracker]
[clj-time.core :as t]
[clojure.string :as str]
[mount.core :as mount]
[pandect.core :as pandect]
[commiteth.util.util :refer [json-api-request]])
(:import [org.web3j
protocol.Web3j
protocol.http.HttpService
protocol.core.DefaultBlockParameterName
protocol.core.methods.response.EthGetTransactionCount
protocol.core.methods.request.RawTransaction
utils.Numeric
crypto.Credentials
crypto.TransactionEncoder
crypto.WalletUtils]))
[commiteth.util.util :refer [json-api-request]]))
(defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545"))
(defn eth-account [] (:eth-account env))
@ -28,83 +20,6 @@
(defn auto-gas-price? [] (env :auto-gas-price false))
(defn offline-signing? [] (env :offline-signing true))
(def web3j-obj
(delay (Web3j/build (HttpService. (eth-rpc-url)))))
(def creds-obj (atom nil))
(defn wallet-file-path []
(env :eth-wallet-file))
(defn wallet-password []
(env :eth-password))
(defn creds []
(or @creds-obj
(let [password (wallet-password)
file-path (wallet-file-path)]
(if (and password file-path)
(swap! creds-obj
(constantly (WalletUtils/loadCredentials
password
file-path)))
(throw (ex-info "Make sure you provided proper credentials in appropriate resources/config.edn"
{:password password :file-path file-path}))))))
(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 [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
as gas price. average_calc is computed from a larger time period than average,
@ -155,7 +70,9 @@
(defn eth-rpc
[{:keys [method params internal-tx-id]}]
{:pre [(string? method) (some? params)]}
(let [request-id (swap! req-id-tracker inc)
(let [[type-kw issue-id] internal-tx-id
tx-id-str (str type-kw "-" issue-id)
request-id (swap! req-id-tracker inc)
body {:jsonrpc "2.0"
:method method
:params params
@ -165,21 +82,22 @@
response @(post (eth-rpc-url) options)
result (safe-read-str (:body response))]
(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)
(if internal-tx-id
(log/infof "%s: eth-rpc req(%s) result: %s" internal-tx-id request-id result)
(log/infof "%s: eth-rpc %s" tx-id-str method))
(log/debugf "%s: eth-rpc req(%s) body: %s" tx-id-str request-id body)
(if tx-id-str
(log/infof "%s: eth-rpc req(%s) result: %s" tx-id-str request-id result)
(log/debugf "no-tx-id: eth-rpc req(%s) result: %s" request-id result))
(cond
;; Ignore any responses that have mismatching request ID
(not= (:id result) request-id)
(log/error "Geth returned an invalid json-rpc request ID, ignoring response")
(throw (ex-info "Geth returned an invalid json-rpc request ID, ignoring response"
{:result result}))
;; If request ID matches but contains error, throw
(:error result)
(throw
(ex-info (format "%s: Error submitting transaction via eth-rpc %s"
(or internal-tx-id "(no-tx-id)") (:error result))
(or tx-id-str "(no-tx-id)") (:error result))
(:error result)))
:else
@ -277,36 +195,70 @@
(eth-rpc {:method "eth_call"
:params [{:to contract :data data} "latest"]})))
(defn execute
[{:keys [from contract method-id gas-limit params internal-tx-id]}]
{:pre [(string? method-id)]}
(defn construct-params
[{:keys [from contract method-id params gas-price]}]
(let [data (apply format-call-params method-id params)
value (format "0x%x" 0)]
(cond-> {:data data
:from from
:value value}
gas-price
(merge {:gasPrice (integer->hex gas-price)})
contract
(merge {:to contract}))))
(defmulti execute (fn [_] (if (offline-signing?)
:with-tx-signing
:no-tx-signing)))
(defmethod execute :with-tx-signing
[{:keys [from contract method-id gas-limit params internal-tx-id]
:as args}]
{:pre [(string? method-id)]}
(let [[type-kw issue-id] internal-tx-id
nonce (tracker/try-reserve-nonce!)
gas-price (gas-price)
value (format "0x%x" 0)
params (cond-> {:data data
:from from
:value value}
gas-price
(merge {:gasPrice (integer->hex gas-price)})
contract
(merge {:to contract}))
gas (or gas-limit (estimate-gas from contract value params))
params (if (offline-signing?)
(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
{:method "eth_sendRawTransaction"
:params [params]
:internal-tx-id internal-tx-id})
(eth-rpc
{:method "personal_sendTransaction"
:params [params (eth-password)]
:internal-tx-id internal-tx-id}))))
params (construct-params (assoc args :gas-price gas-price))
gas (or gas-limit (estimate-gas from contract (:value params) params))
params (get-signed-tx (biginteger gas-price)
(hex->big-integer gas)
contract
(:data params)
nonce)
tx-hash (try
(eth-rpc
{:method "eth_sendRawTransaction"
:params [params]
:internal-tx-id internal-tx-id})
(catch Throwable ex
(tracker/drop-nonce! nonce)
(throw ex)))]
{:tx-hash tx-hash
:issue-id issue-id
:type type-kw
:nonce nonce
:timestamp (t/now)}))
(defmethod execute :no-tx-signing
[{:keys [from contract method-id gas-limit params internal-tx-id]
:as args}]
{:pre [(string? method-id)]}
(let [[type-kw issue-id] internal-tx-id
gas-price (gas-price)
params (construct-params (assoc args :gas-price gas-price))
gas (or gas-limit (estimate-gas from contract (:value params) params))
params (assoc params :gas gas)
tx-hash (eth-rpc
{:method "personal_sendTransaction"
:params [params (eth-password)]
:internal-tx-id internal-tx-id})]
{:tx-hash tx-hash
:type type-kw
:timestamp (t/now)
:issue-id issue-id}
))
(defn hex-ch->num
[ch]
@ -362,14 +314,3 @@
(filter true?)
(empty?)))
true))))
(mount/defstate
eth-core
:start
(do
(swap! creds-obj (constantly nil))
(log/info "eth/core started"))
:stop
(log/info "eth/core stopped"))

View File

@ -2,6 +2,7 @@
(:require [commiteth.eth.core :as eth]
[commiteth.config :refer [env]]
[clojure.tools.logging :as log]
[commiteth.eth.web3j :refer [web3j-obj creds-obj]]
[taoensso.tufte :as tufte :refer (defnp p profiled profile)]
[commiteth.eth.token-data :as token-data])
(:import [org.web3j
@ -41,7 +42,7 @@
`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)]}
{:pre [(string? owner) (vector? internal-tx-id)]}
(eth/execute {:internal-tx-id internal-tx-id
:from (eth/eth-account)
:contract (factory-contract-addr)
@ -87,7 +88,7 @@
(defn send-all
[{:keys [contract payout-address internal-tx-id]}]
{:pre [(string? contract) (string? payout-address) (string? internal-tx-id)]}
{:pre [(string? contract) (string? payout-address) (vector? internal-tx-id)]}
(log/debug "multisig/send-all " contract payout-address internal-tx-id)
(let [params (eth/format-call-params
(:withdraw-everything method-ids)
@ -106,11 +107,11 @@
(:address token-details)))
(defn watch-token
[bounty-addr token]
[{:keys [bounty-addr token internal-tx-id]}]
(log/debug "multisig/watch-token" bounty-addr token)
(let [token-address (get-token-address token)]
(assert token-address)
(eth/execute {:internal-tx-id (str "watch-token-" (System/currentTimeMillis) "-" bounty-addr)
(eth/execute {:internal-tx-id internal-tx-id
:from (eth/eth-account)
:contract bounty-addr
:method-id (:watch method-ids)
@ -119,8 +120,8 @@
(defn load-bounty-contract [addr]
(MultiSigTokenWallet/load addr
@eth/web3j-obj
(eth/creds)
@web3j-obj
@creds-obj
(eth/gas-price)
(BigInteger/valueOf 500000)))

View File

@ -1,6 +1,7 @@
(ns commiteth.eth.token-registry
(:require [commiteth.eth.core :as eth]
[commiteth.config :refer [env]]
[commiteth.eth.web3j :refer [web3j-obj creds-obj]]
[clojure.tools.logging :as log])
(:import [org.web3j
abi.datatypes.generated.Uint256
@ -22,8 +23,8 @@
(defn- load-tokenreg-contract [addr]
(TokenReg/load addr
@eth/web3j-obj
(eth/creds)
@web3j-obj
@creds-obj
(eth/gas-price)
(BigInteger/valueOf 21000)))
@ -58,8 +59,8 @@
(defn deploy-parity-tokenreg
"Deploy an instance of parity token-registry to current network"
[]
(TokenReg/deploy @eth/web3j-obj
(eth/creds)
(TokenReg/deploy @web3j-obj
@creds-obj
(eth/gas-price)
(BigInteger/valueOf 4000000) ;; gas limit
BigInteger/ZERO))

View File

@ -0,0 +1,99 @@
(ns commiteth.eth.tracker
(:require [clojure.data.json :as json]
[org.httpkit.client :refer [post]]
[commiteth.db.issues :as issues]
[commiteth.config :refer [env]]
[commiteth.eth.web3j :refer [web3j-obj]]
[clojure.tools.logging :as log]
[clojure.string :as str]
[clj-time.core :as t])
(:import [org.web3j
protocol.Web3j
protocol.http.HttpService
protocol.core.DefaultBlockParameterName]))
(defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545"))
(defn get-nonce []
(.. (.ethGetTransactionCount @web3j-obj
(env :eth-account)
DefaultBlockParameterName/PENDING)
sendAsync
get
getTransactionCount))
(defprotocol ITxTracker
(try-reserve-nonce [this]
"Fetch current nonce via eth_getTransactionCount
and either return it if it is not in use yet,
or throw an exception")
(drop-nonce [this nonce]
"If tx execution returned with an error,
release previously reserved nonce")
(track-tx [this tx-info]
"Record tx data after successful submission")
(untrack-tx [this tx-info]
"Mark tx as successfully mined")
(prune-txs [this txs]
"Release nonces related to txs param
and the ones that have expired (haven't been mined after a certain timeout)")
)
(defrecord SequentialTxTracker [current-tx]
ITxTracker
(try-reserve-nonce [this]
(let [nonce (get-nonce)]
(if (or (nil? @current-tx)
(> nonce (:nonce @current-tx)))
(:nonce (reset! current-tx {:nonce nonce}))
(throw (Exception. (str "Attempting to re-use old nonce" nonce))))))
(drop-nonce [this nonce]
(reset! current-tx nil))
(track-tx [this tx-info]
(reset! current-tx tx-info))
(untrack-tx [this tx-info]
(when (= (:nonce tx-info) (:nonce @current-tx))
(reset! current-tx nil)))
(prune-txs [this unmined-txs]
(when (or ((set (map :tx-hash unmined-txs)) (:tx-hash @current-tx))
(and (:timestamp @current-tx)
(t/before? (:timestamp @current-tx) (t/minus (t/now) (t/minutes 10)))))
(log/errorf "Current nonce unmined for 10 minutes, force reset. Tx hash: %s, type: %s"
(:tx-hash @current-tx) (:type @current-tx))
(reset! current-tx nil)))
)
(def tx-tracker (SequentialTxTracker. (atom nil)))
(defn try-reserve-nonce! []
(try-reserve-nonce tx-tracker))
(defn drop-nonce! [nonce]
(drop-nonce tx-tracker nonce))
(defn track-tx!
"Store tx data in tx-tracker and DB"
[{:keys [issue-id tx-hash type]
:as tx-info}]
(track-tx tx-tracker tx-info)
(issues/save-tx-info! issue-id tx-hash type))
(defn untrack-tx!
"Mark tx data stored in tx-tracker and DB as successfully mined"
[{:keys [issue-id result type]
:as tx-info}]
(untrack-tx tx-tracker tx-info)
(issues/save-tx-result! issue-id result type))
(defn prune-txs! [unmined-txs]
"Release nonces related to unmined txs,
and set relevant DB fields to null thereby
marking them as candidates for re-execution"
(doseq [{issue-id :issue_id
tx-hash :tx_hash
type :type} unmined-txs]
(log/infof "issue %s: resetting tx operation: %s for hash: %s" issue-id type tx-hash)
(issues/save-tx-info! issue-id nil type))
(prune-txs tx-tracker unmined-txs))

View File

@ -0,0 +1,46 @@
(ns commiteth.eth.web3j
(:require [commiteth.config :refer [env]]
[clojure.tools.logging :as log])
(:import [org.web3j
protocol.Web3j
protocol.http.HttpService
protocol.core.methods.request.RawTransaction
utils.Numeric
crypto.TransactionEncoder
crypto.WalletUtils]))
(defn eth-rpc-url [] (env :eth-rpc-url "http://localhost:8545"))
(def web3j-obj
(delay (Web3j/build (HttpService. (eth-rpc-url)))))
(defn wallet-file-path []
(env :eth-wallet-file))
(defn wallet-password []
(env :eth-password))
(def creds-obj
(delay
(let [password (wallet-password)
file-path (wallet-file-path)]
(if (and password file-path)
(WalletUtils/loadCredentials
password
file-path)
(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 nonce]
"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"
(log/infof "Signing TX: nonce: %s, gas-price: %s, gas-limit: %s, data: %s"
nonce gas-price gas-limit data)
(-> (RawTransaction/createTransaction nonce gas-price gas-limit to data)
(TransactionEncoder/signMessage @creds-obj)
(Numeric/toHexString)))

View File

@ -12,6 +12,7 @@
[clj-http.client :as http]
[commiteth.config :refer [env]]
[digest :refer [sha-256]]
[commiteth.db.issues :as db-issues]
[clojure.tools.logging :as log]
[cheshire.core :as json]
[clojure.string :as str])
@ -271,13 +272,6 @@
(learn-more-text))
eth-balance-str payee-login))
(defn post-deploying-comment
[owner repo issue-number tx-id]
(let [comment (generate-deploying-comment owner repo issue-number tx-id)]
(log/info "Posting comment to" (str owner "/" repo "/" issue-number) ":" comment)
(issues/create-comment owner repo issue-number comment (self-auth-params))))
(defn make-patch-request [end-point positional query]
(let [{:keys [auth oauth-token]
:as query} query
@ -296,6 +290,24 @@
:otp))]
(assoc req :body (json/generate-string (or raw-query proper-query)))))
(defn post-deploying-comment
[issue-id tx-id]
(let [{owner :owner
repo :repo
issue-number :issue_number
comment-id :comment_id} (db-issues/get-issue-by-id issue-id)
comment (generate-deploying-comment owner repo issue-number tx-id) ]
(log/info "Posting comment to" (str owner "/" repo "/" issue-number) ":" comment)
(if comment-id
(let [req (make-patch-request "repos/%s/%s/issues/comments/%s"
[owner repo comment-id]
(assoc (self-auth-params) :body comment))]
(tentacles/safe-parse (http/request req)))
(let [resp (issues/create-comment owner repo issue-number comment (self-auth-params))]
(db-issues/update-comment-id issue-id (:id resp))
(log/infof "issue %s: post-deploying-comment response: %s" issue-id resp)
resp))))
(defn update-comment
"Update comment for an open bounty issue"
[owner repo comment-id issue-number contract-address eth-balance eth-balance-str tokens]

View File

@ -2,6 +2,7 @@
(:require [commiteth.eth.core :as eth]
[commiteth.eth.multisig-wallet :as multisig]
[commiteth.eth.token-data :as token-data]
[commiteth.eth.tracker :as tracker]
[commiteth.github.core :as github]
[commiteth.db.issues :as issues]
[taoensso.tufte :as tufte :refer (defnp p profiled profile)]
@ -52,11 +53,14 @@
(when-let [receipt (eth/get-transaction-receipt transaction-hash)]
(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 [issue (issues/update-contract-address issue-id contract-address)
(let [_ (tracker/untrack-tx! {:issue-id issue-id
:tx-hash transaction-hash
:result contract-address
:type :deploy})
{owner :owner
repo :repo
comment-id :comment_id
issue-number :issue_number} issue
issue-number :issue_number} (issues/get-issue-by-id issue-id)
balance-eth-str (eth/get-balance-eth contract-address 6)
balance-eth (read-string balance-eth-str)]
(log/infof "issue %s: Updating comment image" issue-id)
@ -90,13 +94,10 @@
[]
(p :deploy-pending-contracts
(doseq [{issue-id :issue_id
issue-number :issue_number
owner :owner
owner-address :owner_address
repo :repo} (db-bounties/pending-contracts)]
owner-address :owner_address} (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)
(bounties/deploy-contract owner-address issue-id)
(catch Throwable t
(log/errorf t "issue %s: deploy-pending-contracts exception: %s" issue-id (ex-data t)))))))
@ -106,15 +107,15 @@
(log/info "In self-sign-bounty")
(p :self-sign-bounty
(doseq [{contract-address :contract_address
issue-id :issue_id
payout-address :payout_address
repo :repo
owner :owner
comment-id :comment_id
issue-number :issue_number
balance-eth :balance_eth
tokens :tokens
winner-login :winner_login} (db-bounties/pending-bounties)]
issue-id :issue_id
payout-address :payout_address
repo :repo
owner :owner
comment-id :comment_id
issue-number :issue_number
balance-eth :balance_eth
tokens :tokens
winner-login :winner_login} (db-bounties/pending-bounties)]
(try
;; TODO(martin) delete this shortly after org-dashboard deploy
;; as we're now setting `winner_login` when handling a new claims
@ -132,11 +133,11 @@
tokens
winner-login
true))
(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)
(let [tx-info (multisig/send-all {:contract contract-address
:payout-address payout-address
:internal-tx-id [:execute issue-id]})]
(log/infof "issue %s: Payout self-signed, called sign-all(%s) tx: %s" issue-id contract-address payout-address (:tx-hash tx-info))
(tracker/track-tx! tx-info)
(github/update-merged-issue-comment owner
repo
comment-id
@ -162,7 +163,10 @@
(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)))
(tracker/untrack-tx! {:issue-id issue-id
:tx-hash execute-hash
:result confirm-hash
:type :execute})))
(catch Throwable ex
(log/errorf ex "issue %s: update-confirm-hash exception:" issue-id)))))
(log/info "Exit update-confirm-hash"))
@ -177,7 +181,10 @@
(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))
(tracker/untrack-tx! {:issue-id issue-id
:tx-hash watch-hash
:result nil
:type :watch}))
(catch Throwable ex
(log/errorf ex "issue %s: update-watch-hash exception:" issue-id))))))
@ -265,8 +272,10 @@
(when (and (nil? watch-hash)
(not= balance internal-balance))
(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)))))))
(let [tx-info (multisig/watch-token {:bounty-addr bounty-addr
:token tla
:internal-tx-id [:watch issue-id]})]
(tracker/track-tx! tx-info)))))))
(catch Throwable ex
(log/error ex "bounty %s: update-bounty-token-balances exception" bounty-addr))))
(log/info "Exit update-bounty-token-balances"))
@ -376,6 +385,13 @@
(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")
(tracker/prune-txs! (issues/unmined-txs))
(log/info "Exit check-tx-receipts"))
(defn wrap-in-try-catch [func]
(try
@ -399,6 +415,7 @@
update-confirm-hash
update-payout-receipt
update-watch-hash
check-tx-receipts
self-sign-bounty
])
(log/info "run-1-min-interval-tasks done")))
@ -416,11 +433,11 @@
(mount/defstate scheduler
:start (let [every-minute (rest
(periodic-seq (t/now)
(t/minutes 1)))
(periodic-seq (t/now)
(t/minutes 1)))
every-10-minutes (rest
(periodic-seq (t/now)
(t/minutes 10)))
(periodic-seq (t/now)
(t/minutes 10)))
error-handler (fn [e]
(log/error "Scheduled task failed" e)
(throw e))