diff --git a/app/app/local.env b/app/app/local.env index 618dfa04e95..b6985c7ff45 100644 --- a/app/app/local.env +++ b/app/app/local.env @@ -46,6 +46,7 @@ FEE_ADDRESS_PRIVATE_KEY= GIPHY_KEY= YOUTUBE_API_KEY= VIEW_BLOCK_API_KEY= +ETHERSCAN_API_KEY= FORTMATIC_LIVE_KEY= FORTMATIC_TEST_KEY= diff --git a/app/app/settings.py b/app/app/settings.py index 870e5814454..34d4b7dd096 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -53,6 +53,7 @@ #social integrations GIPHY_KEY = env('GIPHY_KEY', default='LtaY19ToaBSckiLU4QjW0kV9nIP75NFy') YOUTUBE_API_KEY = env('YOUTUBE_API_KEY', default='YOUR-SupEr-SecRet-YOUTUBE-KeY') +ETHERSCAN_API_KEY = env('ETHERSCAN_API_KEY', default='YOUR-ETHERSCAN-KEY') VIEW_BLOCK_API_KEY = env('VIEW_BLOCK_API_KEY', default='YOUR-VIEW-BLOCK-KEY') FORTMATIC_LIVE_KEY = env('FORTMATIC_LIVE_KEY', default='YOUR-SupEr-SecRet-LiVe-FoRtMaTiC-KeY') FORTMATIC_TEST_KEY = env('FORTMATIC_TEST_KEY', default='YOUR-SupEr-SecRet-TeSt-FoRtMaTiC-KeY') diff --git a/app/app/urls.py b/app/app/urls.py index e6377cf250e..1e600ecaf9b 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -214,6 +214,8 @@ path('revenue/attestations/new', revenue.views.new_attestation, name='revenue_new_attestation'), # Hackathons / special events + path('hackathon//new/', dashboard.views.new_hackathon_bounty, name='new_hackathon_bounty'), + path('hackathon//new', dashboard.views.new_hackathon_bounty, name='new_hackathon_bounty2'), path('hackathon//', dashboard.views.hackathon, name='hackathon'), path('hackathon/', dashboard.views.hackathon, name='hackathon2'), path('hackathon//onboard/', dashboard.views.hackathon_onboard, name='hackathon_onboard2'), @@ -376,6 +378,7 @@ re_path(r'^dynamic/viz/graph/(.*)?$', dataviz.d3_views.viz_graph, name='viz_graph'), re_path(r'^dynamic/viz/sscatterplot/(.*)?$', dataviz.d3_views.viz_scatterplot_stripped, name='viz_sscatterplot'), path('dynamic/js/tokens_dynamic.js', retail.views.tokens, name='tokens'), + url('^api/v1/tokens', retail.views.json_tokens, name='json_tokens'), # sync methods url(r'^sync/web3/?', dashboard.views.sync_web3, name='sync_web3'), diff --git a/app/assets/v2/images/chains/celo.svg b/app/assets/v2/images/chains/celo.svg new file mode 100644 index 00000000000..5094ca607c7 --- /dev/null +++ b/app/assets/v2/images/chains/celo.svg @@ -0,0 +1 @@ + diff --git a/app/assets/v2/images/chains/ethereum-classic.svg b/app/assets/v2/images/chains/ethereum-classic.svg new file mode 100644 index 00000000000..bbdcfa74fcd --- /dev/null +++ b/app/assets/v2/images/chains/ethereum-classic.svg @@ -0,0 +1 @@ + diff --git a/app/assets/v2/images/chains/ethereum.svg b/app/assets/v2/images/chains/ethereum.svg new file mode 100644 index 00000000000..192b677c2e6 --- /dev/null +++ b/app/assets/v2/images/chains/ethereum.svg @@ -0,0 +1 @@ + diff --git a/app/assets/v2/images/chains/paypal.svg b/app/assets/v2/images/chains/paypal.svg new file mode 100644 index 00000000000..ea9836db20f --- /dev/null +++ b/app/assets/v2/images/chains/paypal.svg @@ -0,0 +1 @@ + diff --git a/app/assets/v2/images/chains/zilliqa.svg b/app/assets/v2/images/chains/zilliqa.svg new file mode 100644 index 00000000000..1034b6638ef --- /dev/null +++ b/app/assets/v2/images/chains/zilliqa.svg @@ -0,0 +1 @@ + diff --git a/app/assets/v2/js/pages/bounty_detail/web3_modal.js b/app/assets/v2/js/pages/bounty_detail/web3_modal.js new file mode 100644 index 00000000000..ae40cb28cd2 --- /dev/null +++ b/app/assets/v2/js/pages/bounty_detail/web3_modal.js @@ -0,0 +1,50 @@ +const payWithWeb3 = (fulfillment_id, fulfiller_address, vm, modal) => { + + const amount = vm.fulfillment_context.amount; + const token_name = vm.bounty.token_name; + + web3.eth.sendTransaction({ + to: fulfiller_address, + from: selectedAccount, + value: web3.utils.toWei(String(amount)), + gasPrice: web3.utils.toHex(5 * Math.pow(10, 9)), + gas: web3.utils.toHex(318730), + gasLimit: web3.utils.toHex(318730) + }, + function(error, result) { + if (error) { + _alert({ message: gettext('Unable to payout bounty. Please try again.') }, 'error'); + console.log(error); + } else { + + const payload = { + payout_type: 'web3_modal', + tenant: 'ETH', + amount: amount, + token_name: token_name, + funder_address: selectedAccount, + payout_tx_id: result, + payout_status: 'done' + }; + + modal.closeModal(); + const apiUrlBounty = `/api/v1/bounty/payout/${fulfillment_id}`; + + fetchData(apiUrlBounty, 'POST', payload).then(response => { + if (200 <= response.status && response.status <= 204) { + console.log('success', response); + + vm.fetchBounty(); + _alert('Payment Successful'); + + } else { + _alert('Unable to make payout bounty. Please try again later', 'error'); + console.error(`error: bounty payment failed with status: ${response.status} and message: ${response.message}`); + } + }).catch(function(error) { + _alert('Unable to make payout bounty. Please try again later', 'error'); + console.log(error); + }); + } + }); +} \ No newline at end of file diff --git a/app/assets/v2/js/pages/bounty_details2.js b/app/assets/v2/js/pages/bounty_details2.js index 103acf4b566..71152753932 100644 --- a/app/assets/v2/js/pages/bounty_details2.js +++ b/app/assets/v2/js/pages/bounty_details2.js @@ -38,6 +38,7 @@ Vue.mixin({ projectModal(vm.bounty.pk); } vm.staffOptions(); + vm.fetchIfPendingFulfillments(); }).catch(function(error) { vm.loadingState = 'error'; _alert('Error fetching bounties. Please contact founders@gitcoin.co', 'error'); @@ -279,7 +280,7 @@ Vue.mixin({ const decimals = tokenNameToDetails('mainnet', token_name).decimals; const amount = vm.fulfillment_context.amount; const payout_tx_id = vm.fulfillment_context.payout_tx_id ? vm.fulfillment_context.payout_tx_id : null; - const bounty_owner_address = vm.bounty.bounty_owner_address; + const funder_address = vm.bounty.bounty_owner_address; const tenant = vm.getTenant(token_name); const payload = { @@ -287,7 +288,7 @@ Vue.mixin({ tenant: tenant, amount: amount * 10 ** decimals, token_name: token_name, - bounty_owner_address: bounty_owner_address, + funder_address: funder_address, payout_tx_id: payout_tx_id }; @@ -328,6 +329,12 @@ Vue.mixin({ payWithPYPL(fulfillment_id, fulfiller_identifier, ele, vm, modal); }); }, + payWithWeb3Step: function(fulfillment_id, fulfiller_address) { + let vm = this; + const modal = this.$refs['payout-modal'][0]; + + payWithWeb3(fulfillment_id, fulfiller_address, vm, modal); + }, closeBounty: function() { let vm = this; @@ -416,6 +423,23 @@ Vue.mixin({ ); }, + fetchIfPendingFulfillments: function() { + let vm = this; + + const pendingFulfillments = vm.bounty.fulfillments.filter(fulfillment => + fulfillment.payout_status == 'pending' + ); + + if (pendingFulfillments.length > 0) { + if (!vm.pollInterval) { + vm.pollInterval = setInterval(vm.fetchBounty, 60000); + } + } else { + clearInterval(vm.pollInterval); + vm.pollInterval = null; + } + return; + }, stopWork: function(isOwner) { let text = isOwner ? 'Are you sure you would like to stop this user from working on this bounty ?' : @@ -478,6 +502,8 @@ Vue.mixin({ vm.fulfillment_context.active_step = 'payout_amount'; } else if (fulfillment.payout_type == 'qr') { vm.fulfillment_context.active_step = 'check_wallet_owner'; + } else if (fulfillment.payout_type == 'web3_modal') { + vm.fulfillment_context.active_step = 'payout_amount'; } } }, @@ -528,7 +554,8 @@ if (document.getElementById('gc-bounty-detail')) { decimals: 18, inputBountyOwnerAddress: bounty.bounty_owner_address, contxt: document.contxt, - quickLinks: [] + quickLinks: [], + pollInterval: null }; }, mounted() { diff --git a/app/assets/v2/js/pages/hackathon_new_bounty.js b/app/assets/v2/js/pages/hackathon_new_bounty.js new file mode 100644 index 00000000000..9cbae4999f2 --- /dev/null +++ b/app/assets/v2/js/pages/hackathon_new_bounty.js @@ -0,0 +1,355 @@ +let appFormHackathon; + +window.addEventListener('dataWalletReady', function(e) { + appFormHackathon.network = networkName; + appFormHackathon.form.funderAddress = selectedAccount; +}, false); + +Vue.component('v-select', VueSelect.VueSelect); +Vue.mixin({ + methods: { + getIssueDetails: function(url) { + let vm = this; + + if (!url) { + vm.$set(vm.errors, 'issueDetails', undefined); + vm.form.issueDetails = null; + return vm.form.issueDetails; + } + + if (url.indexOf('github.com/') < 0) { + vm.form.issueDetails = null; + vm.$set(vm.errors, 'issueDetails', 'Please paste a github issue url'); + return; + } + + let ghIssueUrl = new URL(url); + + vm.orgSelected = ''; + + const apiUrldetails = `/sync/get_issue_details?url=${encodeURIComponent(url.trim())}&hackathon_slug=${vm.hackathonSlug}`; + + vm.$set(vm.errors, 'issueDetails', undefined); + + vm.form.issueDetails = undefined; + const getIssue = fetchData(apiUrldetails, 'GET'); + + $.when(getIssue).then((response) => { + vm.orgSelected = ghIssueUrl.pathname.split('/')[1]; + vm.form.issueDetails = response; + vm.$set(vm.errors, 'issueDetails', undefined); + }).catch((err) => { + console.log(err); + vm.$set(vm.errors, 'issueDetails', err.responseJSON.message); + }); + + }, + getTokens: function() { + let vm = this; + const apiUrlTokens = '/api/v1/tokens/'; + const getTokensData = fetchData(apiUrlTokens, 'GET'); + + $.when(getTokensData).then((response) => { + vm.tokens = response; + vm.form.token = vm.filterByChainId[0]; + vm.getAmount(vm.form.token.symbol); + + }).catch((err) => { + console.log(err); + // vm.errorIssueDetails = err.responseJSON.message; + }); + + }, + getAmount: function(token) { + let vm = this; + + if (!token) { + return; + } + const apiUrlAmount = `/sync/get_amount?amount=1&denomination=${token}`; + const getAmountData = fetchData(apiUrlAmount, 'GET'); + + $.when(getAmountData).then((response) => { + vm.coinValue = response.usdt; + vm.calcValues('usd'); + + }).catch((err) => { + console.log(err); + }); + }, + calcValues: function(direction) { + let vm = this; + + if (direction == 'usd') { + let usdValue = vm.form.amount * vm.coinValue; + + vm.form.amountusd = Number(usdValue.toFixed(2)); + } else { + vm.form.amount = Number(vm.form.amountusd * 1 / vm.coinValue).toFixed(4); + console.log(vm.form.amount); + } + + }, + addKeyword: function(item) { + let vm = this; + + vm.form.keywords.push(item); + }, + checkForm: async function(e) { + let vm = this; + + vm.errors = {}; + + if (!vm.form.keywords.length) { + vm.$set(vm.errors, 'keywords', 'Please select the prize keywords'); + } + if (!vm.form.experience_level || !vm.form.project_length || !vm.form.bounty_type) { + vm.$set(vm.errors, 'experience_level', 'Please select the details options'); + } + if (!vm.chainId) { + vm.$set(vm.errors, 'chainId', 'Please select an option'); + } + if (!vm.form.issueDetails || vm.form.issueDetails < 1) { + vm.$set(vm.errors, 'issueDetails', 'Please input a GitHub issue'); + } + if (vm.form.bounty_categories.length < 1) { + vm.$set(vm.errors, 'bounty_categories', 'Select at least one category'); + } + if (!vm.form.funderAddress) { + vm.$set(vm.errors, 'funderAddress', 'Fill the owner wallet address'); + } + if (!vm.form.project_type) { + vm.$set(vm.errors, 'project_type', 'Select the project type'); + } + if (!vm.form.permission_type) { + vm.$set(vm.errors, 'permission_type', 'Select the permission type'); + } + if (!vm.terms) { + vm.$set(vm.errors, 'terms', 'You need to accept the terms'); + } + if (Object.keys(vm.errors).length) { + return false; + } + }, + web3Type() { + let vm = this; + let type; + + switch (vm.chainId) { + case '1': + // ethereum + type = 'web3_modal'; + break; + case '666': + // paypal + type = 'fiat'; + break; + case '61': // ethereum classic + case '102': // zilliqa + case '42220': // celo mainnet + case '44786': // celo alfajores tesnet + type = 'qr'; + break; + default: + type = 'web3_modal'; + } + + vm.form.web3_type = type; + return type; + }, + submitForm: async function(event) { + event.preventDefault(); + let vm = this; + + vm.checkForm(event); + + if (!provider && vm.chainId === '1') { + onConnect(); + return false; + } + + if (Object.keys(vm.errors).length) { + return false; + } + const metadata = { + issueTitle: vm.form.issueDetails.title, + issueDescription: vm.form.issueDetails.description, + issueKeywords: vm.form.keywords.join(), + githubUsername: vm.form.githubUsername, + notificationEmail: vm.form.notificationEmail, + fullName: vm.form.fullName, + experienceLevel: vm.form.experience_level, + projectLength: vm.form.project_length, + bountyType: vm.form.bounty_type, + estimatedHours: vm.form.hours, + fundingOrganisation: '', + eventTag: vm.form.eventTag, + is_featured: undefined, + repo_type: 'public', + featuring_date: 0, + reservedFor: '', + releaseAfter: '', + tokenName: vm.form.token.symbol, + invite: [], + bounty_categories: vm.form.bounty_categories.join(), + activity: '', + chain_id: vm.chainId + }; + + const params = { + 'title': metadata.issueTitle, + 'amount': vm.form.amount, + 'value_in_token': vm.form.amount * 10 ** vm.form.token.decimals, + 'token_name': metadata.tokenName, + 'token_address': vm.form.token.address, + 'bounty_type': metadata.bountyType, + 'project_length': metadata.projectLength, + 'estimated_hours': metadata.estimatedHours, + 'experience_level': metadata.experienceLevel, + 'github_url': vm.form.issueUrl, + 'bounty_owner_email': metadata.notificationEmail, + 'bounty_owner_github_username': metadata.githubUsername, + 'bounty_owner_name': metadata.fullName, // ETC-TODO REMOVE ? + 'bounty_reserved_for': metadata.reservedFor, + 'release_to_public': metadata.releaseAfter, + 'expires_date': vm.hackathonEndDate, + 'metadata': JSON.stringify(metadata), + 'raw_data': {}, // ETC-TODO REMOVE ? + 'network': vm.network, + 'issue_description': metadata.issueDescription, + 'funding_organisation': metadata.fundingOrganisation, + 'balance': vm.form.amount * 10 ** vm.form.token.decimals, // ETC-TODO REMOVE ? + 'project_type': vm.form.project_type, + 'permission_type': vm.form.permission_type, + 'bounty_categories': metadata.bounty_categories, + 'repo_type': metadata.repo_type, + 'is_featured': metadata.is_featured, + 'featuring_date': metadata.featuring_date, + 'fee_amount': 0, + 'fee_tx_id': null, + 'coupon_code': '', + 'privacy_preferences': JSON.stringify({ + show_email_publicly: '1' + }), + 'attached_job_description': '', + 'eventTag': metadata.eventTag, + 'auto_approve_workers': 'True', + 'web3_type': vm.web3Type(), + 'activity': metadata.activity, + 'bounty_owner_address': vm.form.funderAddress + }; + + vm.sendBounty(params); + + }, + sendBounty(data) { + let vm = this; + const apiUrlBounty = '/api/v1/bounty/create'; + const postBountyData = fetchData(apiUrlBounty, 'POST', data); + + $.when(postBountyData).then((response) => { + if (200 <= response.status && response.status <= 204) { + console.log('success', response); + window.location.href = response.bounty_url; + } else if (response.status == 304) { + _alert('Bounty already exists for this github issue.', 'error'); + console.error(`error: bounty creation failed with status: ${response.status} and message: ${response.message}`); + } else { + _alert(`Unable to create a bounty. ${response.message}`, 'error'); + console.error(`error: bounty creation failed with status: ${response.status} and message: ${response.message}`); + } + + }).catch((err) => { + console.log(err); + _alert('Unable to create a bounty. Please try again later', 'error'); + }); + + } + }, + computed: { + sortByPriority: function() { + return this.tokens.sort(function(a, b) { + return b.priority - a.priority; + }); + }, + filterByNetwork: function() { + const vm = this; + + if (vm.network == '') { + return vm.sortByPriority; + } + return vm.sortByPriority.filter((item)=>{ + + return item.network.toLowerCase().indexOf(vm.network.toLowerCase()) >= 0; + }); + }, + filterByChainId: function() { + const vm = this; + let result; + + vm.form.token = {}; + if (vm.chainId == '') { + result = vm.filterByNetwork; + } else { + result = vm.filterByNetwork.filter((item) => { + return String(item.chainId) === vm.chainId; + }); + } + vm.form.token = result[0]; + return result; + } + }, + watch: { + chainId: async function(val) { + if (!provider && val === '1') { + await onConnect(); + } + await this.checkForm(); + } + } +}); + +if (document.getElementById('gc-hackathon-new-bounty')) { + appFormHackathon = new Vue({ + delimiters: [ '[[', ']]' ], + el: '#gc-hackathon-new-bounty', + components: { + 'vue-select': 'vue-select' + }, + data() { + return { + tokens: [], + network: 'mainnet', + chainId: '', + terms: false, + hackathonSlug: document.hackathon.slug, + hackathonEndDate: document.hackathon.endDate, + errors: {}, + sponsors: document.sponsors, + orgSelected: '', + selected: null, + coinValue: null, + form: { + eventTag: document.hackathon.name, + issueDetails: undefined, + issueUrl: '', + githubUsername: document.contxt.github_handle, + notificationEmail: document.contxt.email, + fullName: document.contxt.name, + hours: '24', + bounty_categories: [], + project_type: '', + permission_type: '', + keywords: [], + amount: 0.001, + amountusd: null, + token: {} + } + }; + }, + mounted() { + this.getTokens(); + + } + }); +} diff --git a/app/assets/v2/js/vue-filters.js b/app/assets/v2/js/vue-filters.js index bd81f3ffda2..561cb9df4d8 100644 --- a/app/assets/v2/js/vue-filters.js +++ b/app/assets/v2/js/vue-filters.js @@ -37,6 +37,7 @@ Vue.filter('markdownit', function(val) { if (!val) return ''; const _markdown = new markdownit({ + html: true, linkify: true, highlight: function(str, lang) { if (lang && hljs.getLanguage(lang)) { diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index 6de8ae95e3d..1b8bd1dafa0 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -43,7 +43,7 @@ ) from dashboard.tokens import addr_to_token from economy.utils import ConversionRateNotFoundError, convert_amount -from git.utils import get_gh_issue_details, get_url_dict +from git.utils import get_gh_issue_details, get_url_dict, org_name from jsondiff import diff from marketing.mails import new_reserved_issue from pytz import UTC @@ -179,6 +179,16 @@ def issue_details(request): token = request.GET.get('token', None) url = request.GET.get('url') url_val = URLValidator() + hackathon_slug = request.GET.get('hackathon_slug') + + + if hackathon_slug: + sponsor_profiles = HackathonEvent.objects.filter(slug__iexact=hackathon_slug).prefetch_related('sponsor_profiles').values_list('sponsor_profiles__handle', flat=True) + org_issue = org_name(url).lower() + + if org_issue not in sponsor_profiles: + message = 'This issue is not under any sponsor repository' + return JsonResponse({'status':'false','message':message}, status=404) try: url_val(url) @@ -198,7 +208,8 @@ def issue_details(request): response['message'] = 'could not parse Github url' except Exception as e: logger.warning(e) - response['message'] = 'could not pull back remote response' + message = 'could not pull back remote response' + return JsonResponse({'status':'false','message':message}, status=404) return JsonResponse(response) diff --git a/app/dashboard/management/commands/sync_pending_fulfillments.py b/app/dashboard/management/commands/sync_pending_fulfillments.py index 0dba5bae917..defe52b9ddd 100644 --- a/app/dashboard/management/commands/sync_pending_fulfillments.py +++ b/app/dashboard/management/commands/sync_pending_fulfillments.py @@ -35,11 +35,18 @@ def handle(self, *args, **options): payout_status='pending' ) - timeout_period = timezone.now() - timedelta(minutes=20) + web3_modal_pending_fulfillments = pending_fulfillments.filter(payout_type='web3_modal') + if web3_modal_pending_fulfillments: + for fulfillment in web3_modal_pending_fulfillments.all(): + sync_payout(fulfillment) - pending_fulfillments.filter(created_on__lt=timeout_period).update(payout_status='expired') - fulfillments = pending_fulfillments.filter(payout_status='pending') + qr_pending_fulfillments = pending_fulfillments.filter(payout_type='qr') + if qr_pending_fulfillments: + # Auto expire pending transactions + timeout_period = timezone.now() - timedelta(minutes=20) + qr_pending_fulfillments.filter(created_on__lt=timeout_period).update(payout_status='expired') - for fulfillment in fulfillments.all(): - sync_payout(fulfillment) + fulfillments = qr_pending_fulfillments.filter(payout_status='pending') + for fulfillment in fulfillments.all(): + sync_payout(fulfillment) diff --git a/app/dashboard/models.py b/app/dashboard/models.py index e5a0c8f20f6..b00552e44c2 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -279,8 +279,15 @@ class Bounty(SuperModel): WORK_IN_PROGRESS_STATUSES = ['reserved', 'open', 'started', 'submitted'] TERMINAL_STATUSES = ['done', 'expired', 'cancelled'] + WEB3_TYPES = ( + ('legacy_gitcoin', 'Legacy Bounty'), + ('bounties_network', 'Bounties Network'), + ('qr', 'QR Code'), + ('web3_modal', 'Web3 Modal') + ) + bounty_state = models.CharField(max_length=50, choices=BOUNTY_STATES, default='open', db_index=True) - web3_type = models.CharField(max_length=50, default='bounties_network') + web3_type = models.CharField(max_length=50, choices=WEB3_TYPES, default='bounties_network') title = models.CharField(max_length=1000) web3_created = models.DateTimeField(db_index=True) value_in_token = models.DecimalField(default=1, decimal_places=2, max_digits=50) @@ -1337,7 +1344,8 @@ class BountyFulfillment(SuperModel): PAYOUT_TYPE = [ ('bounties_network', 'bounties_network'), ('qr', 'qr'), - ('fiat', 'fiat') + ('fiat', 'fiat'), + ('web3_modal', 'web3_modal') ] TENANT = [ diff --git a/app/dashboard/sync/eth.py b/app/dashboard/sync/eth.py new file mode 100644 index 00000000000..849fa2487b5 --- /dev/null +++ b/app/dashboard/sync/eth.py @@ -0,0 +1,64 @@ +import logging + +from django.conf import settings +from django.utils import timezone + +import requests +from dashboard.sync.helpers import record_payout_activity + +logger = logging.getLogger(__name__) + +API_KEY = settings.ETHERSCAN_API_KEY + +def get_eth_txn_status(txnid, network='mainnet'): + if not txnid: + return None + + response = { + 'status': 'pending' + } + + try: + if network == 'mainnet': + etherscan_url = f'https://api.etherscan.io/api?module=transaction&action=gettxreceiptstatus&txhash={txnid}&apikey={API_KEY}' + else: + etherscan_url = f'https://api-rinkeby.etherscan.io/api?module=transaction&action=gettxreceiptstatus&txhash={txnid}&apikey={API_KEY}' + + headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0. 2272.118 Safari/537.36.'} + etherscan_response = requests.get(etherscan_url, headers=headers).json() + result = etherscan_response['result'] + + response = { + 'status': 'pending' + } + + if result: + + if result.get('status') == '1': + response = { + 'status': 'done' + } + elif result.get('status') == '0': + response = { + 'status': 'expired' + } + + except Exception as e: + logger.error(f'error: get_eth_txn_status - {e}') + finally: + return response + + +def sync_eth_payout(fulfillment): + if fulfillment.payout_tx_id: + txn_status = get_eth_txn_status(fulfillment.payout_tx_id, fulfillment.bounty.network) + if txn_status: + if txn_status.get('status') == 'done': + fulfillment.payout_status = 'done' + fulfillment.accepted_on = timezone.now() + fulfillment.accepted = True + record_payout_activity(fulfillment) + elif txn_status.get('status') == 'expired': + fulfillment.payout_status = 'expired' + + fulfillment.save() diff --git a/app/dashboard/sync/helpers.py b/app/dashboard/sync/helpers.py index b1205a187b6..4d00393e338 100644 --- a/app/dashboard/sync/helpers.py +++ b/app/dashboard/sync/helpers.py @@ -37,4 +37,4 @@ def record_payout_activity(fulfillment): Activity.objects.create(**kwargs) except Exception as e: - logger.error(f"error in record_bounty_activity: {e} - {event_name} - {bounty} - {user}") + logger.error(f"error in record_bounty_activity: {e} - {event_name} - {bounty}") diff --git a/app/dashboard/templates/bounty/details2.html b/app/dashboard/templates/bounty/details2.html index 644e197307b..0f133b641ff 100644 --- a/app/dashboard/templates/bounty/details2.html +++ b/app/dashboard/templates/bounty/details2.html @@ -401,6 +401,75 @@
{% trans "SUBMISSIONS" %}
+ +
+ +
+

