diff --git a/app/app/urls.py b/app/app/urls.py index b71eb9433da..ebb32c8fbef 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -75,6 +75,7 @@ url(r'^bounty/details/?', dashboard.views.bounty_details, name='bounty_details'), url(r'^funding/details/?', dashboard.views.bounty_details, name='funding_details'), url(r'^legacy/funding/details/?', dashboard.views.bounty_details, name='legacy_funding_details'), + url(r'^funding/increase/?', dashboard.views.increase_bounty, name='increase_bounty'), url(r'^funding/kill/?', dashboard.views.kill_bounty, name='kill_bounty'), url(r'^tip/receive/?', dashboard.views.receive_tip, name='receive_tip'), url(r'^tip/send/2/?', dashboard.views.send_tip_2, name='send_tip_2'), diff --git a/app/assets/v2/js/pages/bounty_details.js b/app/assets/v2/js/pages/bounty_details.js index 61890063331..f9986a76485 100644 --- a/app/assets/v2/js/pages/bounty_details.js +++ b/app/assets/v2/js/pages/bounty_details.js @@ -342,9 +342,11 @@ var do_actions = function(result) { var show_github_link = result['github_url'].substring(0, 4) == 'http'; var show_submit_work = true; var show_kill_bounty = !is_status_done && !is_status_expired && !is_status_cancelled; + var show_increase_bounty = !is_status_done && !is_status_expired && !is_status_cancelled; var kill_bounty_enabled = isBountyOwner(result); var submit_work_enabled = !isBountyOwner(result); var start_stop_work_enabled = !isBountyOwner(result); + var increase_bounty_enabled = isBountyOwner(result); if (is_legacy) { show_start_stop_work = false; @@ -420,6 +422,19 @@ var do_actions = function(result) { actions.push(_entry); } + if (show_increase_bounty) { + var _entry = { + href: '/funding/increase?source=' + result['github_url'], + text: 'Add Contribution', + parent: 'right_actions', + color: increase_bounty_enabled ? 'darkBlue' : 'darkGrey', + extraClass: increase_bounty_enabled ? '' : 'disabled', + title: increase_bounty_enabled ? 'Increase the funding of this bounty' : 'Can only be performed if you are the funder.' + }; + + actions.push(_entry); + } + if (show_kill_bounty) { var enabled = kill_bounty_enabled; var _entry = { diff --git a/app/assets/v2/js/pages/increase_bounty.js b/app/assets/v2/js/pages/increase_bounty.js new file mode 100644 index 00000000000..9ec546c653b --- /dev/null +++ b/app/assets/v2/js/pages/increase_bounty.js @@ -0,0 +1,157 @@ +load_tokens(); + +// Wait until page is loaded, then run the function +$(document).ready(function() { + $('input[name=amount]').keyup(setUsdAmount); + $('input[name=amount]').blur(setUsdAmount); + $('select[name=deonomination]').change(setUsdAmount); + + $('input[name=amount]').focus(); + + var denomSelect = $('select[name=deonomination]'); + + localStorage['tokenAddress'] = denomSelect.data('tokenAddress'); + localStorage['amount'] = 0; + denomSelect.select2(); + $('.js-select2').each(function() { + $(this).select2(); + }); + + // submit bounty button click + $('#submitBounty').click(function(e) { + mixpanel.track('Increase Bounty Clicked (funder)', {}); + + // setup + e.preventDefault(); + loading_button($(this)); + + var issueURL = $('input[name=issueURL]').val(); + var amount = $('input[name=amount]').val(); + var tokenAddress = $('select[name=deonomination]').val(); + var token = tokenAddressToDetails(tokenAddress); + var decimals = token['decimals']; + var tokenName = token['name']; + var decimalDivisor = Math.pow(10, decimals); + + // validation + var isError = false; + + if ($('#terms:checked').length == 0) { + _alert({ message: 'Please accept the terms of service.' }); + isError = true; + } else { + localStorage['acceptTOS'] = true; + } + var is_issueURL_invalid = issueURL == '' || + issueURL.indexOf('http') != 0 || + issueURL.indexOf('github') == -1 || + issueURL.indexOf('javascript:') != -1 + + ; + if (is_issueURL_invalid) { + _alert({ message: 'Please enter a valid github issue URL.' }); + isError = true; + } + if (amount == '') { + _alert({ message: 'Please enter an amount.' }); + isError = true; + } + if (isError) { + unloading_button($(this)); + return; + } + $(this).attr('disabled', 'disabled'); + + // setup web3 + // TODO: web3 is using the web3.js file. In the future we will move + // to the node.js package. github.com/ethereum/web3.js + var isETH = tokenAddress == '0x0000000000000000000000000000000000000000'; + var token_contract = web3.eth.contract(token_abi).at(tokenAddress); + var account = web3.eth.coinbase; + + amount = amount * decimalDivisor; + // Create the bounty object. + // This function instantiates a contract from the existing deployed Standard Bounties Contract. + // bounty_abi is a giant object containing the different network options + // bounty_address() is a function that looks up the name of the network and returns the hash code + var bounty = web3.eth.contract(bounty_abi).at(bounty_address()); + + // setup inter page state + localStorage[issueURL] = JSON.stringify({ + 'timestamp': null, + 'txid': null + }); + + function web3Callback(error, result) { + if (error) { + mixpanel.track('Increase Bounty Error (funder)', {step: 'post_bounty', error: error}); + _alert({ message: 'There was an error. Please try again or contact support.' }); + unloading_button($('#submitBounty')); + return; + } + + // update localStorage issuePackage + var issuePackage = JSON.parse(localStorage[issueURL]); + + issuePackage['txid'] = result; + issuePackage['timestamp'] = timestamp(); + localStorage[issueURL] = JSON.stringify(issuePackage); + + _alert({ message: 'Submission sent to web3.' }, 'info'); + setTimeout(function() { + mixpanel.track('Submit New Bounty Success', {}); + document.location.href = '/funding/details/?url=' + issueURL; + }, 1000); + } + + var bountyAmount = parseInt($('input[name=valueInToken]').val(), 10); + var ethAmount = isETH ? amount : 0; + var bountyId = $('input[name=standardBountiesId]').val(); + var fromAddress = $('input[name=bountyOwnerAddress]').val(); + + var errormsg = undefined; + + if (bountyAmount == 0 || open == false) { + errormsg = 'No active funded issue found at this address. Are you sure this is an active funded issue?'; + } + if (fromAddress != web3.eth.coinbase) { + errormsg = 'Only the address that submitted this funded issue may increase the payout.'; + } + + if (errormsg) { + _alert({ message: errormsg }); + unloading_button($('#submitBounty')); + return; + } + + function approveSuccessCallback() { + bounty.increasePayout( + bountyId, + bountyAmount + amount, + amount, + { + from: account, + value: ethAmount, + gasPrice: web3.toHex($('#gasPrice').val()) * Math.pow(10, 9) + }, + web3Callback + ); + } + + if (isETH) { + // no approvals needed for ETH + approveSuccessCallback(); + } else { + token_contract.approve( + bounty_address(), + amount, + { + from: account, + value: 0, + gasPrice: web3.toHex($('#gasPrice').val()) * Math.pow(10, 9) + }, + approveSuccessCallback + ); + } + }); +}); diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js index 1ad09e7cc44..64932812dc8 100644 --- a/app/assets/v2/js/pages/new_bounty.js +++ b/app/assets/v2/js/pages/new_bounty.js @@ -1,13 +1,6 @@ /* eslint-disable no-console */ /* eslint-disable nonblock-statement-body-position */ load_tokens(); -var setUsdAmount = function(event) { - var amount = $('input[name=amount]').val(); - var denomination = $('#token option:selected').text(); - var estimate = getUSDEstimate(amount, denomination, function(estimate) { - $('#usd_amount').html(estimate); - }); -}; // Wait until page is loaded, then run the function $(document).ready(function() { diff --git a/app/assets/v2/js/shared.js b/app/assets/v2/js/shared.js index bf70629d7f2..0c51b2660a7 100644 --- a/app/assets/v2/js/shared.js +++ b/app/assets/v2/js/shared.js @@ -679,3 +679,11 @@ $(document).ready(function() { window.addEventListener('load', function() { setInterval(listen_for_web3_changes, 300); }); + +var setUsdAmount = function(event) { + var amount = $('input[name=amount]').val(); + var denomination = $('#token option:selected').text(); + var estimate = getUSDEstimate(amount, denomination, function(estimate) { + $('#usd_amount').html(estimate); + }); +}; diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index a930e2ffc4c..5a857e44457 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -508,6 +508,8 @@ def process_bounty_changes(old_bounty, new_bounty): event_name = 'killed_bounty' else: event_name = 'work_done' + elif old_bounty.value_in_token < new_bounty.value_in_token: + event_name = 'increased_bounty' else: event_name = 'unknown_event' logging.error(f'got an unknown event from bounty {old_bounty.pk} => {new_bounty.pk}: {json_diff}') diff --git a/app/dashboard/notifications.py b/app/dashboard/notifications.py index 2172dd98538..94359e26664 100644 --- a/app/dashboard/notifications.py +++ b/app/dashboard/notifications.py @@ -91,6 +91,11 @@ def maybe_market_to_twitter(bounty, event_name): "Hot off the blockchain! 🔥🔥🔥 There's a new task worth {} {} {} \n\n{}", "💰 New Task Alert.. 💰 Earn {} {} {} for working on this 👇 \n\n{}", ] + if event_name == 'increased_bounty': + tweet_txts = tweet_txts + [ + "Looking for paid Open Source work? Earn {} {} {} and boost your reputation by completing this task \n\n{}", + "Ding ding ding!! A bounty was just increased to {} {} {}! Someone must really want you to work on this task \n\n{}" + ] random.shuffle(tweet_txts) tweet_txt = tweet_txts[0] @@ -239,6 +244,14 @@ def build_github_notification(bounty, event_name, profile_pairs=None): f"[here]({absolute_url})\n * Questions? Get help on the " \ f"Gitcoin Slack\n * ${amount_open_work}" \ " more Funded OSS Work Available at: https://gitcoin.co/explorer\n" + if event_name == 'increased_bounty': + msg = f"__The funding of this issue was increased to {natural_value} " \ + f"{bounty.token_name} {usdt_value}.__\n\n * If you would " \ + f"like to work on this issue you can claim it [here]({absolute_url}).\n " \ + "* If you've completed this issue and want to claim the bounty you can do so " \ + f"[here]({absolute_url})\n * Questions? Get help on the " \ + f"Gitcoin Slack\n * ${amount_open_work}" \ + " more Funded OSS Work Available at: https://gitcoin.co/explorer\n" elif event_name == 'killed_bounty': msg = f"__The funding of {natural_value} {bounty.token_name} " \ f"{usdt_value} attached to this issue has been **killed** by the bounty submitter__\n\n " \ diff --git a/app/dashboard/templates/increase_bounty.html b/app/dashboard/templates/increase_bounty.html new file mode 100644 index 00000000000..7755c8571eb --- /dev/null +++ b/app/dashboard/templates/increase_bounty.html @@ -0,0 +1,109 @@ +{% comment %} +Copyright (C) 2017 Gitcoin Core + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +{% endcomment %} +{% load static %} + + + + + {% include 'shared/head.html' %} + {% include 'shared/cards.html' %} + + + + {% include 'shared/tag_manager_2.html' %} +
+ {% include 'shared/nav.html' %} +
+
+
+
+
+ {% include 'shared/no_metamask_error.html' %} + {% include 'shared/zero_balance_error.html' %} + {% include 'shared/unlock_metamask.html' %} + + + +
+
+
+

Increase Funding

+ +
+ {% include 'shared/network_status.html' %} +
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+
+ + +
+
+ {% include 'shared/metamask_estimate.html' %} + + {% include 'shared/newsletter.html' %} +
+
+
+
+
+
+ {% include 'shared/bottom_notification.html' %} + {% include 'shared/analytics.html' %} + {% include 'shared/footer_scripts.html' %} + {% include 'shared/rollbar.html' %} + {% include 'shared/footer.html' %} + + + + + + + + + + + + + + + + diff --git a/app/dashboard/tests/test_notifications.py b/app/dashboard/tests/test_notifications.py index 528b1fb9d3e..0e0a4bd03d0 100644 --- a/app/dashboard/tests/test_notifications.py +++ b/app/dashboard/tests/test_notifications.py @@ -69,6 +69,14 @@ def test_build_github_notification_killed_bounty(self): assert 'Questions?' in message assert f'${self.amount_open_work}' in message + def test_build_github_notification_increased_bounty(self): + """Test the dashboard helper build_github_notification method with new_bounty.""" + message = build_github_notification(self.bounty, 'increased_bounty') + assert message.startswith(f'__The funding of this issue was increased to {self.natural_value} {self.bounty.token_name}') + assert self.usdt_value in message + assert f'[here]({self.absolute_url})' in message + assert f'${self.amount_open_work}' in message + def tearDown(self): """Perform cleanup for the testcase.""" self.bounty.delete() diff --git a/app/dashboard/views.py b/app/dashboard/views.py index f6ae7ec3069..74ce85360ae 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -450,6 +450,34 @@ def fulfill_bounty(request): return TemplateResponse(request, 'fulfill_bounty.html', params) +def increase_bounty(request): + """Increase a bounty (funder)""" + issue_url = request.GET.get('source') + params = { + 'issue_url': issue_url, + 'title': 'Increase Bounty', + 'active': 'increase_bounty', + 'recommend_gas_price': recommend_min_gas_price_to_confirm_in_time(confirm_time_minutes_target), + 'eth_usd_conv_rate': eth_usd_conv_rate(), + 'conf_time_spread': conf_time_spread(), + } + + try: + bounties = Bounty.objects.current().filter(github_url=issue_url) + if bounties: + bounty = bounties.order_by('pk').first() + params['standard_bounties_id'] = bounty.standard_bounties_id + params['bounty_owner_address'] = bounty.bounty_owner_address + params['value_in_token'] = bounty.value_in_token + params['token_address'] = bounty.token_address + except Bounty.DoesNotExist: + pass + except Exception as e: + print(e) + logging.error(e) + + return TemplateResponse(request, 'increase_bounty.html', params) + def kill_bounty(request): """Kill an expired bounty."""