diff --git a/app/assets/v2/js/amounts.js b/app/assets/v2/js/amounts.js index 0acfae5a379..7100db9671d 100644 --- a/app/assets/v2/js/amounts.js +++ b/app/assets/v2/js/amounts.js @@ -86,7 +86,9 @@ var getUSDEstimate = function(amount, denomination, callback) { } var request_url = '/sync/get_amount?amount=' + amount + '&denomination=' + denomination; - jQuery.get(request_url, function(result) { + jQuery.get(request_url, function(results) { + const result = results[0]; + amount_usdt = result['usdt']; eth_amount = parseFloat(result['eth']); conv_rate = amount_usdt / amount; @@ -137,7 +139,9 @@ var getAmountEstimate = function(usd_amount, denomination, callback) { } var request_url = '/sync/get_amount?amount=' + amount + '&denomination=' + denomination; - jQuery.get(request_url, function(result) { + jQuery.get(request_url, function(results) { + const result = results[0]; + amount_usdt = result['usdt']; eth_amount = parseFloat(result['eth']); conv_rate = amount_usdt / amount; diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index c8adf9cf9d1..65f6c3c1cbe 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -513,6 +513,26 @@ Vue.component('grants-cart', { return amount.div(factor).toString(10); }, + async applyAmountToAllGrants(grant) { + const preferredAmount = grant.grant_donation_amount; + const preferredTokenName = grant.grant_donation_currency; + const fallbackAmount = await this.valueToEth(preferredAmount, preferredTokenName); + + this.grantData.forEach((grant, index) => { + const acceptedCurrencies = this.currencies[index]; // tokens accepted by this grant + + if (!acceptedCurrencies.includes(preferredTokenName)) { + // If the selected token is not available, fallback to ETH + this.grantData[index].grant_donation_amount = fallbackAmount; + this.grantData[index].grant_donation_currency = 'ETH'; + } else { + // Otherwise use the user selected option + this.grantData[index].grant_donation_amount = preferredAmount; + this.grantData[index].grant_donation_currency = preferredTokenName; + } + }); + }, + /** * @notice Checkout flow */ @@ -804,19 +824,23 @@ Vue.component('grants-cart', { return y_lower + (((y_upper - y_lower) * (x - x_lower)) / (x_upper - x_lower)); }, - async valueToDai(amount, tokenSymbol) { + valueToDai(amount, tokenSymbol, tokenPrices) { + const tokenIndex = tokenPrices.findIndex(item => item.token === tokenSymbol); + const amountOfOne = tokenPrices[tokenIndex].usdt; // value of 1 tokenSymbol + + return Number(amount) * Number(amountOfOne); // convert based on quantity and return + }, + + async valueToEth(amount, tokenSymbol) { const url = `${window.location.origin}/sync/get_amount?amount=${amount}&denomination=${tokenSymbol}`; const response = await fetch(url); const newAmount = await response.json(); - return newAmount.usdt; + return newAmount.eth; }, - async predictCLRMatch(grant) { - const rawAmount = Number(grant.grant_donation_amount); - let amount = await this.valueToDai(rawAmount, grant.grant_donation_currency); - - const clr_prediction_curve_2d = JSON.parse(grant.grant_clr_prediction_curve); + async predictCLRMatch(grant, amount) { + const clr_prediction_curve_2d = grant.grant_clr_prediction_curve; const clr_prediction_curve = clr_prediction_curve_2d.map(row => row[2]); if (amount > 10000) { @@ -877,6 +901,12 @@ Vue.component('grants-cart', { grantData: { async handler() { CartData.setCart(this.grantData); + const tokenNames = Array.from(new Set(this.grantData.map(grant => grant.grant_donation_currency))); + + const priceUrl = `${window.location.origin}/sync/get_amount?denomination=${tokenNames}`; + const priceResponse = await fetch(priceUrl); + const tokenPrices = (await priceResponse.json()); + for (let i = 0; i < this.grantData.length; i += 1) { const verification_required_to_get_match = false; @@ -887,7 +917,16 @@ Vue.component('grants-cart', { this.grantData[i].grant_donation_clr_match = 0; } else { const grant = this.grantData[i]; - const matchAmount = await this.predictCLRMatch(grant); + // Convert amount to DAI + const rawAmount = Number(grant.grant_donation_amount); + const STABLE_COINS = [ 'DAI', 'SAI', 'USDT', 'TUSD', 'aDAI', 'USDC' ]; + // All stable coins are handled with USDT (see app/app/settings.py for list) + const tokenName = STABLE_COINS.includes(grant.grant_donation_currency) + ? 'USDT' + : grant.grant_donation_currency; + + const amount = this.valueToDai(rawAmount, tokenName, tokenPrices); + const matchAmount = await this.predictCLRMatch(grant, amount); this.grantData[i].grant_donation_clr_match = matchAmount ? matchAmount.toFixed(2) : 0; } @@ -922,6 +961,26 @@ Vue.component('grants-cart', { this.grantData = CartData.loadCart(); // Initialize array of empty comments this.comments = this.grantData.map(grant => undefined); + + // Get list of all grant IDs and unique tokens in the cart + const grantIds = this.grantData.map(grant => grant.grant_id); + + // Fetch updated CLR curves for all grants + const url = `${window.location.origin}/grants/v1/api/clr?pks=${grantIds.join(',')}`; + const response = await fetch(url); + const clrCurves = (await response.json()).grants; + + // Update CLR curves + this.grantData.forEach((grant, index) => { + // Find the clrCurves entry with the same grant ID as this grant + const clrIndex = clrCurves.findIndex(item => { + return Number(item.pk) === Number(grant.grant_id); + }); + + // Replace the CLR prediction curve + this.grantData[index].grant_clr_prediction_curve = clrCurves[clrIndex].clr_prediction_curve; + }); + // Wait until we can load token list let elapsedTime = 0; let delay = 50; // 50 ms debounce diff --git a/app/assets/v2/js/grants/funding.js b/app/assets/v2/js/grants/funding.js index 457c11cc8d4..486a35dcfc1 100644 --- a/app/assets/v2/js/grants/funding.js +++ b/app/assets/v2/js/grants/funding.js @@ -56,41 +56,84 @@ $(document).ready(function() { $('#close-side-cart').click(function() { hideSideCart(); }); + + $('#side-cart-data').on('click', '#apply-to-all', async function() { + // Get preferred cart data + let cartData = CartData.loadCart(); + const network = document.web3network || 'mainnet'; + const selected_grant_index = $(this).data('id'); + const preferredAmount = cartData[selected_grant_index].grant_donation_amount; + const preferredTokenName = cartData[selected_grant_index].grant_donation_currency; + const preferredTokenAddress = tokens(network) + .filter(token => token.name === preferredTokenName) + .map(token => token.addr)[selected_grant_index]; + + // Get fallback amount in ETH (used when token is not available for a grant) + const url = `${window.location.origin}/sync/get_amount?amount=${preferredAmount}&denomination=${preferredTokenName}`; + const response = await fetch(url); + const fallbackAmount = (await response.json()).eth; + + // Update cart values + cartData.forEach((grant, index) => { + const acceptsAllTokens = (grant.grant_token_address === '0x0000000000000000000000000000000000000000'); + const acceptsSelectedToken = grant.grant_token_address === preferredTokenAddress; + + if (acceptsAllTokens || acceptsSelectedToken) { + // Use the user selected option + cartData[index].grant_donation_amount = preferredAmount; + cartData[index].grant_donation_currency = preferredTokenName; + } else { + // If the selected token is not available, fallback to ETH + cartData[index].grant_donation_amount = fallbackAmount; + cartData[index].grant_donation_currency = 'ETH'; + } + }); // end cartData.forEach + + // Update cart + CartData.setCart(cartData); + showSideCart(); + }); }); // HELPERS -function sideCartRowForGrant(grant) { +function sideCartRowForGrant(grant, index) { let cartRow = ` -
-
-
- Grant logo -
-
- ${grant.grant_title} -
-
- -
-
-
-
-
- -
-
- +
+
+ -
-
+ +
- `; +
+
+
+ Apply to all +
+
+ + `; return cartRow; } @@ -143,8 +186,8 @@ function showSideCart() { // Add all elements in side cart let cartData = CartData.loadCart(); - cartData.forEach(grant => { - const cartRowHtml = sideCartRowForGrant(grant); + cartData.forEach((grant, index) => { + const cartRowHtml = sideCartRowForGrant(grant, index); $('#side-cart-data').append(cartRowHtml); diff --git a/app/assets/v2/js/shared.js b/app/assets/v2/js/shared.js index 9cca215e236..410821b5213 100644 --- a/app/assets/v2/js/shared.js +++ b/app/assets/v2/js/shared.js @@ -512,7 +512,8 @@ var retrieveAmount = function() { } // if not, use remote one - $.get(request_url, function(result) { + $.get(request_url, function(results) { + const result = results[0]; // update UI var usd_amount = result['usdt']; diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py index 759563debdf..6de8ae95e3d 100644 --- a/app/dashboard/helpers.py +++ b/app/dashboard/helpers.py @@ -127,25 +127,33 @@ def amount(request): response = {} try: - amount = str(request.GET.get('amount')) + amount = str(request.GET.get('amount', '1')) + if not amount.replace('.','').isnumeric(): return HttpResponseBadRequest('not number') + denomination = request.GET.get('denomination', 'ETH') - if not denomination: - denomination = 'ETH' + tokens = denomination.split(',') + + response = [] + + for token in tokens: + if token in settings.STABLE_COINS: + token = 'USDT' + if token == 'ETH': + amount_in_eth = float(amount) + else: + amount_in_eth = convert_amount(amount, token, 'ETH') + amount_in_usdt = convert_amount(amount_in_eth, 'ETH', 'USDT') + + response.append({ + 'token': token, + 'amount': float(amount), + 'eth': amount_in_eth, + 'usdt': amount_in_usdt + }) - if denomination in settings.STABLE_COINS: - denomination = 'USDT' - if denomination == 'ETH': - amount_in_eth = float(amount) - else: - amount_in_eth = convert_amount(amount, denomination, 'ETH') - amount_in_usdt = convert_amount(amount_in_eth, 'ETH', 'USDT') - response = { - 'eth': amount_in_eth, - 'usdt': amount_in_usdt, - } - return JsonResponse(response) + return JsonResponse(response, safe=False) except ConversionRateNotFoundError as e: logger.debug(e) raise Http404 diff --git a/app/dashboard/tests/test_dashboard_helpers.py b/app/dashboard/tests/test_dashboard_helpers.py index 0704eed8e3f..caaa5409f03 100644 --- a/app/dashboard/tests/test_dashboard_helpers.py +++ b/app/dashboard/tests/test_dashboard_helpers.py @@ -51,7 +51,7 @@ def test_amount(self): """Test the dashboard helper amount method.""" params = {'amount': '5', 'denomination': 'ETH'} request = self.factory.get('/sync/get_amount', params) - assert amount(request).content == b'{"eth": 5.0, "usdt": 10.0}' + assert amount(request).content == b'[{"token": "ETH", "amount": 5, "eth": 5.0, "usdt": 10.0}]' def test_lowball_bounty(self): assert is_lowball_bounty(settings.LOWBALL_BOUNTY_THRESHOLD - 1.0) diff --git a/app/grants/templates/grants/cart-vue.html b/app/grants/templates/grants/cart-vue.html index 7f933e3be5e..01c1d7a3b4b 100644 --- a/app/grants/templates/grants/cart-vue.html +++ b/app/grants/templates/grants/cart-vue.html @@ -142,6 +142,9 @@

+ Apply to all + {% comment %} Add comment {% endcomment %}
@@ -214,6 +217,11 @@

+ +

{% comment %} CLR Match Amount {% endcomment %}
diff --git a/app/grants/urls.py b/app/grants/urls.py index 1e3d915afe7..8f5714c0d3b 100644 --- a/app/grants/urls.py +++ b/app/grants/urls.py @@ -21,8 +21,8 @@ from grants.views import ( flag, grant_activity, grant_categories, grant_details, grant_fund, grant_new, grant_new_whitelabel, grants, - grants_addr_as_json, grants_bulk_add, grants_by_grant_type, grants_cart_view, grants_stats_view, invoice, - leaderboard, new_matching_partner, profile, quickstart, subscription_cancel, + grants_addr_as_json, grants_bulk_add, grants_by_grant_type, grants_cart_view, grants_clr, grants_stats_view, + invoice, leaderboard, new_matching_partner, profile, quickstart, subscription_cancel, ) app_name = 'grants' @@ -57,4 +57,6 @@ path('cart', grants_cart_view, name='cart'), path('', grants_by_grant_type, name='grants_by_category2'), path('/', grants_by_grant_type, name='grants_by_category'), + path('v1/api/clr', grants_clr, name='grants_clr'), + ] diff --git a/app/grants/views.py b/app/grants/views.py index 1f967a4843f..1bad91401eb 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -394,17 +394,19 @@ def grants_by_grant_type(request, grant_type): title = matching_live + str(_('Grants')) has_real_grant_type = grant_type and grant_type != 'activity' grant_type_title_if_any = grant_type.title() if has_real_grant_type else '' + if grant_type_title_if_any == "Media": grant_type_title_if_any = "Community" - if grant_type_title_if_any == "Change": + elif grant_type_title_if_any == "Change": grant_type_title_if_any = "Crypto for Black Lives" - grant_type_gfx_if_any = grant_type if has_real_grant_type else 'total' + if has_real_grant_type: title = f"{matching_live} {grant_type_title_if_any.title()} {category.title()} Grants" if grant_type == 'stats': title = f"Round {clr_round} Stats" cht = [] chart_list = '' + try: what = 'all_grants' pinned = PinnedPost.objects.get(what=what) @@ -1188,3 +1190,39 @@ def grant_activity(request, grant_id=None): return JsonResponse({ 'error': False }) + +@require_GET +def grants_clr(request): + response = { + 'status': 400, + 'message': 'error: Bad Request. Unable to fetch grant clr' + } + + pks = request.GET.get('pks', None) + + if not pks: + response['message'] = 'error: missing parameter pks' + return JsonResponse(response) + + grants = [] + + try: + for grant in Grant.objects.filter(pk__in=pks.split(',')): + grants.append({ + 'pk': grant.pk, + 'title': grant.title, + 'clr_prediction_curve': grant.clr_prediction_curve + }) + except Exception as e: + print(e) + response = { + 'status': 500, + 'message': 'error: something went wrong while fetching grants clr' + } + return JsonResponse(response) + + response = { + 'status': 200, + 'grants': grants + } + return JsonResponse(response)