Payout

+ + +

+ [[ fulfillment.fulfiller_github_username ]] +

+ +

+ [[ fulfillment.fulfiller_address | truncateHash ]] +

+
+ + +
+

How much are you paying out to this person?

+ +

+ + [[ bounty.token_name ]] +

+ + + +
+ + +
+

Pay using your web3 wallet.

+ +
+

+ From: [[ bounty.bounty_owner_address ]] +

+

+ Amount: [[ fulfillment_context.amount ]] [[ bounty.token_name ]] +

+

To:

+

+ + [[ fulfillment.fulfiller_address ]] + + Copy + +

+
+ + +

+ + + Previous Step + +

+
+ +
+
@@ -871,6 +940,7 @@

{{ noscript.keywords }}

const csrftoken = $('input[name="csrfmiddlewaretoken"]').attr('value'); + @@ -880,14 +950,6 @@

{{ noscript.keywords }}

- {% for message in messages %} - {% if message.tags == 'success'%} - - {% endif %} - - {% endfor %} diff --git a/app/dashboard/templates/dashboard/hackathon/new_bounty.html b/app/dashboard/templates/dashboard/hackathon/new_bounty.html new file mode 100644 index 00000000000..4d64c5d2800 --- /dev/null +++ b/app/dashboard/templates/dashboard/hackathon/new_bounty.html @@ -0,0 +1,483 @@ +{% comment %} + Copyright (C) 2019 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 i18n static email_obfuscator add_url_schema avatar_tags %} + + + + + {% include 'shared/head.html' %} + {% include 'shared/cards.html' %} + + + + + + + + + {% include 'shared/tag_manager_2.html' %} +
+ {% include 'shared/top_nav.html' with class='d-md-flex' %} + {% include 'shared/nav.html' %} +
+
+ {% firstof hackathon.logo_svg or hackathon.logo as logo %} + {% if logo %} + + {% else %} +
+ {{ hackathon.name }} +
+ {% endif %} +
+
+
+
+
+
+

