From 4e8a5148d5bc228d1f0129a9d3b89ee5ef5bc0e4 Mon Sep 17 00:00:00 2001 From: kagel Date: Tue, 6 Sep 2016 03:18:33 +0300 Subject: [PATCH] * deploy multisig contract bytecode * check for transaction receipt every 5 minutes --- project.clj | 1 + .../20160905235051-contracts.down.sql | 7 + .../20160905235051-contracts.up.sql | 7 + resources/sol/wallet.sol | 386 ++++++++++++++++++ resources/sql/queries.sql | 67 +-- src/clj/commiteth/db/issues.clj | 25 +- src/clj/commiteth/eth/core.clj | 61 +++ src/clj/commiteth/github/core.clj | 4 +- src/clj/commiteth/routes/webhooks.clj | 29 +- src/cljs/commiteth/home/page.cljs | 20 +- 10 files changed, 552 insertions(+), 55 deletions(-) create mode 100644 resources/migrations/20160905235051-contracts.down.sql create mode 100644 resources/migrations/20160905235051-contracts.up.sql create mode 100644 resources/sol/wallet.sol create mode 100644 src/clj/commiteth/eth/core.clj diff --git a/project.clj b/project.clj index 28c22fa..c2ed114 100644 --- a/project.clj +++ b/project.clj @@ -36,6 +36,7 @@ [org.postgresql/postgresql "9.4.1209"] [org.webjars/webjars-locator-jboss-vfs "0.1.0"] [luminus-immutant "0.2.2"] + [overtone/at-at "1.2.0"] [tentacles "0.5.1"]] :min-lein-version "2.0.0" diff --git a/resources/migrations/20160905235051-contracts.down.sql b/resources/migrations/20160905235051-contracts.down.sql new file mode 100644 index 0000000..1bfa275 --- /dev/null +++ b/resources/migrations/20160905235051-contracts.down.sql @@ -0,0 +1,7 @@ +-- noinspection SqlResolveForFile +ALTER TABLE public.issues + DROP COLUMN transaction_hash; +ALTER TABLE public.issues + DROP COLUMN contract_address; +ALTER TABLE public.issues + ADD address VARCHAR(256); diff --git a/resources/migrations/20160905235051-contracts.up.sql b/resources/migrations/20160905235051-contracts.up.sql new file mode 100644 index 0000000..8a47b89 --- /dev/null +++ b/resources/migrations/20160905235051-contracts.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE public.issues + ADD transaction_hash VARCHAR(128) NULL; +ALTER TABLE public.issues + ADD contract_address VARCHAR(42) NULL; +-- noinspection SqlResolve +ALTER TABLE public.issues + DROP COLUMN address; diff --git a/resources/sol/wallet.sol b/resources/sol/wallet.sol new file mode 100644 index 0000000..dc69cd6 --- /dev/null +++ b/resources/sol/wallet.sol @@ -0,0 +1,386 @@ +//sol Wallet +// Multi-sig, daily-limited account proxy/wallet. +// @authors: +// Gav Wood +// inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a +// single, or, crucially, each of a number of, designated owners. +// usage: +// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by +// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the +// interior is executed. +contract multiowned { + + // TYPES + + // struct for the status of a pending operation. + struct PendingState { + uint yetNeeded; + uint ownersDone; + uint index; + } + + // EVENTS + + // this contract only has six types of events: it can accept a confirmation, in which case + // we record owner and operation (hash) alongside it. + event Confirmation(address owner, bytes32 operation); + event Revoke(address owner, bytes32 operation); + // some others are in the case of an owner changing. + event OwnerChanged(address oldOwner, address newOwner); + event OwnerAdded(address newOwner); + event OwnerRemoved(address oldOwner); + // the last one is emitted if the required signatures change + event RequirementChanged(uint newRequirement); + + // MODIFIERS + + // simple single-sig function modifier. + modifier onlyowner { + if (isOwner(msg.sender)) + _ + } + // multi-sig function modifier: the operation must have an intrinsic hash in order + // that later attempts can be realised as the same underlying operation and + // thus count as confirmations. + modifier onlymanyowners(bytes32 _operation) { + if (confirmAndCheck(_operation)) + _ + } + + // METHODS + + // constructor is given number of sigs required to do protected "onlymanyowners" transactions + // as well as the selection of addresses capable of confirming them. + function multiowned(address[] _owners, uint _required) { + m_numOwners = _owners.length + 1; + m_owners[1] = uint(msg.sender); + m_ownerIndex[uint(msg.sender)] = 1; + for (uint i = 0; i < _owners.length; ++i) + { + m_owners[2 + i] = uint(_owners[i]); + m_ownerIndex[uint(_owners[i])] = 2 + i; + } + m_required = _required; + } + + // Revokes a prior confirmation of the given operation + function revoke(bytes32 _operation) external { + uint ownerIndex = m_ownerIndex[uint(msg.sender)]; + // make sure they're an owner + if (ownerIndex == 0) return; + uint ownerIndexBit = 2**ownerIndex; + var pending = m_pending[_operation]; + if (pending.ownersDone & ownerIndexBit > 0) { + pending.yetNeeded++; + pending.ownersDone -= ownerIndexBit; + Revoke(msg.sender, _operation); + } + } + + // Replaces an owner `_from` with another `_to`. + function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external { + if (isOwner(_to)) return; + uint ownerIndex = m_ownerIndex[uint(_from)]; + if (ownerIndex == 0) return; + + clearPending(); + m_owners[ownerIndex] = uint(_to); + m_ownerIndex[uint(_from)] = 0; + m_ownerIndex[uint(_to)] = ownerIndex; + OwnerChanged(_from, _to); + } + + function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external { + if (isOwner(_owner)) return; + + clearPending(); + if (m_numOwners >= c_maxOwners) + reorganizeOwners(); + if (m_numOwners >= c_maxOwners) + return; + m_numOwners++; + m_owners[m_numOwners] = uint(_owner); + m_ownerIndex[uint(_owner)] = m_numOwners; + OwnerAdded(_owner); + } + + function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external { + uint ownerIndex = m_ownerIndex[uint(_owner)]; + if (ownerIndex == 0) return; + if (m_required > m_numOwners - 1) return; + + m_owners[ownerIndex] = 0; + m_ownerIndex[uint(_owner)] = 0; + clearPending(); + reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot + OwnerRemoved(_owner); + } + + function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external { + if (_newRequired > m_numOwners) return; + m_required = _newRequired; + clearPending(); + RequirementChanged(_newRequired); + } + + // Gets an owner by 0-indexed position (using numOwners as the count) + function getOwner(uint ownerIndex) external constant returns (address) { + return address(m_owners[ownerIndex + 1]); + } + + function isOwner(address _addr) returns (bool) { + return m_ownerIndex[uint(_addr)] > 0; + } + + function hasConfirmed(bytes32 _operation, address _owner) constant returns (bool) { + var pending = m_pending[_operation]; + uint ownerIndex = m_ownerIndex[uint(_owner)]; + + // make sure they're an owner + if (ownerIndex == 0) return false; + + // determine the bit to set for this owner. + uint ownerIndexBit = 2**ownerIndex; + return !(pending.ownersDone & ownerIndexBit == 0); + } + + // INTERNAL METHODS + + function confirmAndCheck(bytes32 _operation) internal returns (bool) { + // determine what index the present sender is: + uint ownerIndex = m_ownerIndex[uint(msg.sender)]; + // make sure they're an owner + if (ownerIndex == 0) return; + + var pending = m_pending[_operation]; + // if we're not yet working on this operation, switch over and reset the confirmation status. + if (pending.yetNeeded == 0) { + // reset count of confirmations needed. + pending.yetNeeded = m_required; + // reset which owners have confirmed (none) - set our bitmap to 0. + pending.ownersDone = 0; + pending.index = m_pendingIndex.length++; + m_pendingIndex[pending.index] = _operation; + } + // determine the bit to set for this owner. + uint ownerIndexBit = 2**ownerIndex; + // make sure we (the message sender) haven't confirmed this operation previously. + if (pending.ownersDone & ownerIndexBit == 0) { + Confirmation(msg.sender, _operation); + // ok - check if count is enough to go ahead. + if (pending.yetNeeded <= 1) { + // enough confirmations: reset and run interior. + delete m_pendingIndex[m_pending[_operation].index]; + delete m_pending[_operation]; + return true; + } + else + { + // not enough: record that this owner in particular confirmed. + pending.yetNeeded--; + pending.ownersDone |= ownerIndexBit; + } + } + } + + function reorganizeOwners() private { + uint free = 1; + while (free < m_numOwners) + { + while (free < m_numOwners && m_owners[free] != 0) free++; + while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; + if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0) + { + m_owners[free] = m_owners[m_numOwners]; + m_ownerIndex[m_owners[free]] = free; + m_owners[m_numOwners] = 0; + } + } + } + + function clearPending() internal { + uint length = m_pendingIndex.length; + for (uint i = 0; i < length; ++i) + if (m_pendingIndex[i] != 0) + delete m_pending[m_pendingIndex[i]]; + delete m_pendingIndex; + } + + // FIELDS + + // the number of owners that must confirm the same operation before it is run. + uint public m_required; + // pointer used to find a free slot in m_owners + uint public m_numOwners; + + // list of owners + uint[256] m_owners; + uint constant c_maxOwners = 250; + // index on the list of owners to allow reverse lookup + mapping(uint => uint) m_ownerIndex; + // the ongoing operations. + mapping(bytes32 => PendingState) m_pending; + bytes32[] m_pendingIndex; +} + +// inheritable "property" contract that enables methods to be protected by placing a linear limit (specifiable) +// on a particular resource per calendar day. is multiowned to allow the limit to be altered. resource that method +// uses is specified in the modifier. +contract daylimit is multiowned { + + // MODIFIERS + + // simple modifier for daily limit. + modifier limitedDaily(uint _value) { + if (underLimit(_value)) + _ + } + + // METHODS + + // constructor - stores initial daily limit and records the present day's index. + function daylimit(uint _limit) { + m_dailyLimit = _limit; + m_lastDay = today(); + } + // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. + function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external { + m_dailyLimit = _newLimit; + } + // resets the amount already spent today. needs many of the owners to confirm. + function resetSpentToday() onlymanyowners(sha3(msg.data)) external { + m_spentToday = 0; + } + + // INTERNAL METHODS + + // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and + // returns true. otherwise just returns false. + function underLimit(uint _value) internal onlyowner returns (bool) { + // reset the spend limit if we're on a different day to last time. + if (today() > m_lastDay) { + m_spentToday = 0; + m_lastDay = today(); + } + // check to see if there's enough left - if so, subtract and return true. + // overflow protection // dailyLimit check + if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) { + m_spentToday += _value; + return true; + } + return false; + } + // determines today's index. + function today() private constant returns (uint) { return now / 1 days; } + + // FIELDS + + uint public m_dailyLimit; + uint public m_spentToday; + uint public m_lastDay; +} + +// interface contract for multisig proxy contracts; see below for docs. +contract multisig { + + // EVENTS + + // logged events: + // Funds has arrived into the wallet (record how much). + event Deposit(address _from, uint value); + // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). + event SingleTransact(address owner, uint value, address to, bytes data); + // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). + event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data); + // Confirmation still needed for a transaction. + event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data); + + // FUNCTIONS + + // TODO: document + function changeOwner(address _from, address _to) external; + function execute(address _to, uint _value, bytes _data) external returns (bytes32); + function confirm(bytes32 _h) returns (bool); +} + +// usage: +// bytes32 h = Wallet(w).from(oneOwner).transact(to, value, data); +// Wallet(w).from(anotherOwner).confirm(h); +contract Wallet is multisig, multiowned, daylimit { + + // TYPES + + // Transaction structure to remember details of transaction lest it need be saved for a later call. + struct Transaction { + address to; + uint value; + bytes data; + } + + // METHODS + + // constructor - just pass on the owner array to the multiowned and + // the limit to daylimit + function Wallet(address[] _owners, uint _required, uint _daylimit) + multiowned(_owners, _required) daylimit(_daylimit) { + } + + // kills the contract sending everything to `_to`. + function kill(address _to) onlymanyowners(sha3(msg.data)) external { + suicide(_to); + } + + // gets called when no other function matches + function() { + // just being sent some cash? + if (msg.value > 0) + Deposit(msg.sender, msg.value); + } + + // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. + // If not, goes into multisig process. We provide a hash on return to allow the sender to provide + // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value + // and _data arguments). They still get the option of using them if they want, anyways. + function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 _r) { + // first, take the opportunity to check that we're under the daily limit. + if (underLimit(_value)) { + SingleTransact(msg.sender, _value, _to, _data); + // yes - just execute the call. + _to.call.value(_value)(_data); + return 0; + } + // determine our operation hash. + _r = sha3(msg.data, block.number); + if (!confirm(_r) && m_txs[_r].to == 0) { + m_txs[_r].to = _to; + m_txs[_r].value = _value; + m_txs[_r].data = _data; + ConfirmationNeeded(_r, msg.sender, _value, _to, _data); + } + } + + // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order + // to determine the body of the transaction from the hash provided. + function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) { + if (m_txs[_h].to != 0) { + m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data); + MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data); + delete m_txs[_h]; + return true; + } + } + + // INTERNAL METHODS + + function clearPending() internal { + uint length = m_pendingIndex.length; + for (uint i = 0; i < length; ++i) + delete m_txs[m_pendingIndex[i]]; + super.clearPending(); + } + + // FIELDS + + // pending transactions we have at present. + mapping (bytes32 => Transaction) m_txs; +} diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 0bf1a46..b012578 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -79,13 +79,12 @@ WHERE repo_id = :repo_id; -- :name create-issue! :! :n -- :doc creates issue -INSERT INTO issues (repo_id, issue_id, issue_number, title, address) +INSERT INTO issues (repo_id, issue_id, issue_number, title) SELECT :repo_id, :issue_id, :issue_number, - :title, - :address + :title WHERE NOT exists(SELECT 1 FROM issues WHERE repo_id = :repo_id AND issue_id = :issue_id); @@ -95,7 +94,27 @@ INSERT INTO issues (repo_id, issue_id, issue_number, title, address) UPDATE issues SET commit_id = :commit_id WHERE issue_id = :issue_id -RETURNING repo_id, issue_id, issue_number, title, address, commit_id; +RETURNING repo_id, issue_id, issue_number, title, commit_id, contract_address; + +-- :name update-transaction-hash :! :n +-- :doc updates transaction-hash for a given issue +UPDATE issues +SET transaction_hash = :transaction_hash +WHERE issue_id = :issue_id; + +-- :name update-contract-address :! :n +-- :doc updates contract-address for a given issue +UPDATE issues +SET contract_address = :contract_address +WHERE issue_id = :issue_id; + +-- :name list-pending-deployments :? :* +-- :doc retrieves pending transaction ids +SELECT + issue_id, + transaction_hash +FROM issues +WHERE contract_address IS NULL; -- Pull Requests ------------------------------------------------------------------- @@ -118,19 +137,19 @@ INSERT INTO pull_requests (repo_id, pr_id, pr_number, issue_number, commit_id, u -- :name bounties-list :? :* -- :doc lists fixed issues SELECT - i.address AS issue_address, - i.issue_id AS issue_id, - i.issue_number AS issue_number, - i.title AS issue_title, - i.repo_id AS repo_id, - p.pr_id AS pr_id, - p.user_id AS user_id, - p.pr_number AS pr_number, - u.address AS payout_address, - u.login AS user_login, - u.name AS user_name, - r.login AS owner_name, - r.repo AS repo_name + i.contract_address AS contract_address, + i.issue_id AS issue_id, + i.issue_number AS issue_number, + i.title AS issue_title, + i.repo_id AS repo_id, + p.pr_id AS pr_id, + p.user_id AS user_id, + p.pr_number AS pr_number, + u.address AS payout_address, + u.login AS user_login, + u.name AS user_name, + r.login AS owner_name, + r.repo AS repo_name FROM issues i INNER JOIN pull_requests p ON (p.commit_id = i.commit_id OR coalesce(p.issue_number, -1) = i.issue_number) @@ -144,13 +163,13 @@ WHERE r.user_id = :owner_id; -- :name issues-list :? :* -- :doc lists all issues SELECT - i.address AS issue_address, - i.issue_id AS issue_id, - i.issue_number AS issue_number, - i.title AS issue_title, - i.repo_id AS repo_id, - r.login AS owner_name, - r.repo AS repo_name + i.contract_address AS contract_address, + i.issue_id AS issue_id, + i.issue_number AS issue_number, + i.title AS issue_title, + i.repo_id AS repo_id, + r.login AS owner_name, + r.repo AS repo_name FROM issues i INNER JOIN repositories r ON r.repo_id = i.repo_id diff --git a/src/clj/commiteth/db/issues.clj b/src/clj/commiteth/db/issues.clj index f1c0951..597ce14 100644 --- a/src/clj/commiteth/db/issues.clj +++ b/src/clj/commiteth/db/issues.clj @@ -5,16 +5,35 @@ (defn create "Creates issue" - [repo-id issue-id issue-number issue-title address] + [repo-id issue-id issue-number issue-title] (jdbc/with-db-connection [con-db *db*] (db/create-issue! con-db {:repo_id repo-id :issue_id issue-id :issue_number issue-number - :title issue-title - :address address}))) + :title issue-title}))) (defn close "Updates issue with commit_id" [commit-id issue-id] (jdbc/with-db-connection [con-db *db*] (db/close-issue! con-db {:issue_id issue-id :commit_id commit-id}))) + +(defn update-transaction-hash + "Updates issue with transaction-hash" + [issue-id transaction-hash] + (jdbc/with-db-connection [con-db *db*] + (db/update-transaction-hash con-db {:issue_id issue-id + :transaction_hash transaction-hash}))) + +(defn update-contract-address + "Updates issue with contract-address" + [issue-id contract-address] + (jdbc/with-db-connection [con-db *db*] + (db/update-contract-address con-db {:issue_id issue-id + :contract_address contract-address}))) + +(defn list-pending-deployments + "Retrieves pending transaction ids" + [] + (jdbc/with-db-connection [con-db *db*] + (db/list-pending-deployments con-db))) diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj new file mode 100644 index 0000000..a6639a2 --- /dev/null +++ b/src/clj/commiteth/eth/core.clj @@ -0,0 +1,61 @@ +(ns commiteth.eth.core + (:require [clojure.data.json :as json] + [org.httpkit.client :refer [post]] + [clojure.java.io :as io] + [overtone.at-at :refer [every mk-pool]] + [commiteth.config :refer [env]] + [commiteth.db.issues :as issues] + [clojure.tools.logging :as log])) + +(def eth-rpc-url "http://localhost:8545") +(defn eth-account [] (:eth-account env)) +(defn eth-password [] (:eth-password env)) + +(defn eth-rpc + [method params] + (let [body (json/write-str {:jsonrpc "2.0" + :method method + :params params + :id 1}) + options {:body body} + result (:body @(post eth-rpc-url options))] + (:result (json/read-str result :key-fn keyword)))) + +(defn compile-solidity + [source] + (eth-rpc "eth_compileSolidity" [source])) + +(defn send-transaction + [from to value & [params]] + (eth-rpc "personal_signAndSendTransaction" [(merge params {:from from + :to to + :value value}) + (eth-password)])) + +(defn get-transaction-receipt + [hash] + (eth-rpc "eth_getTransactionReceipt" [hash])) + +(defn deploy-contract + [] + (let [contract-src (-> "sol/wallet.sol" io/resource slurp) + contract-name :Wallet + contract-data (compile-solidity contract-src) + contract-code (get-in contract-data [contract-name :code])] + (send-transaction (eth-account) nil 1 + {:gas "1248650" + :data contract-code}))) + +;; @todo: move to another ns + +(def pool (mk-pool)) + +(defn update-issue-contract-address [] + (for [{issue-id :issue_id + transaction-hash :transaction_hash} (issues/list-pending-deployments)] + (when-let [receipt (get-transaction-receipt transaction-hash)] + (log/info "transaction receipt for issue #" issue-id ": " receipt) + (when-let [contract-address (:contractAddress receipt)] + (issues/update-contract-address issue-id contract-address))))) + +(every (* 5 60 1000) update-issue-contract-address pool) diff --git a/src/clj/commiteth/github/core.clj b/src/clj/commiteth/github/core.clj index b90237c..ebfb05d 100644 --- a/src/clj/commiteth/github/core.clj +++ b/src/clj/commiteth/github/core.clj @@ -93,9 +93,9 @@ (repos/delete-hook user repo hook-id (auth-params token))) (defn post-comment - [user repo issue-id issue-address] + [user repo issue-id] (issues/create-comment user repo issue-id - (str "a comment with an image link to the web service. Issue address is " issue-address) (self-auth-params))) + (str "a comment with an image link to the web service.") (self-auth-params))) (defn get-commit [user repo commit-id] diff --git a/src/clj/commiteth/routes/webhooks.clj b/src/clj/commiteth/routes/webhooks.clj index 4147f0c..c27f2f0 100644 --- a/src/clj/commiteth/routes/webhooks.clj +++ b/src/clj/commiteth/routes/webhooks.clj @@ -4,10 +4,10 @@ [commiteth.db.pull-requests :as pull-requests] [commiteth.db.issues :as issues] [commiteth.db.users :as users] + [commiteth.eth.core :as eth] [ring.util.http-response :refer [ok]] [clojure.string :refer [join]]) - (:import [java.util UUID] - [java.lang Integer])) + (:import [java.lang Integer])) (def label-name "bounty") @@ -25,6 +25,16 @@ (find-issue-event event-type user) (:commit_id))) +(defn handle-issue-labeled + [issue] + (let [{repo-id :id} (:repository issue) + {issue-id :id + issue-number :number + issue-title :title} (:issue issue) + created-issue (issues/create repo-id issue-id issue-number issue-title)] + (when (= 1 created-issue) + (issues/update-transaction-hash issue-id (eth/deploy-contract))))) + (defn handle-issue-closed [{{{user :login} :owner repo :name} :repository {issue-id :id issue-number :number} :issue}] @@ -95,24 +105,11 @@ (= "labeled" action) (= label-name (get-in issue [:label :name])))) -(defn gen-address [] - (UUID/randomUUID)) - (defn handle-issue [issue] (when-let [action (:action issue)] (when (labeled-as-bounty? action issue) - (let [repository (:repository issue) - {repo-id :id - {owner-login :login} :owner - repo-name :name} repository - issue (:issue issue) - {issue-id :id - issue-number :number - issue-title :title} issue - issue-address (gen-address)] - (github/post-comment owner-login repo-name issue-number issue-address) - (issues/create repo-id issue-id issue-number issue-title issue-address))) + (handle-issue-labeled issue)) (when (and (= "closed" action) (has-bounty-label? (:issue issue))) diff --git a/src/cljs/commiteth/home/page.cljs b/src/cljs/commiteth/home/page.cljs index dde1990..ea80bc1 100644 --- a/src/cljs/commiteth/home/page.cljs +++ b/src/cljs/commiteth/home/page.cljs @@ -19,15 +19,15 @@ [address] (.-length address)) -(defn bounty-row [{issue-id :issue_id - issue-number :issue_number - pr-number :pr_number - user :user_login - owner :owner_name - repo :repo_name - issue-title :issue_title - address :payout_address - issue-address :issue_address}] +(defn bounty-row [{issue-id :issue_id + issue-number :issue_number + pr-number :pr_number + user :user_login + owner :owner_name + repo :repo_name + issue-title :issue_title + address :payout_address + contract-address :contract_address}] ^{:key issue-id} [:li.list-group-item [:div @@ -37,7 +37,7 @@ " by " [:a {:href (user-url user)} user]] [:div "Payout address: " address] - [:div "Amount: " (get-amount issue-address) " ETH"]]) + [:div "Amount: " (get-amount contract-address) " ETH"]]) (defn bounties-list [] (let [bounties (rf/subscribe [:bounties])]