Fund Prize

+

Fund your GitHub issue and work with talented developers!

+ +
+ +

Pick the chain you will fund the bounty

+ +
+ + + + {% if is_staff %} + + {% endif %} + + + + + + {% if is_staff %} + + {% endif %} + +
+
+ [[errors.chainId]] +
+
+
+ + + + + +
+ [[errors.issueDetails]] +
+
+ +
+
+ Issue Title +
+
+ [[form.issueDetails.title]] +
+ +
+ Issue Details +
+
+ + Edit on Github + + +
+ +

Insert keywords relevant to your issue to make it easily discoverable by contributors

+
+ + + + + + + +
+
+ Add tags from your repo: +
    +
  • [[keyword]]
  • +
+
+
+ [[errors.keywords]] +
+
+
+
+
+ +
+
+ +
+ +

Pick the most accurate categories for this bounty to get the right contributors

+ +
+ + + + + +
+
+ [[errors.bounty_categories]] +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ [[errors.project_type]] +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ [[errors.permission_type]] +
+
+ +
+ +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ [[errors.experience_level || errors.project_length || errors.bounty_type]] +
+
+ +
+ +
+ +
+
+ + + + + + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+ [[errors.funderAddress]] +
+
+
+
+
+ +
+
+
+
+
+
Total
+
+
+

Payment Due

+ [[form.amount]] [[form.token.symbol]] + +

+ Bounty 0.001 + [[form.token.symbol]] ($[[form.amountusd]]) +

+ +
+
+
+
+ + +
+
+ [[errors.terms]] +
+ +
+
+
+
+
+
+
+ +
+
+ Please verify forms errors and try again +
+
+
+
+
+ + + {% include 'shared/bottom_notification.html' %} + {% include 'shared/analytics.html' %} + {% include 'shared/footer.html' %} + {% include 'shared/footer_scripts.html' with vue=True %} + {% include 'shared/current_profile.html' %} + + + + + + + + + + + + diff --git a/app/dashboard/utils.py b/app/dashboard/utils.py index bb3fcf678b5..d0d8a263be6 100644 --- a/app/dashboard/utils.py +++ b/app/dashboard/utils.py @@ -36,6 +36,7 @@ from dashboard.models import Activity, BlockedUser, Bounty, BountyFulfillment, Profile, UserAction from dashboard.sync.celo import sync_celo_payout from dashboard.sync.etc import sync_etc_payout +from dashboard.sync.eth import sync_eth_payout from dashboard.sync.zil import sync_zil_payout from eth_utils import to_checksum_address from gas.utils import conf_time_spread, eth_usd_conv_rate, gas_advisories, recommend_min_gas_price_to_confirm_in_time @@ -487,12 +488,16 @@ def sync_payout(fulfillment): if not token_name: token_name = fulfillment.bounty.token_name - if token_name == 'ETC': - sync_etc_payout(fulfillment) - elif token_name == 'cUSD' or token_name == 'cGLD': - sync_celo_payout(fulfillment) - elif token_name == 'ZIL': - sync_zil_payout(fulfillment) + if fulfillment.payout_type == 'web3_modal': + sync_eth_payout(fulfillment) + + elif fulfillment.payout_type == 'qr': + if token_name == 'ETC': + sync_etc_payout(fulfillment) + elif token_name == 'cUSD' or token_name == 'cGLD': + sync_celo_payout(fulfillment) + elif token_name == 'ZIL': + sync_zil_payout(fulfillment) def get_bounty_id(issue_url, network): diff --git a/app/dashboard/views.py b/app/dashboard/views.py index f1c4bd07404..4a855d1acc1 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -3263,6 +3263,32 @@ def new_bounty(request): return TemplateResponse(request, 'bounty/fund.html', params) +@login_required +def new_hackathon_bounty(request, hackathon=''): + """Create a new hackathon bounty.""" + from .utils import clean_bounty_url + + try: + hackathon_event = HackathonEvent.objects.filter(slug__iexact=hackathon).prefetch_related('sponsor_profiles').latest('id') + except HackathonEvent.DoesNotExist: + return redirect(reverse('get_hackathons')) + + bounty_params = { + 'newsletter_headline': _('Be the first to know about new funded issues.'), + 'issueURL': clean_bounty_url(request.GET.get('source') or request.GET.get('url', '')), + 'amount': request.GET.get('amount'), + 'hackathon':hackathon_event + } + + params = get_context( + user=request.user if request.user.is_authenticated else None, + confirm_time_minutes_target=confirm_time_minutes_target, + active='submit_bounty', + title=_('Create Funded Issue'), + update=bounty_params + ) + return TemplateResponse(request, 'dashboard/hackathon/new_bounty.html', params) + @csrf_exempt def get_suggested_contributors(request): previously_worked_developers = [] @@ -4758,7 +4784,7 @@ def create_bounty_v1(request): bounty.bounty_categories = request.POST.get("bounty_categories", '').split(',') bounty.network = request.POST.get("network", 'mainnet') bounty.admin_override_suspend_auto_approval = not request.POST.get("auto_approve_workers", True) - bounty.value_in_token = request.POST.get("value_in_token", 0) + bounty.value_in_token = float(request.POST.get("value_in_token", 0)) bounty.token_address = request.POST.get("token_address") bounty.bounty_owner_email = request.POST.get("bounty_owner_email") bounty.bounty_owner_name = request.POST.get("bounty_owner_name", '') # ETC-TODO: REMOVE ? @@ -5114,8 +5140,8 @@ def payout_bounty_v1(request, fulfillment_id): if not payout_type: response['message'] = 'error: missing parameter payout_type' return JsonResponse(response) - if payout_type not in ['fiat', 'qr']: - response['message'] = 'error: parameter payout_type must be fiat / qr' + if payout_type not in ['fiat', 'qr', 'web3_modal']: + response['message'] = 'error: parameter payout_type must be fiat / qr / web_modal' return JsonResponse(response) tenant = request.POST.get('tenant') @@ -5148,18 +5174,14 @@ def payout_bounty_v1(request, fulfillment_id): fulfillment.funder_identifier = funder_identifier fulfillment.payout_status = payout_status - elif payout_type == 'qr': - - if not bounty.bounty_owner_address: - bounty_owner_address = request.POST.get('bounty_owner_address') - if not bounty_owner_address: - response['message'] = 'error: missing parameter bounty_owner_address' - return JsonResponse(response) + else: - bounty.bounty_owner_address = bounty_owner_address - bounty.save() + funder_address = request.POST.get('funder_address') + if not funder_address : + response['message'] = 'error: missing parameter funder_address for web3 modal / qr payment' + return JsonResponse(response) - fulfillment.funder_address = fulfillment.bounty.bounty_owner_address # TODO: Obtain from frontend for tribe mgmt + fulfillment.funder_address = fulfillment.funder_address fulfillment.payout_status = 'pending' payout_tx_id = request.POST.get('payout_tx_id') @@ -5180,7 +5202,7 @@ def payout_bounty_v1(request, fulfillment_id): fulfillment.save() - if payout_type == 'qr': + if payout_type == 'qr' or payout_type == 'web3_modal': sync_payout(fulfillment) response = { @@ -5398,7 +5420,6 @@ def validate_number(user, twilio, phone, redis, delivery_method='sms'): redis.set(f'verification:{user.id}:phone', hash_number) - @login_required def send_verification(request): user = request.user diff --git a/app/economy/migrations/0002_auto_20200616_2140.py b/app/economy/migrations/0002_auto_20200616_2140.py new file mode 100644 index 00000000000..f8173386e1c --- /dev/null +++ b/app/economy/migrations/0002_auto_20200616_2140.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2020-06-16 21:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('economy', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='token', + name='chain_id', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='token', + name='network_id', + field=models.IntegerField(default=1), + ), + ] diff --git a/app/economy/models.py b/app/economy/models.py index 54dfecfa9e1..b06a295eef6 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -215,6 +215,8 @@ class Token(SuperModel): network = models.CharField(max_length=25, db_index=True) decimals = models.IntegerField(default=18) priority = models.IntegerField(default=1) + chain_id = models.IntegerField(default=1) + network_id = models.IntegerField(default=1) metadata = JSONField(null=True, default=dict, blank=True) approved = models.BooleanField(default=True) diff --git a/app/economy/utils.py b/app/economy/utils.py index 73f2af178b8..b407a6367a1 100644 --- a/app/economy/utils.py +++ b/app/economy/utils.py @@ -61,6 +61,8 @@ def convert_amount(from_amount, from_currency, to_currency, timestamp=None): if to_currency in settings.STABLE_COINS: to_currency = 'USDT' + if from_currency == to_currency: + return float(from_amount) if timestamp: conversion_rate = ConversionRate.objects.filter( diff --git a/app/retail/views.py b/app/retail/views.py index 0ad08257ad5..dc9bd44c93c 100644 --- a/app/retail/views.py +++ b/app/retail/views.py @@ -17,6 +17,7 @@ along with this program. If not, see . ''' +import json import logging import re import time @@ -1489,6 +1490,32 @@ def tokens(request): return TemplateResponse(request, 'tokens_js.txt', context, content_type='text/javascript') +def json_tokens(request): + context = {} + networks = ['mainnet', 'ropsten', 'rinkeby', 'unknown', 'custom'] + # for network in networks: + # key = f"{network}_tokens" + # context[key] = Token.objects.filter(network=network, approved=True) + tokens=Token.objects.filter(approved=True) + token_json = [] + for token in tokens: + _token = { + 'id': token.id, + 'address': token.address, + 'symbol': token.symbol, + 'network': token.network, + 'networkId': token.network_id, + 'chainId': token.chain_id, + 'decimals': token.decimals, + 'priority': token.priority + } + + + token_json.append(_token) + # return TemplateResponse(request, 'tokens_js.txt', context, content_type='text/javascript') + # return JsonResponse(json.loads(json.dumps(list(context), default=str)), safe=False) + return JsonResponse(json.loads(json.dumps(token_json)), safe=False) + @csrf_exempt @ratelimit(key='ip', rate='5/m', method=ratelimit.UNSAFE, block=True) def increase_funding_limit_request(request): diff --git a/scripts/crontab b/scripts/crontab index 91f23f8834d..30e349d5c7a 100644 --- a/scripts/crontab +++ b/scripts/crontab @@ -13,7 +13,7 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/us * * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash sync_listener mainnet >> /var/log/gitcoin/sync_listener.log 2>&1 12 */12 * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash sync_geth rinkeby -200 0 >> /var/log/gitcoin/sync_geth_rinkeby.log 2>&1 */5 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash subminer mainnet --live >> /var/log/gitcoin/subminer_mainnet.log 2>&1 -*/5 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash sync_pending_fulfillments >> /var/log/gitcoin/sync_pending_fulfillments.log 2>&1 +*/3 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash sync_pending_fulfillments >> /var/log/gitcoin/sync_pending_fulfillments.log 2>&1 ## HACKATHON 1,30 * * * * cd gitcoin/coin; bash scripts/run_management_command_if_not_already_running.bash setup_hackathon_event_chat >> /var/log/gitcoin/setup_hackathon_event_chat.log 2>&1