diff --git a/.travis.yml b/.travis.yml index 7b276093d31..adeef80b03c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,6 +59,7 @@ jobs: provider: pages token: $GITHUB_TOKEN keep_history: true + skip_cleanup: true local_dir: _build/site fqdn: docs.gitcoin.co target_branch: gh-pages diff --git a/app/app/db.py b/app/app/db.py index 64d3b6328d3..e52a7e27ed7 100644 --- a/app/app/db.py +++ b/app/app/db.py @@ -10,11 +10,7 @@ def db_for_read(self, model, **hints): Else go to default DB """ replicas = ['read_replica_1', 'read_replica_2'] - if settings.JOBS_NODE: - return random.choice(replicas) - if settings.CELERY_NODE: - return random.choice(replicas) - return 'default' + return random.choice(replicas) def db_for_write(self, model, **hints): """ diff --git a/app/app/settings.py b/app/app/settings.py index 1dce4559135..f941a0cb6cf 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -40,7 +40,7 @@ DEBUG = env.bool('DEBUG', default=True) QUESTS_LIVE = True ENV = env('ENV', default='local') -DEBUG_ENVS = env.list('DEBUG_ENVS', default=['local', 'stage', 'test']) +DEBUG_ENVS = env.list('DEBUG_ENVS', default=['local', 'stage', 'test', 'travis']) IS_DEBUG_ENV = ENV in DEBUG_ENVS HOSTNAME = env('HOSTNAME', default=socket.gethostname()) BASE_URL = env('BASE_URL', default='http://localhost:8000/') @@ -111,11 +111,11 @@ 'retail', 'ptokens', 'rest_framework', + 'django_filters', 'marketing', 'economy', 'dashboard', 'quests', - 'faucet', 'tdi', 'gas', 'git', @@ -368,7 +368,7 @@ def callback(request): } # Production logging -if ENV not in ['local', 'test', 'staging', 'preview']: +if ENV not in ['local', 'test', 'staging', 'preview', 'travis']: # add AWS monitoring boto3_session = Session( aws_access_key_id=AWS_ACCESS_KEY_ID, @@ -417,7 +417,7 @@ def callback(request): MEDIAFILES_LOCATION = env.str('MEDIAFILES_LOCATION', default='media') STATIC_ROOT = root(STATICFILES_LOCATION) -if ENV in ['prod', 'stage']: +if ENV in ['prod', 'stage', 'test']: DEFAULT_FILE_STORAGE = env('DEFAULT_FILE_STORAGE', default='app.static_storage.MediaFileStorage') STATICFILES_STORAGE = env('STATICFILES_STORAGE', default='app.static_storage.SilentFileStorage') STATIC_HOST = env('STATIC_HOST', default='https://s.gitcoin.co/') @@ -772,8 +772,6 @@ def callback(request): INSTALLED_APPS += env.list('DEBUG_APPS', default=[]) -# Faucet App config -FAUCET_AMOUNT = env.float('FAUCET_AMOUNT', default=.00025) SENDGRID_EVENT_HOOK_URL = env('SENDGRID_EVENT_HOOK_URL', default='sg_event_process') GITHUB_EVENT_HOOK_URL = env('GITHUB_EVENT_HOOK_URL', default='github/payload/') diff --git a/app/app/sitemaps.py b/app/app/sitemaps.py index 5008c72bc46..7b4eb6e08c3 100644 --- a/app/app/sitemaps.py +++ b/app/app/sitemaps.py @@ -16,7 +16,7 @@ class StaticViewSitemap(sitemaps.Sitemap): def items(self): return [ 'dashboard', 'new_funding', 'tip', 'terms', 'privacy', 'cookie', 'prirp', 'apitos', 'about', 'index', - 'help', 'whitepaper', 'whitepaper_access', '_leaderboard', 'faucet', 'mission', 'slack', 'labs', 'results', + 'help', 'whitepaper', 'whitepaper_access', '_leaderboard', 'mission', 'slack', 'labs', 'results', 'activity', 'kudos_main', 'kudos_marketplace', 'grants', 'funder_bounties', 'quests_index', 'newquest', 'products', 'avatar_landing' ] diff --git a/app/app/tests/test_app_urls.py b/app/app/tests/test_app_urls.py index 935155f3298..9948cd31f32 100644 --- a/app/app/tests/test_app_urls.py +++ b/app/app/tests/test_app_urls.py @@ -80,15 +80,6 @@ def test_stats_resolve(self): """Test the stats url and check the resolution.""" self.assertEqual(resolve('/_administration/stats/').view_name, 'stats') - def test_faucet_reverse(self): - """Test the faucet url and check the reverse.""" - self.assertEqual(reverse('faucet'), '/faucet') - - def test_faucet_resolve(self): - """Test the faucet url and check the resolution.""" - self.assertEqual(resolve('/faucet').view_name, 'faucet') - self.assertEqual(resolve('/faucet/').view_name, 'faucet') - def test_explorer_reverse(self): """Test the explorer url and check the reverse.""" self.assertEqual(reverse('explorer'), '/explorer') diff --git a/app/app/travis.env b/app/app/travis.env index 6b42d1c9a6f..675ada47484 100644 --- a/app/app/travis.env +++ b/app/app/travis.env @@ -1,7 +1,7 @@ CACHE_URL=dbcache://my_cache_table COLLECTFAST_CACHE_URL=dbcache://collectfast DATABASE_URL=psql://postgres:postgres@localhost/testdb -ENV=test +ENV=travis REDIS_URL=rediscache://localhost:6379/0?client_class=django_redis.client.DefaultClient CACHEOPS_REDIS=redis://localhost:6379/0 GEOIP_PATH=/opt/GeoIP/ diff --git a/app/app/urls.py b/app/app/urls.py index 89e5611ec91..5d70d4c5f2b 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -36,7 +36,6 @@ import dashboard.views import dataviz.d3_views import dataviz.views -import faucet.views import gitcoinbot.views import healthcheck.views import kudos.views @@ -256,7 +255,6 @@ ), url(r'^api/v0.1/org_perms', dashboard.views.org_perms, name='org_perms'), url(r'^api/v0.1/bulk_invite', dashboard.views.bulk_invite, name='bulk_invite'), - url(r'^api/v0.1/faucet/save/?', faucet.views.save_faucet, name='save_faucet'), url(r'^api/v0.1/', include(dbrouter.urls)), url(r'^api/v0.1/', include(kdrouter.urls)), url(r'^api/v0.1/', include(grant_router.urls)), @@ -493,7 +491,6 @@ re_path(r'^labs/?$', dashboard.views.labs, name='labs'), # gas views - url(r'^gas/faucets/?', dashboard.gas_views.gas_faucet_list, name='gas_faucet_list'), url(r'^gas/faq/?', dashboard.gas_views.gas_faq, name='gas_faq'), url(r'^gas/intro/?', dashboard.gas_views.gas_intro, name='gas_intro'), url(r'^gas/calculator/?', dashboard.gas_views.gas_calculator, name='gas_calculator'), @@ -607,9 +604,6 @@ url(r'^l/(.*)$/?', linkshortener.views.linkredirect, name='redirect'), url(r'^credit/(.*)$/?', credits.views.credits, name='credit'), - # faucet views - re_path(r'^faucet/?', faucet.views.faucet, name='faucet'), - # bounty requests re_path(r'^requests/?', bounty_requests.views.bounty_request, name='bounty_requests'), url( @@ -674,8 +668,6 @@ path('_administration/email/kudos_mkt', retail.emails.kudos_mkt, name='kudos_mkt'), path('_administration/email/new_bounty', retail.emails.new_bounty, name='admin_new_bounty'), path('_administration/email/roundup', retail.emails.roundup, name='roundup'), - path('_administration/email/faucet_rejected', retail.emails.faucet_rejected, name='email_faucet_rejected'), - path('_administration/email/faucet', retail.emails.faucet, name='email_faucet'), path('_administration/email/new_tip', retail.emails.new_tip, name='new_tip'), path('_administration/email/new_match', retail.emails.new_match, name='new_match'), path('_administration/email/quarterly_roundup', retail.emails.quarterly_roundup, name='quarterly_roundup'), @@ -724,11 +716,6 @@ tdi.views.process_accesscode_request, name='process_accesscode_request' ), - re_path( - r'^_administration/process_faucet_request/(.*)$', - faucet.views.process_faucet_request, - name='process_faucet_request' - ), re_path(r'^_administration/bulkemail/', dashboard.views.bulkemail, name='bulkemail'), re_path( r'^_administration/email/start_work_approved$', retail.emails.start_work_approved, name='start_work_approved' diff --git a/app/assets/v2/images/grants/polygon.svg b/app/assets/v2/images/grants/polygon.svg new file mode 100644 index 00000000000..57a330be040 --- /dev/null +++ b/app/assets/v2/images/grants/polygon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/assets/v2/js/cart-ethereum-polygon.js b/app/assets/v2/js/cart-ethereum-polygon.js new file mode 100644 index 00000000000..502c9986f72 --- /dev/null +++ b/app/assets/v2/js/cart-ethereum-polygon.js @@ -0,0 +1,447 @@ +const bulkCheckoutAddressPolygon = '0x3E2849E2A489C8fE47F52847c42aF2E8A82B9973'; + +function objectMap(object, mapFn) { + return Object.keys(object).reduce(function(result, key) { + result[key] = mapFn(object[key]); + return result; + }, {}); +} + +Vue.component('grantsCartEthereumPolygon', { + props: { + currentTokens: { type: Array, required: true }, // Array of available tokens for the selected web3 network + donationInputs: { type: Array, required: true }, // donationInputs computed property from cart.js + grantsByTenant: { type: Array, required: true }, // Array of grants in cart + maxCartItems: { type: Number, required: true }, // max number of items in cart + grantsUnderMinimalContribution: { type: Array, required: true } // Array of grants under min contribution + }, + + data: function() { + return { + network: 'testnet', + polygon: { + showModal: false, // true to show modal to user, false to hide + checkoutStatus: 'not-started', // options are 'not-started', 'pending', and 'complete' + estimatedGasCost: '700000' + }, + + cart: { + tokenList: [], // array of tokens in the cart + unsupportedTokens: [] // tokens in cart which are not supported by Polygon + }, + + user: { + requiredAmounts: null + } + }; + }, + + mounted() { + window.addEventListener('beforeunload', (e) => { + if (this.polygon.checkoutStatus === 'pending') { + e.returnValue = 'Polygon checkout in progress. Are you sure you want to leave?'; + } + }); + + // Update Polygon checkout connection, state, and data frontend needs when wallet connection changes + window.addEventListener('dataWalletReady', async(e) => { + await this.onChangeHandler(this.donationInputs); + }); + }, + + computed: { + /** + * @dev List of tokens supported by Polygon + Gitcoin. To add a token to this list: + * 1. Make sure the token is top 10 used tokens based on Gitcoin's historical data + * 2. Confirm the token exists on Polygon's list of supported tokens: https://mapper.matic.today/ + * 2. Add the token symbol to the appropriate list below + * @dev We hardcode the list from Gitcoin's historical data based on the top ten tokens + * on ethereum chain and also Polygon network used by users to checkout + */ + supportedTokens() { + const mainnetTokens = [ 'DAI', 'ETH', 'USDT', 'USDC', 'PAN', 'BNB', 'UNI', 'CELO', 'MASK', 'MATIC' ]; + const testnetTokens = [ 'MATIC', 'ETH', 'DAI' ]; + + return appCart.$refs.cart.network === 'mainnet' ? mainnetTokens : testnetTokens; + }, + + donationInputsNativeAmount() { + return appCart.$refs.cart.donationInputsNativeAmount; + }, + + requiredAmountsString() { + let string = ''; + + requiredAmounts = this.user.requiredAmounts; + Object.keys(requiredAmounts).forEach(key => { + // Round to 2 digits + if (requiredAmounts[key]) { + const amount = requiredAmounts[key]; + const formattedAmount = amount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + + if (string === '') { + string += `${formattedAmount} ${key}`; + } else { + string += ` + ${formattedAmount} ${key}`; + } + } + }); + return string; + } + }, + + watch: { + // Watch donationInputs prop so we can update cart.js as needed on changes + donationInputs: { + immediate: true, + async handler(donations) { + // Update state and data that frontend needs + await this.onChangeHandler(donations); + } + }, + + // When network changes we need to update Polygon config, fetch new balances, etc. + network: { + immediate: true, + async handler() { + await this.onChangeHandler(this.donationInputs); + } + } + }, + + methods: { + initWeb3() { + let url; + + if (appCart.$refs.cart.network === 'mainnet') { + appCart.$refs.cart.networkId = '137'; + url = 'https://rpc-mainnet.maticvigil.com'; + } else { + appCart.$refs.cart.networkId = '80001'; + appCart.$refs.cart.network = 'testnet'; + url = 'https://rpc-mumbai.maticvigil.com'; + } + + return new Web3(url); + }, + + openBridgeUrl() { + let url = appCart.$refs.cart.network == 'mainnet' + ? 'https://wallet.matic.network/bridge' + : 'https://wallet.matic.today/bridge'; + + window.open(url, '_blank'); + this.polygon.checkoutStatus = 'depositing'; + }, + + handleError(e) { + appCart.$refs.cart.handleError(e); + }, + + getDonationInputs() { + return appCart.$refs.cart.getDonationInputs(); + }, + + getTokenByName(name) { + return appCart.$refs.cart.getTokenByName(name, true); + }, + + async postToDatabase(txHash, contractAddress, userAddress) { + await appCart.$refs.cart.postToDatabase(txHash, contractAddress, userAddress, 'eth_polygon'); + }, + + async finalizeCheckout() { + await appCart.$refs.cart.finalizeCheckout(); + }, + + async getAllowanceData(userAddress, targetContract) { + return await appCart.$refs.cart.getAllowanceData(userAddress, targetContract, true); + }, + + async requestAllowanceApprovalsThenExecuteCallback( + allowanceData, userAddress, targetContract, callback, callbackParams + ) { + return await appCart.$refs.cart.requestAllowanceApprovalsThenExecuteCallback( + allowanceData, userAddress, targetContract, callback, callbackParams + ); + }, + + // We want to run this whenever wallet or cart content changes + async onChangeHandler(donations) { + // Get array of token symbols based on cart data. For example, if the user has two + // DAI grants and one ETH grant in their cart, this returns `[ 'DAI', 'ETH' ]` + this.cart.tokenList = [...new Set(donations.map((donation) => donation.name))]; + + // Get list of tokens in cart not supported by Polygon + this.cart.unsupportedTokens = this.cart.tokenList.filter( + (token) => !this.supportedTokens.includes(token) + ); + + // Update the fee estimate and gas cost based on changes + this.estimatedGasCost = await this.estimateGasCost(); + + // Emit event so cart.js can update state accordingly to display info to user + this.$emit('polygon-data-updated', { + polygonUnsupportedTokens: this.cart.unsupportedTokens, + polygonEstimatedGasCost: this.estimatedGasCost + }); + }, + + // Reset Polygon modal status after a checkout failure + resetPolygonModal() { + this.polygon.checkoutStatus = 'not-started'; + }, + + closePolygonModal() { + this.polygon.showModal = false; + }, + + async setupPolygon() { + indicateMetamaskPopup(); + // Connect to Polygon network with MetaMask + const network = appCart.$refs.cart.network; + let chainId = network === 'mainnet' ? '0x89' : '0x13881'; + let rpcUrl = network === 'mainnet' ? 'https://rpc-mainnet.maticvigil.com' + : 'https://rpc-mumbai.maticvigil.com'; + + try { + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }] + }); + } catch (switchError) { + // This error code indicates that the chain has not been added to MetaMask + if (switchError.code === 4902) { + let networkText = network === 'rinkeby' || network === 'goerli' || + network === 'ropsten' || network === 'kovan' ? 'testnet' : network; + + try { + await ethereum.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId, + rpcUrls: [rpcUrl], + chainName: `Polygon ${networkText.replace(/\b[a-z]/g, (x) => x.toUpperCase())}`, + nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 } + }] + }); + } catch (addError) { + if (addError.code === 4001) { + throw new Error('Please connect MetaMask to Polygon network.'); + } else { + console.error(addError); + } + } + } else if (switchError.code === 4001) { + throw new Error('Please connect MetaMask to Polygon network.'); + } else { + console.error(switchError); + } + } + this.network = getDataChains(ethereum.networkVersion, 'chainId')[0] && getDataChains(ethereum.networkVersion, 'chainId')[0].network; + }, + + // Send a batch transfer based on donation inputs + async checkoutWithPolygon() { + try { + + if (typeof ga !== 'undefined') { + ga('send', 'event', 'Grant Checkout', 'click', 'Person'); + } + + // Throw if invalid Gitcoin contribution percentage + if (Number(this.gitcoinFactorRaw) < 0 || Number(this.gitcoinFactorRaw) > 99) { + throw new Error('Gitcoin contribution amount must be between 0% and 99%'); + } + + // Throw if there's negative values in the cart + this.donationInputs.forEach(donation => { + if (Number(donation.amount) < 0) { + throw new Error('Cannot have negative donation amounts'); + } + }); + + // If user has enough balance within Polygon, cost equals the minimum amount + let { isBalanceSufficient, requiredAmounts } = await this.hasEnoughBalanceInPolygon(); + + if (!isBalanceSufficient) { + this.polygon.showModal = true; + this.polygon.checkoutStatus = 'not-started'; + + this.user.requiredAmounts = objectMap(requiredAmounts, value => { + if (value.isBalanceSufficient == false) { + return value.amount; + } + }); + return; + } + + await this.setupPolygon(); + appCart.$refs.cart.userSwitchedToPolygon = true; + + // Token approvals and balance checks from bulk checkout contract + // (just checks data, does not execute approvals) + const allowanceData = await this.getAllowanceData( + ethereum.selectedAddress, bulkCheckoutAddressPolygon + ); + + // Save off cart data + this.polygon.checkoutStatus = 'pending'; + + if (allowanceData.length === 0) { + // Send transaction and exit function + await this.sendDonationTx(ethereum.selectedAddress); + return; + } + + // Request approvals then send donations --------------------------------------------------- + await this.requestAllowanceApprovalsThenExecuteCallback( + allowanceData, + ethereum.selectedAddress, + bulkCheckoutAddressPolygon, + this.sendDonationTx, + [ethereum.selectedAddress] + ); + + } catch (e) { + this.handleError(e); + } + }, + + async sendDonationTx(userAddress) { + // Get our donation inputs + const bulkTransaction = new web3.eth.Contract(bulkCheckoutAbi, bulkCheckoutAddressPolygon); + const donationInputsFiltered = this.getDonationInputs(); + + // Send transaction + bulkTransaction.methods + .donate(donationInputsFiltered) + .send({ from: userAddress, gas: this.estimatedGasCost, value: this.donationInputsNativeAmount }) + .on('transactionHash', async(txHash) => { + indicateMetamaskPopup(true); + console.log('Donation transaction hash: ', txHash); + _alert('Saving contributions. Please do not leave this page.', 'success', 2000); + await this.postToDatabase([txHash], bulkCheckoutAddressPolygon, userAddress); // Save contributions to database + await this.finalizeCheckout(); // Update UI and redirect + }) + .on('error', (error, receipt) => { + // If the transaction was rejected by the network with a receipt, the second parameter will be the receipt. + this.handleError(error); + }); + }, + + // Estimates the total gas cost of a polygon checkout and sends it to cart.js + async estimateGasCost() { + // The below heuristics are used instead of `estimateGas()` so we can send the donation + // transaction before the approval txs are confirmed, because if the approval txs + // are not confirmed then estimateGas will fail. + let networkId = appCart.$refs.cart.networkId; + + if (networkId !== '80001' && networkId !== '137' && appCart.$refs.cart.chainId !== '1') { + return; + } + + // If we have a cart where all donations are in Dai, we use a linear regression to + // estimate gas costs based on real checkout transaction data, and add a 50% margin + const donationCurrencies = this.donationInputs.map(donation => donation.token); + const daiAddress = this.getTokenByName('DAI')?.addr; + const isAllDai = donationCurrencies.every((addr) => addr === daiAddress); + + if (isAllDai) { + if (donationCurrencies.length === 1) { + // Special case since we overestimate here otherwise + return 70000; + } + // TODO: find a suitable curve using + // https://github.com/mds1/Gitcoin-Checkout-Gas-Analysis + // return 27500 * donationCurrencies.length + 125000; + return 10000 * donationCurrencies.length + 70000; + } + + /** + * Otherwise, based on contract tests, we use the more conservative heuristic below to get + * a gas estimate. The estimates used here are based on testing the cost of a single + * donation (i.e. one item in the cart). Because gas prices go down with batched + * transactions, whereas this assumes they're constant, this gives us a conservative estimate + */ + const gasLimit = this.donationInputs.reduce((accumulator, currentValue) => { + const tokenAddr = currentValue.token?.toLowerCase(); + + if (currentValue.token === MATIC_ADDRESS) { + return accumulator + 70000; // MATIC donation gas estimate + + } else if (tokenAddr === '0x960b236A07cf122663c4303350609A66A7B288C0'.toLowerCase()) { + return accumulator + 170000; // ANT donation gas estimate + } + + return accumulator + 70000; // generic token donation gas estimate + }, 0); + + return gasLimit; + }, + + // Returns true if user has enough balance within Polygon to avoid L1 deposit, false otherwise + async hasEnoughBalanceInPolygon() { + const requiredAmounts = {}; // keys are token symbols, values are required amounts as BigNumber + + // Get total amount needed for eack token by summing over donation inputs + this.donationInputs.forEach((donation) => { + const tokenSymbol = donation.name; + const amount = toBigNumber(donation.amount); + + if (!requiredAmounts[tokenSymbol]) { + // First time seeing this token, set the field and initial value + requiredAmounts[tokenSymbol] = { amount }; + } else { + // Increment total required amount of the token with new found value + requiredAmounts[tokenSymbol].amount = requiredAmounts[tokenSymbol].amount.add(amount); + } + }); + + // Compare amounts needed to balance + const web3 = this.initWeb3(); + const userAddress = ethereum.selectedAddress; + let isBalanceSufficient = true; + + for (let i = 0; i < this.cart.tokenList.length; i += 1) { + const tokenSymbol = this.cart.tokenList[i]; + + requiredAmounts[tokenSymbol].isBalanceSufficient = true; // initialize sufficiency result + const tokenDetails = this.getTokenByName(tokenSymbol); + + if (tokenDetails.name === 'MATIC') { + const userMaticBalance = toBigNumber(await web3.eth.getBalance(userAddress)); + + if (userMaticBalance.lt(requiredAmounts[tokenSymbol].amount)) { + // User MATIC balance is too small compared to selected donation amounts + requiredAmounts[tokenSymbol].isBalanceSufficient = false; + requiredAmounts[tokenSymbol].amount = ( + requiredAmounts[tokenSymbol].amount - userMaticBalance + ) / 10 ** tokenDetails.decimals; + isBalanceSufficient = false; + } + } else { + const tokenContract = new web3.eth.Contract(token_abi, tokenDetails.addr); + // Check user token balance against required amount + const userTokenBalance = toBigNumber(await tokenContract.methods + .balanceOf(userAddress) + .call({ from: userAddress })); + + if (userTokenBalance.lt(requiredAmounts[tokenSymbol].amount)) { + requiredAmounts[tokenSymbol].isBalanceSufficient = false; + requiredAmounts[tokenSymbol].amount = ( + requiredAmounts[tokenSymbol].amount - userTokenBalance + ) / 10 ** tokenDetails.decimals; + isBalanceSufficient = false; + } + } + } + + // Return result and required amounts + return { isBalanceSufficient, requiredAmounts }; + } + } +}); diff --git a/app/assets/v2/js/cart-ethereum-zksync.js b/app/assets/v2/js/cart-ethereum-zksync.js index 5ec2ddf1769..dd90f0794db 100644 --- a/app/assets/v2/js/cart-ethereum-zksync.js +++ b/app/assets/v2/js/cart-ethereum-zksync.js @@ -236,7 +236,8 @@ Vue.component('grantsCartEthereumZksync', { await appCart.$refs.cart.postToDatabase( txHashes, // array of transaction hashes for each contribution this.zksync.contractAddress, // we use the zkSync mainnet contract address to represent zkSync deposits - this.user.address + this.user.address, + 'eth_zksync' ); this.zksync.checkoutStatus = 'complete'; // allows user to freely close tab now await appCart.$refs.cart.finalizeCheckout(); // Update UI and redirect diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index bcaf527739e..f611bf29ace 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -9,13 +9,25 @@ const { Zero: ZERO } = ethers.constants; const { BigNumber } = ethers; let appCart; -document.addEventListener('dataWalletReady', function(e) { +document.addEventListener('dataWalletReady', async function(e) { appCart.$refs['cart'].network = networkName; + appCart.$refs['cart'].networkId = String(Number(web3.eth.currentProvider.chainId)); + if (!appCart.$refs.cart.userSwitchedToPolygon) { + try { + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: networkName == 'mainnet' ? '0x1' : '0x4' }] + }); // mainnet or rinkeby + } catch (e) { + console.log(e); + } + } }, false); // needWalletConnection(); // Constants +const MATIC_ADDRESS = '0x0000000000000000000000000000000000001010'; const ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; const gitcoinAddress = '0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6'; // Gitcoin donation address for mainnet and rinkeby @@ -43,7 +55,9 @@ Vue.component('grants-cart', { { text: 'Wallet address', value: 'address' }, { text: 'Transaction Hash', value: 'txid' } ], + userSwitchedToPolygon: false, chainId: '', + networkId: '', network: 'mainnet', tabSelected: 'ETH', tabIndex: null, @@ -66,6 +80,8 @@ Vue.component('grants-cart', { // Checkout, zkSync zkSyncUnsupportedTokens: [], // Used to inform user which tokens in their cart are not on zkSync zkSyncEstimatedGasCost: undefined, // Used to tell user which checkout method is cheaper + polygonUnsupportedTokens: [], // Used to inform user which tokens in their cart are not on zkSync + polygonEstimatedGasCost: undefined, // Used to tell user which checkout method is cheaper isZkSyncDown: false, // disable zkSync when true isPolkadotExtInstalled: false, chainScripts: { @@ -171,6 +187,19 @@ Vue.component('grants-cart', { } return result; }, + filterByNetworkId: function() { + const vm = this; + let result; + + if (vm.networkId == '') { + result = vm.filterByChainId; + } else { + result = vm.filterByChainId.filter((item) => { + return String(item.networkId) === vm.networkId; + }); + } + return result; + }, // Returns true if user is logged in with GitHub, false otherwise isLoggedIn() { return document.contxt.github_handle; @@ -213,6 +242,8 @@ Vue.component('grants-cart', { // Array of objects containing all donations and associated data donationInputs() { + let isPolygon = this.nativeCurrency == 'MATIC'; + if (!this.grantsByTenant || this.tabSelected !== 'ETH') { return undefined; } @@ -220,13 +251,13 @@ Vue.component('grants-cart', { // Generate array of objects containing donation info from cart let gitcoinFactor = String(100 - (100 * this.gitcoinFactor)); const donations = this.grantsByTenant.map((grant, index) => { - const tokenDetails = this.getTokenByName(grant.grant_donation_currency); - const amount = parseUnits(String(grant.grant_donation_amount || 0), tokenDetails.decimals) + const tokenDetails = this.getTokenByName(grant.grant_donation_currency, isPolygon); + const amount = parseUnits(String(grant.grant_donation_amount || 0), tokenDetails?.decimals) .mul(gitcoinFactor) .div(100); return { - token: tokenDetails.addr, + token: tokenDetails?.addr, amount: amount.toString(), dest: grant.grant_admin_address, name: grant.grant_donation_currency, // token abbreviation, e.g. DAI @@ -238,8 +269,8 @@ Vue.component('grants-cart', { // Append the Gitcoin donations (these already account for gitcoinFactor) Object.keys(this.donationsToGitcoin).forEach((token) => { - const tokenDetails = this.getTokenByName(token); - const amount = parseUnits(String(this.donationsToGitcoin[token]), tokenDetails.decimals); + const tokenDetails = this.getTokenByName(token, isPolygon); + const amount = parseUnits(String(this.donationsToGitcoin[token]), tokenDetails?.decimals); const gitcoinGrantInfo = { // Manually fill this in so we can access it for the POST requests. @@ -265,7 +296,7 @@ Vue.component('grants-cart', { if (amount.gt(ZERO)) { donations.push({ amount: amount.toString(), - token: tokenDetails.addr, + token: tokenDetails?.addr, dest: gitcoinAddress, name: token, // token abbreviation, e.g. DAI grant: gitcoinGrantInfo, // equivalent to grant data from localStorage @@ -278,17 +309,24 @@ Vue.component('grants-cart', { return donations; }, - // Total amount of ETH that needs to be sent along with the transaction - donationInputsEthAmount() { - // Get the total ETH we need to send + // Total amount of native currency that needs to be sent along with the transaction + donationInputsNativeAmount() { + // Get the total native currency we need to send const initialValue = new BN('0'); - const ethAmountBN = this.donationInputs.reduce((accumulator, currentValue) => { - return currentValue.token === ETH_ADDRESS - ? accumulator.add(new BN(currentValue.amount)) // ETH donation + let nativeCurrencyAddress = this.nativeCurrency === 'MATIC' ? MATIC_ADDRESS : ETH_ADDRESS; + const nativeCurrencyAmountBN = this.donationInputs.reduce((accumulator, currentValue) => { + return currentValue.token === nativeCurrencyAddress + ? accumulator.add(new BN(currentValue.amount)) // native currency donation : accumulator.add(new BN('0')); // token donation }, initialValue); - return ethAmountBN.toString(10); + return nativeCurrencyAmountBN.toString(10); + }, + + nativeCurrency() { + let isPolygon = this.networkId === '80001' || this.networkId === '137'; + + return isPolygon ? 'MATIC' : 'ETH'; }, // Estimated gas limit for the transaction @@ -322,7 +360,7 @@ Vue.component('grants-cart', { // donation (i.e. one item in the cart). Because gas prices go down with batched // transactions, whereas this assumes they're constant, this gives us a conservative estimate const gasLimit = this.donationInputs.reduce((accumulator, currentValue) => { - const tokenAddr = currentValue.token.toLowerCase(); + const tokenAddr = currentValue.token?.toLowerCase(); if (currentValue.token === ETH_ADDRESS) { return accumulator + 70000; // ETH donation gas estimate @@ -351,19 +389,35 @@ Vue.component('grants-cart', { checkoutRecommendation() { const estimateL1 = Number(this.donationInputsGasLimitL1); // L1 gas cost estimate const estimateZkSync = Number(this.zkSyncEstimatedGasCost); // zkSync gas cost estimate + const estimatePolygon = Number(this.polygonEstimatedGasCost); // polygon gas cost estimate + + const compareWithL2 = (estimateL2, name) => { + if (estimateL1 < estimateL2) { + const savingsInGas = estimateL2 - estimateL1; + const savingsInPercent = Math.round(savingsInGas / estimateL2 * 100); + + return { name: 'Standard checkout', savingsInGas, savingsInPercent }; + } + + const savingsInGas = estimateL1 - estimateL2; + const percentSavings = savingsInGas / estimateL1 * 100; + const savingsInPercent = percentSavings > 99 ? 99 : Math.round(percentSavings); // max value of 99% + + return { name: name, savingsInGas, savingsInPercent }; + }; - if (estimateL1 < estimateZkSync) { - const savingsInGas = estimateZkSync - estimateL1; - const savingsInPercent = Math.round(savingsInGas / estimateZkSync * 100); + zkSyncComparisonResult = compareWithL2(estimateZkSync, 'zkSync'); + polygonComparisonResult = compareWithL2(estimatePolygon, 'Polygon'); + zkSyncSavings = zkSyncComparisonResult.name === 'zkSync' ? zkSyncComparisonResult.savingsInPercent : 0; + polygonSavings = polygonComparisonResult.name === 'Polygon' ? polygonComparisonResult.savingsInPercent : 0; - return { name: 'Standard checkout', savingsInGas, savingsInPercent }; + if (zkSyncSavings > polygonSavings) { + return zkSyncComparisonResult; + } else if (zkSyncSavings < polygonSavings) { + return polygonComparisonResult; } - - const savingsInGas = estimateL1 - estimateZkSync; - const percentSavings = savingsInGas / estimateL1 * 100; - const savingsInPercent = percentSavings > 99 ? 99 : Math.round(percentSavings); // max value of 99% - - return { name: 'zkSync', savingsInGas, savingsInPercent }; + return zkSyncComparisonResult; // recommendation will be standard checkout + }, isHarmonyExtInstalled() { @@ -433,6 +487,11 @@ Vue.component('grants-cart', { this.zkSyncEstimatedGasCost = data.zkSyncEstimatedGasCost; }, + onPolygonUpdate: function(data) { + this.polygonUnsupportedTokens = data.polygonUnsupportedTokens; + this.polygonEstimatedGasCost = data.polygonEstimatedGasCost; + }, + tabChange: async function(input) { let vm = this; @@ -587,6 +646,7 @@ Vue.component('grants-cart', { confirmClearCart() { if (confirm('Are you sure you want to clear your cart?')) { this.clearCart(); + this.isLoading = false; } }, @@ -737,8 +797,8 @@ Vue.component('grants-cart', { * response to facilitate backward compatibility * @param {String} name Token name, e.g. ETH or DAI */ - getTokenByName(name) { - if (name === 'ETH') { + getTokenByName(name, isPolygon = false) { + if (name === 'ETH' && !isPolygon) { return { addr: ETH_ADDRESS, address: ETH_ADDRESS, @@ -747,8 +807,24 @@ Vue.component('grants-cart', { decimals: 18, priority: 1 }; + } else if (name === 'MATIC' && isPolygon) { + return { + addr: MATIC_ADDRESS, + address: MATIC_ADDRESS, + name: 'MATIC', + symbol: 'MATIC', + decimals: 18, + priority: 1 + }; + } + + if (isPolygon) { + token = this.filterByChainId.filter(token => token.name === name && token.networkId == this.networkId)[0]; + return token; } return this.filterByChainId.filter(token => token.name === name)[0]; + + }, async applyAmountToAllGrants(grant) { @@ -813,7 +889,7 @@ Vue.component('grants-cart', { * @param targetContract Address of the contract to check allowance against. Currently this * should only be the bulkCheckout contract address */ - async getAllowanceData(userAddress, targetContract) { + async getAllowanceData(userAddress, targetContract, isPolygon = false) { // Get list of tokens user is donating with const selectedTokens = Object.keys(this.donationsToGrants); @@ -827,22 +903,22 @@ Vue.component('grants-cart', { return this.donationInputs.reduce((accumulator, currentValue) => { return currentValue.token === tokenDetails.addr ? accumulator.add(new BN(currentValue.amount)) // token donation - : accumulator.add(new BN('0')); // ETH donation + : accumulator.add(new BN('0')); // native currency donation }, initialValue); }; // Loop over each token in the cart and check allowance for (let i = 0; i < selectedTokens.length; i += 1) { const tokenName = selectedTokens[i]; - const tokenDetails = this.getTokenByName(tokenName); + const tokenDetails = this.getTokenByName(tokenName, isPolygon); - // If ETH donation no approval is necessary, just check balance - if (tokenDetails.name === 'ETH') { + // If native currency donation no approval is necessary, just check balance + if (tokenDetails.name === this.nativeCurrency) { const userEthBalance = await web3.eth.getBalance(userAddress); - if (new BN(userEthBalance, 10).lt(new BN(this.donationInputsEthAmount, 10))) { + if (new BN(userEthBalance, 10).lt(new BN(this.donationInputsNativeAmount, 10))) { // User ETH balance is too small compared to selected donation amounts - throw new Error('Insufficient ETH balance to complete checkout'); + throw new Error(`Insufficient ${tokenDetails.name} balance to complete checkout`); } // ETH balance is sufficient, continue to next iteration since no approval check continue; @@ -1018,7 +1094,7 @@ Vue.component('grants-cart', { indicateMetamaskPopup(); bulkTransaction.methods .donate(donationInputsFiltered) - .send({ from: userAddress, gas: this.donationInputsGasLimitL1, value: this.donationInputsEthAmount }) + .send({ from: userAddress, gas: this.donationInputsGasLimitL1, value: this.donationInputsNativeAmount }) .on('transactionHash', async(txHash) => { console.log('Donation transaction hash: ', txHash); indicateMetamaskPopup(true); @@ -1033,23 +1109,20 @@ Vue.component('grants-cart', { }, // POSTs donation data to database. Wrapped in a try/catch, and if it fails, we fallback to the manual ingestion script - async postToDatabase(txHash, contractAddress, userAddress) { + async postToDatabase(txHash, contractAddress, userAddress, checkoutType = 'eth_std') { try { // this.grantsByTenant is the array used for donations, and this.donationInputs is computed from it // We loop through each donation to configure the payload then POST the required data const donations = this.donationInputs; const csrfmiddlewaretoken = document.querySelector('[name=csrfmiddlewaretoken]').value; - // All transactions are the same type, so if any hash begins with `sync-tx:` we know it's a zkSync checkout - const checkout_type = txHash[0].startsWith('sync') ? 'eth_zksync' : 'eth_std'; - // If standard checkout, stretch it so there's one hash for each donation (required for `for` loop below) - const txHashes = checkout_type === 'eth_zksync' ? txHash : new Array(donations.length).fill(txHash[0]); + const txHashes = checkoutType === 'eth_zksync' ? txHash : new Array(donations.length).fill(txHash[0]); // Configure template payload const saveSubscriptionPayload = { // Values that are constant for all donations - checkout_type, + checkoutType, contributor_address: userAddress, csrfmiddlewaretoken, frequency_count: 1, @@ -1093,11 +1166,19 @@ Vue.component('grants-cart', { const tokenName = donation.grant.grant_donation_currency; const tokenDetails = this.getTokenByName(tokenName); - // Gitcoin uses the zero address to represent ETH, but the contract does not. Therefore we - // get the value of denomination and token_address using the below logic instead of + // Gitcoin uses special addresses to represent native chain currencies, but the contract does not. + // Therefore we get the value of denomination and token_address using the below logic instead of // using tokenDetails.addr - const isEth = tokenName === 'ETH'; - const tokenAddress = isEth ? '0x0000000000000000000000000000000000000000' : tokenDetails.addr; + switch (tokenName) { + case 'ETH': + tokenAddress = '0x0000000000000000000000000000000000000000'; + break; + case 'MATIC': + tokenAddress = '0x0000000000000000000000000000000000001010'; + break; + default: + tokenAddress = tokenDetails.addr; + } // Replace undefined comments with empty strings const comment = donation.grant.grant_comments === undefined ? '' : donation.grant.grant_comments; @@ -1165,13 +1246,12 @@ Vue.component('grants-cart', { }, // Alternative to postToDatabase that uses the manual ingestion process - async postToDatabaseManualIngestion(txHash, userAddress) { + async postToDatabaseManualIngestion(txHash, userAddress, checkoutType = 'eth_std') { // Determine if this was a zkSync checkout or standard L1 checkout. For this endpoint, we pass a txHash // to ingest L1 contributions or an address to pass L2 contributions - const checkout_type = txHash[0].startsWith('sync') ? 'eth_zksync' : 'eth_std'; - txHash = checkout_type === 'eth_std' ? txHash[0] : ''; // txHash is always an array of hashes from checkout - userAddress = checkout_type === 'eth_zksync' ? userAddress : ''; + txHash = checkoutType === 'eth_std' || checkoutType === 'eth_polygon' ? txHash[0] : ''; // txHash is always an array of hashes from checkout + userAddress = checkoutType === 'eth_zksync' ? userAddress : ''; // Get user's signature to prevent ingesting arbitrary transactions under your own username, then ingest const { signature, message } = await this.signMessage(userAddress); @@ -1210,7 +1290,7 @@ Vue.component('grants-cart', { const ethersProvider = new ethers.providers.Web3Provider(provider); // ethers provider instance const signer = ethersProvider.getSigner(); // ethers signers const { chainId } = await ethersProvider.getNetwork(); // append chain ID if not mainnet to mitigate replay attack - const message = chainId === 1 ? baseMessage : `${baseMessage}\n\nChain ID: ${chainId}`; + const message = chainId === 1 || chainId === 137 ? baseMessage : `${baseMessage}\n\nChain ID: ${chainId}`; // Get signature from user const isValidSignature = (sig) => ethers.utils.isHexString(sig) && sig.length === 132; // used to verify signature @@ -1237,7 +1317,7 @@ Vue.component('grants-cart', { * success alert */ async finalizeCheckout() { - // Number of items descides the timeout time + // Number of items decides the timeout time const timeout_amount = 1500 + (this.grantsByTenant.length * 500); // Clear cart, redirect back to grants page, and show success alert diff --git a/app/assets/v2/js/data-chains.js b/app/assets/v2/js/data-chains.js index d64e34229c7..576473ffefc 100644 --- a/app/assets/v2/js/data-chains.js +++ b/app/assets/v2/js/data-chains.js @@ -1254,6 +1254,46 @@ var dataChains = ], "infoURL": "https://poa.network" + }, + { + "name": "Polygon(Matic) Testnet Mumbai", + "chainId": 80001, + "shortName": "maticmum", + "chain": "Polygon(Matic)", + "network": "testnet", + "networkId": 80001, + "rpc": [ + "https://rpc-mumbai.maticvigil.com" + ], + "faucets": [ + "https://faucet.matic.network/" + ], + "nativeCurrency": { + "name": "Matic", + "symbol": "MATIC", + "decimals": 18 + }, + "infoURL":"https://matic.network/" + }, + { + "name": "Polygon(Matic) Mainnet", + "chainId": 137, + "shortName": "matic", + "chain": "Polygon(Matic)", + "network": "mainnet", + "networkId": 137, + "rpc": [ + "https://rpc-mainnet.maticvigil.com", + "https://rpc-mainnet.matic.network", + "https://rpc-mainnet.matic.quiknode.pro", + "https://matic-mainnet.chainstacklabs.com" + ], + "nativeCurrency": { + "name": "Matic", + "symbol": "MATIC", + "decimals": 18 + }, + "infoURL":"https://matic.network/" } ]; diff --git a/app/assets/v2/js/pages/faucet_form.js b/app/assets/v2/js/pages/faucet_form.js deleted file mode 100644 index 5a37fe76a51..00000000000 --- a/app/assets/v2/js/pages/faucet_form.js +++ /dev/null @@ -1,162 +0,0 @@ -$('document').ready(function() { - - $('#comment').bind('input propertychange', function() { - this.value = this.value.replace(/ +(?= )/g, ''); - - if (this.value.length > 500) { - this.value = this.value.substring(0, 500); - } - - if (this.value.length) { - $('#charcount').html(501 - this.value.length); - } - - }); - - $('#githubProfile').on('focus', function() { - $('#githubProfileHelpBlock').hide(); - $('#githubProfile').removeClass('is-invalid'); - }); - - $('#emailAddress').on('focus', function() { - $('#emailAddressHelpBlock').hide(); - $('#emailAddress').removeClass('is-invalid'); - }); - - $('#emailAddress').on('change', function() { - var exp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - - if (!exp.test(this.value)) { - $('#emailAddress').addClass('is-invalid'); - $('#emailAddressHelpBlock').html(gettext('We could not validate that input as an email address')).show(); - } - }); - - function csrfSafeMethod(method) { - // these HTTP methods do not require CSRF protection - return ((/^(GET|HEAD|OPTIONS|TRACE)$/).test(method)); - } - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - } - }); - - $('#submitFaucet').on('click', function(e) { - e.preventDefault(); - if (web3Modal && !web3Modal.cachedProvider) { - onConnect().then(() => { - trigger_faucet_form_web3_hooks(provider); - }); - return false; - } - - if ($(this).hasClass('disabled')) { - return; - } - $('#submitFaucet').addClass('disabled'); - - if (e.target.hasAttribute('disabled') || - $('#githubProfile').is(['is-invalid']) || - $('#emailAddress').is(['is-invalid']) || - $('#githubProfile').val() === '' || - $('#emailAddress').val() === '') { - _alert(gettext('Please make sure to fill out all fields.')); - $('#submitFaucet').removeClass('disabled'); - return; - } - - var faucetRequestData = { - 'githubProfile': $('#githubProfile').val().replace('@', ''), - 'ethAddress': $('#ethAddress').val(), - 'emailAddress': $('#emailAddress').val(), - 'comment': $('#comment').val() - }; - - $.post('/api/v0.1/faucet/save', faucetRequestData) - .done(function(d) { - $('#primary_form').hide(); - $('#success_container').show(); - $('#submitFaucet').removeClass('disabled'); - }) - - .fail(function(response) { - var message = gettext('Got an unexpected error'); - - if (response && response.responseJSON && response.responseJSON.message) { - message = response.responseJSON.message; - } - $('#submitFaucet').removeClass('disabled'); - $('#primary_form').hide(); - $('#fail_message').html(message); - $('#fail_container').show(); - }); - }); -}); - -var trigger_faucet_form_web3_hooks = function(data) { - if (!data) { - return; - } - let cb_address = data.selectedAddress; - - if ($('#faucet_form').length) { - $('#ethAddress').val(cb_address); - var faucet_amount = parseInt($('#currentFaucet').val() * (Math.pow(10, 18))); - - if (typeof web3 == 'undefined') { - $('#no_metamask_error').css('display', 'block'); - $('#faucet_form').addClass('hidden'); - return; - } else if (!cb_address) { - $('#no_metamask_error').css('display', 'none'); - $('#unlock_metamask_error').css('display', 'block'); - $('#connect_metamask_error').css('display', 'none'); - $('#over_balance_error').css('display', 'none'); - $('#faucet_form').addClass('hidden'); - return; - } else if (balance >= faucet_amount) { - $('#no_metamask_error').css('display', 'none'); - $('#unlock_metamask_error').css('display', 'none'); - $('#connect_metamask_error').css('display', 'none'); - $('#over_balance_error').css('display', 'block'); - $('#faucet_form').addClass('hidden'); - } else { - $('#over_balance_error').css('display', 'none'); - $('#no_metamask_error').css('display', 'none'); - $('#unlock_metamask_error').css('display', 'none'); - $('#connect_metamask_error').css('display', 'none'); - $('#faucet_form').removeClass('hidden'); - } - } - if ($('#admin_faucet_form').length) { - if (typeof web3 == 'undefined') { - $('#no_metamask_error').css('display', 'block'); - $('#faucet_form').addClass('hidden'); - return; - } - if (!cb_address) { - $('#unlock_metamask_error').css('display', 'block'); - $('#faucet_form').addClass('hidden'); - return; - } - web3.eth.getBalance(cb_address, function(errors, result) { - if (errors) { - return; - } - - if (!result.toNumber()) { - $('#zero_balance_error').css('display', 'block'); - $('#admin_faucet_form').remove(); - } - }); - } -}; - -needWalletConnection(); -window.addEventListener('dataWalletReady', function(e) { - // BN = web3.utils.BN; - trigger_faucet_form_web3_hooks(provider); -}, false); diff --git a/app/assets/v2/js/pages/process_faucet.js b/app/assets/v2/js/pages/process_faucet.js deleted file mode 100644 index 1410ba903e5..00000000000 --- a/app/assets/v2/js/pages/process_faucet.js +++ /dev/null @@ -1,36 +0,0 @@ -$(document).ready(function() { - var post_receipt = function(err, res) { - $('#loadingImg').hide(); - $('.js-submit').removeAttr('disabled'); - if (err != null) { - $('#failureReason').html(err.message); - $('#errResponse').show(); - return; - } - $('#admin_faucet_form').submit(); - }; - - $('#submitFaucet').on('click', function(e) { - e.preventDefault(); - if (web3Modal && !web3Modal.cachedProvider) { - onConnect(); - return false; - } - $('.js-submit').attr('disabled', 'disabled'); - $('#loadingImg').show(); - var destinationAccount = $('#destinationAccount').val(); - var faucetAmount = $('#faucetAmount').val(); - - decimals = 6; - faucetAmount = Math.round(faucetAmount * 10 ** decimals) / 10 ** decimals; - web3.eth.getCoinbase(function(_, fundingAccount) { - console.log(fundingAccount, 'from:to', destinationAccount); - web3.eth.sendTransaction({ - from: fundingAccount, - to: destinationAccount, - value: web3.utils.toWei(String(parseFloat(faucetAmount)), 'ether'), - gasPrice: web3.utils.toHex(document.gas_price * Math.pow(10, 9)) - }, post_receipt); - }); - }); -}); diff --git a/app/assets/v2/scss/base.scss b/app/assets/v2/scss/base.scss index 123e05ce177..76254659aa1 100644 --- a/app/assets/v2/scss/base.scss +++ b/app/assets/v2/scss/base.scss @@ -639,9 +639,6 @@ input.is-invalid { border-color: red; } -.faucet_response { - display: none; -} .tooltip-info { padding: 10px 20px; diff --git a/app/assets/v2/scss/faucet.scss b/app/assets/v2/scss/faucet.scss deleted file mode 100644 index dc2449c3adc..00000000000 --- a/app/assets/v2/scss/faucet.scss +++ /dev/null @@ -1,65 +0,0 @@ -.form-container { - text-align: center; - margin: 50px auto; - width: 50%; -} - -.faucet-form label { - margin-bottom: 5px; -} - -.faucet-input-group { - margin-bottom: 15px; -} - -.faucet-form-img { - margin: 0 auto; - max-width: 475px; -} - -.faucet-form-img img { - width: 100%; -} - -.faucet_response { - display: none; -} - -.hidden-desktop { - display: none; -} - -a#submitFaucet.disabled { - background-color: grey; -} - -@media (max-width: 481px) { - .form-container { - width: 90%; - } - - .faucet-form-img { - max-width: 150px; - margin-bottom: 20px; - } - - .faucet-form-submit { - width: 100%; - } - - .faucet-comment { - height: 100px; - } - - .faucet-input { - border-radius: 0; - } - - .hidden-mobile { - display: none; - } - - .hidden-desktop { - display: block; - } -} diff --git a/app/dashboard/gas_views.py b/app/dashboard/gas_views.py index 756b98ddd51..b03c638d939 100644 --- a/app/dashboard/gas_views.py +++ b/app/dashboard/gas_views.py @@ -103,16 +103,6 @@ def gas_faq(request): return TemplateResponse(request, 'gas_faq.html', context) -def gas_faucet_list(request): - - context = { - 'title': _('Live Ethereum (ETH) Gas Faucet List'), - 'card_desc': _('Ethereum (ETH) Gas Faucet List including the Mainnet, Rinkeby Testnet and Ropsten Testnet'), - 'hide_send_tip': True, - } - return TemplateResponse(request, 'gas_faucet_list.html', context) - - def gas_calculator(request): recommended_gas_price = recommend_min_gas_price_to_confirm_in_time(confirm_time_minutes_target) _cts = conf_time_spread() diff --git a/app/dashboard/management/commands/activity_report.py b/app/dashboard/management/commands/activity_report.py index f4e4795047e..b70e20dd86f 100644 --- a/app/dashboard/management/commands/activity_report.py +++ b/app/dashboard/management/commands/activity_report.py @@ -31,7 +31,6 @@ from dashboard.models import Bounty, Profile from dashboard.utils import all_sendcryptoasset_models from economy.utils import convert_amount -from faucet.models import FaucetRequest from marketing.mails import send_mail DATE_FORMAT = '%Y/%m/%d' @@ -128,29 +127,6 @@ def format_cryptoasset(self, ca): 'payee_location': location, } - def format_faucet_distribution(self, fr): - location, bio = get_bio(fr.github_username) - - return { - 'type': 'faucet_distribution', - 'created_on': fr.created_on, - 'last_activity': fr.modified_on, - 'amount': fr.amount, - 'denomination': 'ETH', - 'amount_eth': fr.amount, - 'amount_usdt': convert_amount(fr.amount, 'ETH', 'USDT'), - 'from_address': '0x4331B095bC38Dc3bCE0A269682b5eBAefa252929', - 'claimee_address': fr.address, - 'repo': 'n/a', - 'from_username': 'admin', - 'fulfiller_github_username': fr.github_username, - 'status': 'sent', - 'comments': f"faucet distribution {fr.pk}", - 'payee_bio': bio, - 'payee_location': location, - } - - def upload_to_s3(self, filename, contents): s3 = boto.connect_s3(settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY) bucket = s3.get_bucket(settings.S3_REPORT_BUCKET) @@ -167,13 +143,6 @@ def handle(self, *args, **options): ).order_by('web3_created', 'id') formatted_bounties = imap(self.format_bounty, bounties) - frs = FaucetRequest.objects.filter( - created_on__gte=options['start_date'], - created_on__lte=options['end_date'], - fulfilled=True, - ).order_by('created_on', 'id') - formatted_frs = imap(self.format_faucet_distribution, frs) - all_scram = [] for _class in all_sendcryptoasset_models(): objs = _class.objects.filter( @@ -186,9 +155,8 @@ def handle(self, *args, **options): all_scram += objs # python3 list hack - formatted_frs = [x for x in formatted_frs] formatted_bounties = [x for x in formatted_bounties] - all_items = formatted_bounties + all_scram + formatted_frs + all_items = formatted_bounties + all_scram csvfile = StringIO() csvwriter = csv.DictWriter(csvfile, fieldnames=[ diff --git a/app/faucet/management/commands/process_faucet_requests.py b/app/dashboard/management/commands/update_trust_bonus.py similarity index 52% rename from app/faucet/management/commands/process_faucet_requests.py rename to app/dashboard/management/commands/update_trust_bonus.py index 662bf8fc946..3baf483c972 100644 --- a/app/faucet/management/commands/process_faucet_requests.py +++ b/app/dashboard/management/commands/update_trust_bonus.py @@ -15,22 +15,32 @@ along with this program. If not, see . ''' + from django.core.management.base import BaseCommand -from faucet.models import FaucetRequest -from marketing.mails import reject_faucet_request +from dashboard.models import Profile +from dashboard.tasks import update_trust_bonus class Command(BaseCommand): - help = 'processes easy to process faucet requests so that admins dont have to.' + help = 'Update every users trust_bonus score' + + def add_arguments(self, parser): + parser.add_argument( + '--call-now', + type=int, + help="disable execution on celery and call now" + ) def handle(self, *args, **options): - reject_comments = "Please tell us what you're planning on using these funds for in the comments section! Thanks." - requests = FaucetRequest.objects.filter(rejected=False, fulfilled=False, comment='') - for faucet_request in requests: - faucet_request.comment_admin = reject_comments - faucet_request.rejected = True - faucet_request.save() - reject_faucet_request(faucet_request) - print(faucet_request.pk) + profiles = Profile.objects.all() + print(profiles.count()) + for profile in profiles.iterator(): + if (options['call_now']): + params = profile.as_dict + params['trust_bonus'] = profile.trust_bonus + print("Saving - %s - %s" % (profile.handle, params['trust_bonus'])) + profile.save() + else: + update_trust_bonus.delay(profile.pk) diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 82517d09cad..010b8f60670 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -1510,16 +1510,16 @@ def value_true(self): def value_in_usdt_at_time(self, at_time): try: if self.token_name in ['USDT', 'USDC']: - return float(self.payout_amount / 10 ** 6) + return float(self.payout_amount) if self.token_name in settings.STABLE_COINS: - return float(self.payout_amount / 10 ** 18) + return float(self.payout_amount) if self.token_name in ['ETH']: return round(float(convert_amount(self.payout_amount, self.token_name, 'USDT', at_time)), 2) try: - return round(float(convert_amount(self.value_true, self.token_name, 'USDT', at_time)), 2) + return round(float(convert_amount(self.payout_amount, self.token_name, 'USDT', at_time)), 2) except ConversionRateNotFoundError: try: - in_eth = round(float(convert_amount(self.value_true, self.token_name, 'ETH', at_time)), 2) + in_eth = round(float(convert_amount(self.payout_amount, self.token_name, 'ETH', at_time)), 2) return round(float(convert_amount(in_eth, 'USDT', 'USDT', at_time)), 2) except ConversionRateNotFoundError: return None @@ -2891,7 +2891,7 @@ class Profile(SuperModel): ) trust_profile = models.BooleanField( default=False, - help_text='If this option is chosen, the user is able to submit a faucet/ens domain registration even if they are new to github', + help_text='If this option is chosen, the user is able to submit a ens domain registration even if they are new to github', ) dont_autofollow_earnings = models.BooleanField( default=False, @@ -4400,6 +4400,7 @@ def to_dict(self): 'card_title': f'@{self.handle} | Gitcoin', 'org_works_with': org_works_with, 'card_desc': desc, + 'trust_bonus': self.trust_bonus, 'avatar_url': self.avatar_url_with_gitcoin_logo, 'count_bounties_completed': total_fulfilled, 'works_with_collected': works_with_collected, diff --git a/app/dashboard/tasks.py b/app/dashboard/tasks.py index f0eb2901467..3cd4ffe69b5 100644 --- a/app/dashboard/tasks.py +++ b/app/dashboard/tasks.py @@ -246,6 +246,21 @@ def profile_dict(self, pk, retry: bool = True) -> None: profile.save() +@app.shared_task(bind=True, max_retries=3) +def update_trust_bonus(self, pk): + """ + :param self: + :param pk: + :return: + """ + profile = Profile.objects.get(pk=pk) + params = profile.as_dict + if profile.trust_bonus != params.get('trust_bonus', None): + params['trust_bonus'] = profile.trust_bonus + print("Saving - %s - %s" % (profile.handle, params['trust_bonus'])) + profile.save() + + @app.shared_task(bind=True) def maybe_market_to_user_slack(self, bounty_pk, event_name, retry: bool = True) -> None: """ diff --git a/app/dashboard/templates/profiles/profile.html b/app/dashboard/templates/profiles/profile.html index ec4e592cf7a..1638b5caa65 100644 --- a/app/dashboard/templates/profiles/profile.html +++ b/app/dashboard/templates/profiles/profile.html @@ -160,7 +160,7 @@ {% bundle merge_js file profile %} - + diff --git a/app/dashboard/templates/shared/gas_nav.html b/app/dashboard/templates/shared/gas_nav.html index 20194195f37..653c380e94e 100644 --- a/app/dashboard/templates/shared/gas_nav.html +++ b/app/dashboard/templates/shared/gas_nav.html @@ -80,7 +80,4 @@
  • FAQ
  • -
  • - Get Gas -
  • diff --git a/app/dashboard/templates/shared/menu.html b/app/dashboard/templates/shared/menu.html index 4266dab737c..fecb7e22cdc 100644 --- a/app/dashboard/templates/shared/menu.html +++ b/app/dashboard/templates/shared/menu.html @@ -139,7 +139,7 @@ - +
    @@ -149,8 +149,8 @@
    - {% trans "Govern" %} - {% trans "Decide the future of the open web" %} + {% trans "DAO" %} + {% trans "Get involved in the decentralized GitcoinDAO community." %}
    diff --git a/app/dashboard/templates/shared/zero_balance_error.html b/app/dashboard/templates/shared/zero_balance_error.html index effd339bf40..7348e0bd6d2 100644 --- a/app/dashboard/templates/shared/zero_balance_error.html +++ b/app/dashboard/templates/shared/zero_balance_error.html @@ -22,13 +22,10 @@

    {% trans "You're almost there!" %}

    1. {% trans "In order to continue, you'll need some Ether." %}
    2. {% trans "'Ether' is a digital asset that can be used to pay for the computational resources needed to run an application or program. In this case, Gitcoin." %}
    3. -
    4. {% trans "The easiest way to get Ether is to request it via the Gitcoin Faucet:" %}

    - {% trans "View Faucet" %} -

    {% trans "Can't get web3 to work? Get help here." %}

    diff --git a/app/dashboard/templatetags/bundle.py b/app/dashboard/templatetags/bundle.py index 1900bdf5cc1..2629e771948 100644 --- a/app/dashboard/templatetags/bundle.py +++ b/app/dashboard/templatetags/bundle.py @@ -38,8 +38,8 @@ """ -# check for production env -isProduction = settings.ENV in ['prod'] +# If in development, we won't push assets to S3 +isDevelopment = settings.ENV not in ['prod', 'test'] # define variables to include in every script (and react to any changes) @@ -76,7 +76,7 @@ def get_file_ts(asset, reportException=False): def clean_block_and_hash(block): # clean up the block -- we want to drop anything that gets added by staticfinder (we could remove this if we purge {% static ... %} from tags) - if isProduction: + if not isDevelopment: # in prod - staticfinder will attach static_url and an additional hash to the resource which doesn't exist on the local disk block = re.sub(re.compile(r'(' + re.escape(settings.STATIC_URL) + r')([^>]*)(\.[0-9a-zA-Z]{12}?)\.(css|scss|js)'), r'\2.\4', block) else: @@ -111,9 +111,9 @@ def check_for_changes(elems, attr, kind, outputFile): if not changed: for el in elems: if el.get(attr): - # check if we're loading an alternative source in production + # check if we're loading an alternative source file = el[attr] - if isProduction and el.get('prod'): + if not isDevelopment and el.get('prod'): file = el['prod'] # discover ts using the absolute path of the given asset @@ -161,7 +161,7 @@ def get_bundled(elems, attr, kind, merge): # check if we're loading an alternative source in production file = el[attr] - if isProduction and el.get('prod'): + if not isDevelopment and el.get('prod'): file = el['prod'] # absolute path of the given asset @@ -193,12 +193,13 @@ def get_bundled(elems, attr, kind, merge): content = sass.compile(string='%s \n %s' % (get_sass_extras(), content)) # minify the content in production - if isProduction and 'js' in kind: - import rjsmin - content = rjsmin.jsmin(content) - elif isProduction and 'css' in kind: - import rcssmin - content = rcssmin.cssmin(content) + if not isDevelopment: + if 'js' in kind: + import rjsmin + content = rjsmin.jsmin(content) + elif 'css' in kind: + import rcssmin + content = rcssmin.cssmin(content) # content is compiled and minified (if in production) return content @@ -222,7 +223,7 @@ def render(block, kind, mode, name='asset', forced=False): block, blockHash = clean_block_and_hash(block) # in production we don't need to generate new content unless we're running this via the bundle command - if not isProduction or forced == True: + if isDevelopment or forced == True: # concat all input in the block content = '' # pull the appropriate tags from the block diff --git a/app/dashboard/utils.py b/app/dashboard/utils.py index ef740839344..24cdd554203 100644 --- a/app/dashboard/utils.py +++ b/app/dashboard/utils.py @@ -1149,7 +1149,7 @@ def list_urls(lis, acc=None): def get_url_first_indexes(): - return ['_administration','about','action','actions','activity','api','avatar','blog','bounties','bounty','btctalk','casestudies','casestudy','chat','community','contributor','contributor_dashboard','credit','dashboard','docs','dynamic','explorer','extension','faucet','fb','feedback','funder','funder_dashboard','funding','gas','ghlogin','github','gitter','grant','grants','hackathon','hackathonlist','hackathons','health','help','home','how','impersonate','inbox','interest','issue','itunes','jobs','jsi18n','kudos','l','labs','landing','lazy_load_kudos','lbcheck','leaderboard','legacy','legal','livestream','login','logout','mailing_list','medium','mission','modal','new','not_a_token','o','onboard','podcast','postcomment','press','presskit','products','profile','quests','reddit','refer','register_hackathon','requestincrease','requestmoney','requests','results','revenue','robotstxt','schwag','send','service','settings','sg_sendgrid_event_processor','sitemapsectionxml','sitemapxml','slack','spec','strbounty_network','submittoken','sync','terms','tip','townsquare','tribe','tribes','twitter','users','verified','vision','wallpaper','wallpapers','web3','whitepaper','wiki','wikiazAZ09azdAZdazd','youtube'] + return ['_administration','about','action','actions','activity','api','avatar','blog','bounties','bounty','btctalk','casestudies','casestudy','chat','community','contributor','contributor_dashboard','credit','dashboard','docs','dynamic','explorer','extension','fb','feedback','funder','funder_dashboard','funding','gas','ghlogin','github','gitter','grant','grants','hackathon','hackathonlist','hackathons','health','help','home','how','impersonate','inbox','interest','issue','itunes','jobs','jsi18n','kudos','l','labs','landing','lazy_load_kudos','lbcheck','leaderboard','legacy','legal','livestream','login','logout','mailing_list','medium','mission','modal','new','not_a_token','o','onboard','podcast','postcomment','press','presskit','products','profile','quests','reddit','refer','register_hackathon','requestincrease','requestmoney','requests','results','revenue','robotstxt','schwag','send','service','settings','sg_sendgrid_event_processor','sitemapsectionxml','sitemapxml','slack','spec','strbounty_network','submittoken','sync','terms','tip','townsquare','tribe','tribes','twitter','users','verified','vision','wallpaper','wallpapers','web3','whitepaper','wiki','wikiazAZ09azdAZdazd','youtube'] # TODO: figure out the recursion issue with the URLs at a later date # or just cache them in the backend dynamically diff --git a/app/faucet/__init__.py b/app/faucet/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/faucet/admin.py b/app/faucet/admin.py deleted file mode 100644 index b9040f05814..00000000000 --- a/app/faucet/admin.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -"""Define admin related functionality for faucet. - -Copyright (C) 2021 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 . - -""" -from __future__ import unicode_literals - -from django.contrib import admin -from django.utils.safestring import mark_safe - -from .models import FaucetRequest - - -class GeneralAdmin(admin.ModelAdmin): - """Define the Faucet specific admin handling.""" - - ordering = ['-id'] - list_display = ['created_on', '__str__'] - - -class FaucetRequestAdmin(admin.ModelAdmin): - """Setup the FaucetRequest admin results display.""" - - raw_id_fields = ['profile'] - ordering = ['-created_on'] - list_display = [ - 'created_on', 'fulfilled', 'rejected', 'link', 'get_profile_handle', - 'get_profile_email', 'email', 'address', 'comment', - ] - search_fields = [ - 'created_on', 'fulfilled', 'rejected', 'profile__handle', - 'email', 'address', 'comment', - ] - - def get_queryset(self, request): - """Override the get_queryset method to include FK lookups.""" - return super(FaucetRequestAdmin, self).get_queryset(request).select_related('profile') - - def get_profile_email(self, obj): - """Get the profile email address.""" - profile = getattr(obj, 'profile', None) - if profile: - return profile.email - return 'N/A' - - get_profile_email.admin_order_field = 'email' - get_profile_email.short_description = 'Profile Email' - - def get_profile_handle(self, obj): - """Get the profile handle.""" - profile = getattr(obj, 'profile', None) - if profile and profile.handle: - return mark_safe( - f'{profile.handle}' - ) - if obj.github_username: - return obj.github_username - return 'N/A' - - get_profile_handle.admin_order_field = 'handle' - get_profile_handle.short_description = 'Profile Handle' - - def link(self, instance): - """Handle faucet request specific links. - - Args: - instance (FaucetRequest): The faucet request to build a link for. - - Returns: - str: The HTML element for the faucet request link. - - """ - if instance.fulfilled or instance.rejected: - return 'n/a' - return mark_safe(f"process me") - link.allow_tags = True - - -admin.site.register(FaucetRequest, FaucetRequestAdmin) diff --git a/app/faucet/management/__init__.py b/app/faucet/management/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/faucet/management/commands/__init__.py b/app/faucet/management/commands/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/faucet/migrations/0001_initial.py b/app/faucet/migrations/0001_initial.py deleted file mode 100644 index 65e3311d223..00000000000 --- a/app/faucet/migrations/0001_initial.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 2.1.4 on 2018-12-26 17:16 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion -import economy.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('dashboard', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='FaucetRequest', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), - ('modified_on', models.DateTimeField(default=economy.models.get_time)), - ('fulfilled', models.BooleanField(default=False)), - ('rejected', models.BooleanField(default=False)), - ('github_username', models.CharField(db_index=True, max_length=255)), - ('github_meta', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)), - ('address', models.CharField(max_length=50)), - ('email', models.CharField(max_length=255)), - ('comment', models.TextField(blank=True, max_length=500)), - ('comment_admin', models.TextField(blank=True, max_length=500)), - ('fulfill_date', models.DateTimeField(blank=True, null=True)), - ('amount', models.FloatField(default=0.00025)), - ('profile', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='faucet_requests', to='dashboard.Profile')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/app/faucet/migrations/__init__.py b/app/faucet/migrations/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/faucet/models.py b/app/faucet/models.py deleted file mode 100644 index d79a078e42c..00000000000 --- a/app/faucet/models.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -"""Define faucet related models. - -Copyright (C) 2021 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 . - -""" -from __future__ import unicode_literals - -from django.conf import settings -from django.contrib.postgres.fields import JSONField -from django.db import models - -from economy.models import SuperModel - - -class FaucetRequestManager(models.Manager): - """Define the Faucet Request query manager.""" - - def user(self, profile): - """Fetch the FaucetRequests matching the provided profile. - - Args: - profile (str): The Github username. - - Returns: - QuerySet: The filtered FaucetRequest results. - - """ - return self.select_related('profile').filter(profile__username=profile) - - -class FaucetRequest(SuperModel): - """Define the Faucet Request model.""" - - fulfilled = models.BooleanField(default=False) - rejected = models.BooleanField(default=False) - github_username = models.CharField(max_length=255, db_index=True) - github_meta = JSONField(default=dict, blank=True) - address = models.CharField(max_length=50) - email = models.CharField(max_length=255) - comment = models.TextField(max_length=500, blank=True) - comment_admin = models.TextField(max_length=500, blank=True) - fulfill_date = models.DateTimeField(null=True, blank=True) - amount = models.FloatField(default=.00025) - profile = models.ForeignKey( - 'dashboard.Profile', - null=True, - on_delete=models.SET_NULL, - related_name='faucet_requests', - ) - - objects = FaucetRequestManager() - - def __str__(self): - """Return the string representation of FaucetRequest.""" - return f"{self.github_username} / {self.created_on}" diff --git a/app/faucet/templates/bulk_email.html b/app/faucet/templates/bulk_email.html deleted file mode 100644 index 353c6bebd81..00000000000 --- a/app/faucet/templates/bulk_email.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "admin/base_site.html" %} -{% comment %} - Copyright (C) 2021 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 bundle %} - -{% block extrastyle %}{{ block.super }} - {% bundle css file admin_dashboard_bootstrap %} - - - {% endbundle %} -{% endblock %} - -{% block coltype %}colMS{% endblock %} - -{% block bodyclass %}{{ block.super }} dashboard{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} - -
    -

    {% trans "Send Bulk Email" %} {{obj.pk}}

    -
    - {% csrf_token %} - Handles (comma seperated, like: "owocki, vs77bb, danlipert, pixelant") -
    - -
    - Subject: -
    - Message -
    - - From: - From Email: - - - -
    - - - -
    - - -{% endblock %} -{% block extrahead %}{{ block.super }} - {% include 'shared/footer_scripts.html' %} -{% endblock %} diff --git a/app/faucet/templates/faucet_form.html b/app/faucet/templates/faucet_form.html deleted file mode 100644 index ef4c304da93..00000000000 --- a/app/faucet/templates/faucet_form.html +++ /dev/null @@ -1,129 +0,0 @@ -{% comment %} - Copyright (C) 2021 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 %} - - - - - - {% include 'shared/head.html' %} - {% include 'shared/cards.html' %} - - -
    -
    - {% include 'shared/top_nav.html' with class='d-md-flex' %} - {% include 'shared/nav.html' %} -
    -
    -
    -
    - {% include 'shared/no_metamask_error.html' %} - {% include 'shared/unlock_metamask.html' %} - {% include 'shared/connect_metamask.html' %} - {% include 'shared/faucet_over_balance_error.html' %} -
    -
    -
    -

    {% trans "Faucet Request" %} {% trans "Alpha" %}

    -

    - {% blocktrans %} - Pursuant to making Ethereum and Gitcoin broadly accessible to everyone Gitcoin - may provide a minimal faucet distribution of ether so you may easily - claim issues and gain more ether. - {% endblocktrans %} -

    -
    -
    - {% include 'svgs/gas.svg' %} -
    -
    - -

    - {% trans "This form requires human review (thanks to automated bots that have tried to abuse the system!). Please allow up to 1-2 business days for review. " %} -
    -
    - All input fields are required. -

    -
    - - - -
    -
    - - - -
    -
    - - -
    -
    - - -
    - -
    -
    -
    -
    {% trans "Request Received" %}
    - {% include 'svgs/success.svg' %} -
    -

    {% trans "Your request for a faucet distribution has been received." %}

    -

    {% trans "The Gitcoin team will review your request shortly." %}

    -
    -
    -
    -
    -

    {% trans "There was an error" %}:

    -
    -

    - {% trans "Something went wrong submitting your request" %} -

    -

    -
    -
    -
    - {% include 'shared/newsletter.html' %} -
    -
    -
    -
    - {% include 'shared/footer.html' %} - {% include 'shared/footer_scripts.html' %} - - - {% csrf_token %} - -
    - - - - - - - diff --git a/app/faucet/templates/process_faucet_request.html b/app/faucet/templates/process_faucet_request.html deleted file mode 100644 index 38fe8c6d899..00000000000 --- a/app/faucet/templates/process_faucet_request.html +++ /dev/null @@ -1,129 +0,0 @@ -{% extends "admin/base_site.html" %} -{% comment %} - Copyright (C) 2021 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 bundle %} - -{% block extrastyle %}{{ block.super }} - {% bundle css file admin_dashboard %} - - {% endbundle %} - - -{% endblock %} -{% block coltype %}colMS{% endblock %} - -{% block bodyclass %}{{ block.super }} dashboard{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} - -
    -

    {% trans "Process Faucet Request" %} {{obj.pk}}

    -
    - {% trans "Github Profile" %}: @{{obj.github_username}} -
    - {% trans "Comments" %} {{obj.comment}} -
    - - - - - -
    - {% csrf_token %} -

    {% blocktrans %}By funding this faucet request, you will transfer {{ faucet_amount }} ETH to - {{ obj.github_username }} with a wallet address of {{ obj.address }}{% endblocktrans %}

    -
    -

    Connect to your wallet

    - - -
    - - - -
    - - {{pk}} - -
    -
    -
    -
    - -
    -

    Reject Faucet Request For {{obj.github_username}}

    - {% for rejection_reason in common_rejection_reasons %} -
    - {% csrf_token %} - -
    - -
    - {% endfor %} - -
    - -{% endblock %} -{% block extrahead %}{{ block.super }} - - {% include 'shared/footer_scripts.html' %} - - - -{% endblock %} diff --git a/app/faucet/templates/shared/faucet_over_balance_error.html b/app/faucet/templates/shared/faucet_over_balance_error.html deleted file mode 100644 index fe892a6fc03..00000000000 --- a/app/faucet/templates/shared/faucet_over_balance_error.html +++ /dev/null @@ -1,40 +0,0 @@ -{% comment %} - Copyright (C) 2021 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 %} - - diff --git a/app/faucet/views.py b/app/faucet/views.py deleted file mode 100644 index e5ab88d7adc..00000000000 --- a/app/faucet/views.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -"""Define faucet views. - -Copyright (C) 2021 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 . - -""" -from django.conf import settings -from django.contrib import messages -from django.contrib.admin.views.decorators import staff_member_required -from django.core.validators import validate_email, validate_slug -from django.http import Http404, JsonResponse -from django.shortcuts import redirect -from django.template.response import TemplateResponse -from django.utils import timezone -from django.utils.html import escape, strip_tags -from django.utils.translation import gettext_lazy as _ -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_GET, require_http_methods, require_POST - -from faucet.models import FaucetRequest -from gas.utils import recommend_min_gas_price_to_confirm_in_time -from marketing.mails import new_faucet_request, processed_faucet_request, reject_faucet_request - - -@require_GET -def faucet(request): - params = { - 'title': 'Faucet', - 'card_title': _('Gitcoin Faucet'), - 'card_desc': _('Request a distribution of ETH so you can use the Ethereum network and Gitcoin.'), - 'faucet_amount': settings.FAUCET_AMOUNT - } - - return TemplateResponse(request, 'faucet_form.html', params) - - -@csrf_exempt -@require_POST -def save_faucet(request): - """Handle saving faucet requests.""" - email_address = request.POST.get('emailAddress') - eth_address = request.POST.get('ethAddress') - is_authenticated = request.user.is_authenticated - profile = request.user.profile if is_authenticated and hasattr(request.user, 'profile') else None - - if not profile: - return JsonResponse({ - 'message': _('You must be authenticated via github to use this feature!') - }, status=401) - - try: - validate_slug(eth_address) - if email_address: - validate_email(email_address) - except Exception as e: - return JsonResponse({'message': str(e)}, status=400) - - comment = escape(strip_tags(request.POST.get('comment', ''))) - if not profile.trust_profile and profile.github_created_on > (timezone.now() - timezone.timedelta(days=7)): - return JsonResponse({ - 'message': _('For SPAM prevention reasons, you may not perform this action right now. Please contact support if you believe this message is in error.') - }, status=403) - if profile.faucet_requests.filter(fulfilled=True): - return JsonResponse({ - 'message': _('The submitted github profile shows a previous faucet distribution.') - }, status=403) - elif profile.faucet_requests.filter(rejected=False): - return JsonResponse({ - 'message': _('The submitted github profile shows a pending faucet distribution.') - }, status=403) - fr = FaucetRequest.objects.create( - fulfilled=False, - github_username=request.user.username, - github_meta={}, - address=eth_address, - email=email_address if email_address else request.user.email, - comment=comment, - profile=profile, - ) - new_faucet_request(fr) - - return JsonResponse({'message': _('Created.')}, status=201) - - -@require_http_methods(['GET', 'POST']) -@staff_member_required -def process_faucet_request(request, pk): - try: - faucet_request = FaucetRequest.objects.get(pk=pk) - except FaucetRequest.DoesNotExist: - raise Http404 - - redir_link = '/_administrationfaucet/faucetrequest/?fulfilled=f&rejected=f' - faucet_amount = float(settings.FAUCET_AMOUNT) * float(recommend_min_gas_price_to_confirm_in_time(5)) - - if faucet_request.fulfilled: - messages.info(request, 'already fulfilled') - return redirect(redir_link) - - if faucet_request.rejected: - messages.info(request, 'already rejected') - return redirect(redir_link) - - reject_comments = request.POST.get('reject_comments') - if reject_comments: - faucet_request.comment_admin = reject_comments - faucet_request.rejected = True - faucet_request.save() - reject_faucet_request(faucet_request) - messages.success(request, 'rejected') - return redirect(redir_link) - - if request.POST.get('destinationAccount'): - faucet_request.fulfilled = True - faucet_request.fulfill_date = timezone.now() - faucet_request.amount = faucet_amount - faucet_request.save() - processed_faucet_request(faucet_request) - messages.success(request, 'sent') - return redirect(redir_link) - - common_rejection_reasons = [ - "Please tell us what you're planning on using these funds for in the comments section! Thanks.", - "This is a faucet for Gitcoin-specific functionality (like posting Bounties or fulfilling Bounties). Please re-submit your request if you need to do something Gitcoin specific.", - "You don't need ETH to start work on a bounty. Please submit another request if you finish your work and need to submit work.", - "", - ] - - context = { - 'obj': faucet_request, - 'faucet_amount': faucet_amount, - 'recommend_gas_price': round(recommend_min_gas_price_to_confirm_in_time(1), 1), - 'common_rejection_reasons': common_rejection_reasons, - } - - return TemplateResponse(request, 'process_faucet_request.html', context) diff --git a/app/grants/admin.py b/app/grants/admin.py index 76d594efde7..a9efa9b8f06 100644 --- a/app/grants/admin.py +++ b/app/grants/admin.py @@ -104,7 +104,7 @@ class GrantAdmin(GeneralAdmin): 'active', 'visible', 'is_clr_eligible', 'migrated_to', 'region', 'grant_type', 'categories', 'description', 'description_rich', 'github_project_url', 'reference_url', 'admin_address', - 'amount_received', 'amount_received_in_round', 'monthly_amount_subscribed', + 'amount_received', 'amount_received_in_round', 'monthly_amount_subscribed', 'defer_clr_to', 'deploy_tx_id', 'cancel_tx_id', 'admin_profile', 'token_symbol', 'token_address', 'contract_address', 'contract_version', 'network', 'required_gas_price', 'logo_svg_asset', 'logo_asset', 'created_on', 'modified_on', 'team_member_list', @@ -116,7 +116,7 @@ class GrantAdmin(GeneralAdmin): 'polkadot_payout_address', 'kusama_payout_address', 'rsk_payout_address', 'algorand_payout_address', 'emails', 'admin_message', 'has_external_funding' ] readonly_fields = [ - 'logo_svg_asset', 'logo_asset', + 'defer_clr_to', 'logo_svg_asset', 'logo_asset', 'team_member_list', 'clr_prediction_curve', 'subscriptions_links', 'contributions_links', 'link', 'migrated_to', 'view_count', 'in_active_clrs', 'stats_history', diff --git a/app/grants/clr.py b/app/grants/clr.py index 5be90eb2fa8..d7968bea8fc 100644 --- a/app/grants/clr.py +++ b/app/grants/clr.py @@ -415,7 +415,12 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn print(f"- starting slim grant calc at {round(time.time(),1)}") grants_clr = run_clr_calcs(grant_contributions_curr, v_threshold, uv_threshold, total_pot) print(f"- saving slim grant calc at {round(time.time(),1)}") + total_count = len(grants_clr.items()) for grant_calc in grants_clr: + counter += 1 + if counter % 10 == 0 or True: + print(f"- {counter}/{total_count} grants iter, pk:{grant_id}, at {round(time.time(),1)}") + pk = grant_calc['id'] grant = clr_round.grants.using('default').get(pk=pk) latest_calc = grant.clr_calculations.using('default').filter(latest=True, grantclr=clr_round).order_by('-pk').first() @@ -424,10 +429,12 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn continue clr_prediction_curve = copy.deepcopy(latest_calc.clr_prediction_curve) clr_prediction_curve[0][1] = grant_calc['clr_amount'] # update only the existing match estimate + print(clr_prediction_curve) clr_round.record_clr_prediction_curve(grant, clr_prediction_curve) grant.save() # if we are only calculating slim CLR calculations, return here and save 97% compute power print(f"- done calculating at {round(time.time(),1)}") + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}") return print(f"- starting grants iter at {round(time.time(),1)}") @@ -460,7 +467,6 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn else: for amount in potential_donations: # calculate clr with each additional donation and save to grants model - # print(f'using {total_pot_close}') predicted_clr, grants_clr, _, _ = calculate_clr_for_donation( grant, amount, grant_contributions_curr, total_pot, v_threshold, uv_threshold ) @@ -480,6 +486,8 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn else: clr_prediction_curve = [[0.0, 0.0, 0.0] for x in range(0, 6)] + print(clr_prediction_curve) + clr_round.record_clr_prediction_curve(_grant, clr_prediction_curve) if from_date > (clr_calc_start_time - timezone.timedelta(hours=1)): @@ -487,4 +495,6 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn debug_output.append({'grant': grant.id, "clr_prediction_curve": (potential_donations, potential_clr), "grants_clr": grants_clr}) + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}") + return debug_output diff --git a/app/grants/clr2.py b/app/grants/clr2.py new file mode 100644 index 00000000000..7a9bf054945 --- /dev/null +++ b/app/grants/clr2.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +"""Define the Grants application configuration. + +Copyright (C) 2021 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 . + +""" +import copy + +from django.db import connection +from django.utils import timezone + +import numpy as np +from dashboard.models import Profile +from grants.models import Grant, GrantCollection +from townsquare.models import SquelchProfile + +CLR_PERCENTAGE_DISTRIBUTED = 0 + + +def get_summed_contribs_query(grants, created_after, created_before, multiplier, network): + # only consider contribs from current grant set + grantIds = '' + for i in range(len(grants)): + grantIds += "'" + str(grants[i].id) + "'" + (', ' if i+1 != len(grants) else '') + + summedContribs = f''' + -- drop the table if it exists + DROP TABLE IF EXISTS tempUserTotals; + + -- group by ... sum the contributions $ value for each user + SELECT + grants.use_grant_id as grant_id, + grants_contribution.profile_for_clr_id as user_id, + SUM((grants_contribution.normalized_data ->> 'amount_per_period_usdt')::FLOAT * {float(multiplier)}), + MAX(dashboard_profile.as_dict ->> 'trust_bonus')::FLOAT as trust_bonus + INTO TEMP TABLE tempUserTotals + FROM grants_contribution + INNER JOIN dashboard_profile ON (grants_contribution.profile_for_clr_id = dashboard_profile.id) + INNER JOIN grants_subscription ON (grants_contribution.subscription_id = grants_subscription.id) + RIGHT JOIN ( + SELECT + grants_grant.id as grant_id, + ( + CASE + WHEN grants_grant.defer_clr_to_id IS NOT NULL THEN grants_grant.defer_clr_to_id + ELSE grants_grant.id + END + ) as use_grant_id + FROM grants_grant + ) grants ON ((grants_contribution.normalized_data ->> 'id')::FLOAT = grants.grant_id) + WHERE ( + grants_contribution.normalized_data ->> 'id' IN ({grantIds}) AND + grants_contribution.created_on >= '{created_after}' AND + grants_contribution.created_on <= '{created_before}' AND + grants_contribution.match = True AND + grants_subscription.network = '{network}' AND + grants_contribution.success = True AND + (grants_contribution.normalized_data ->> 'amount_per_period_usdt')::FLOAT >= 0 AND + NOT ( + grants_contribution.profile_for_clr_id IN ( + SELECT squelched.profile_id FROM townsquare_squelchprofile squelched WHERE squelched.active = True + ) AND grants_contribution.profile_for_clr_id IS NOT NULL + ) + ) + GROUP BY grants.use_grant_id, grants_contribution.profile_for_clr_id; + + -- index before joining in clr_query + CREATE INDEX ON tempUserTotals (grant_id, user_id); + + SELECT * FROM tempUserTotals; + ''' + + return summedContribs + + +def add_prediction_contrib_query(grant_id, amount): + predictionContrib = f''' + -- delete any previous prediction values from the contributions table + DELETE FROM tempUserTotals WHERE user_id = 999999999; + + -- insert the prediction value into contributions (grant_id, user_id, amount, trust_bonus) + {"INSERT INTO tempUserTotals VALUES(" + str(grant_id) + ", 999999999, " + str(amount) + ", 1);" if amount != 0 else ""} + ''' + return predictionContrib + + +def get_calc_query(v_threshold): + pairwise = ''' + -- produce the pairwise sums + SELECT + c1.user_id, + c2.user_id as user_id_2, + SUM((c1.sum * c2.sum) ^ 0.5) pairwise + FROM tempUserTotals c1 + INNER JOIN tempUserTotals c2 ON (c1.grant_id = c2.grant_id AND c2.user_id > c1.user_id) + GROUP BY c1.user_id, c2.user_id + ''' + + clrAmount = f''' + -- calculate the CLR amount for each grant + SELECT + c1.grant_id, + -- add trust scores and threshold here + SUM((c1.sum * c2.sum) ^ 0.5 / (pw.pairwise / ({v_threshold} * GREATEST(c2.trust_bonus, c1.trust_bonus)) + 1)) final_clr + FROM tempUserTotals c1 + INNER JOIN tempUserTotals c2 ON (c1.grant_id = c2.grant_id AND c2.user_id > c1.user_id) + INNER JOIN ({pairwise}) pw ON (c1.user_id = pw.user_id AND c2.user_id = pw.user_id_2) + GROUP BY c1.grant_id + ORDER BY c1.grant_id; + ''' + + # # CTE of pairwise, clrAmount and clrResult (this will return the clr_amount, number_contribtions and contribution_amount for each grant) + # clrResult = f''' + # -- group by ... sum the contributions $ value for each grant and place the clr + # SELECT + # c1.grant_id, + # -- use MAX/MIN because we know we will only match a single CLR here + # MAX(clr.final_clr) clr_amount, + # SUM(1) number_contributions, + # SUM(c1.sum) contribution_amount + # FROM tempUserTotals c1 + # INNER JOIN ({clrAmount}) clr ON (c1.grant_id = clr.grant_id) + # GROUP BY c1.grant_id; + # ''' + + return clrAmount + + +def fetch_grants(clr_round, network='mainnet'): + grant_filters = clr_round.grant_filters + collection_filters = clr_round.collection_filters + + grants = clr_round.grants.filter(network=network, hidden=False, active=True, is_clr_eligible=True, link_to_new_grant=None) + + if grant_filters: + # Grant Filters (grant_type, category) + grants = grants.filter(**grant_filters) + elif collection_filters: + # Collection Filters + grant_ids = GrantCollection.objects.filter(**collection_filters).values_list('grants', flat=True) + grants = grants.filter(pk__in=grant_ids) + + return grants + + +def calculate_clr_for_donation(grant_id, amount, cursor, total_pot, v_threshold): + # collect results + bigtot = 0 + totals = {} + + # find grant in contributions list and add donation + clr_query = add_prediction_contrib_query(grant_id, amount) + get_calc_query(v_threshold) + cursor.execute(clr_query) + for _row in cursor.fetchall(): + bigtot += _row[1] if _row[1] else 0 + totals[_row[0]] = {'clr_amount': _row[1]} + + global CLR_PERCENTAGE_DISTRIBUTED + + # check if saturation is reached + if bigtot >= total_pot: # saturation reached + # print(f'saturation reached. Total Pot: ${total_pot} | Total Allocated ${bigtot}. Normalizing') + CLR_PERCENTAGE_DISTRIBUTED = 100 + for pk, grant_calc in totals.items(): + grant_calc['clr_amount'] = ((grant_calc['clr_amount'] / bigtot) * total_pot) + else: + CLR_PERCENTAGE_DISTRIBUTED = (bigtot / total_pot) * 100 + if bigtot == 0: + bigtot = 1 + percentage_increase = np.log(total_pot / bigtot) / 100 + for pk, grant_calc in totals.items(): + grant_calc['clr_amount'] = grant_calc['clr_amount'] * (1 + percentage_increase) if grant_calc['clr_amount'] else 0 + + # find grant we added the contribution to and get the new clr amount + if grant_id and totals.get(grant_id): + clr = totals[grant_id] + return ( + clr, + clr['clr_amount'] + ) + + # print(f'info: no contributions found for grant {grant_id}') + return (totals, 0.0) + + +def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainnet', only_grant_pk=None, what='full'): + import time + + # setup + clr_calc_start_time = timezone.now() + debug_output = [] + + # one-time data call + total_pot = float(clr_round.total_pot) + v_threshold = float(clr_round.verified_threshold) + + print(f"- starting fetch_grants at {round(time.time(),1)}") + grants = fetch_grants(clr_round, network) + + # override the grants list to the one selected + if only_grant_pk: + grants = grants.filter(pk=only_grant_pk) + + print(f"- starting get data and sum at {round(time.time(),1)}") + # collect contributions for clr_round into temp table + initial_query = get_summed_contribs_query(grants, clr_round.start_date, clr_round.end_date, clr_round.contribution_multiplier, network) + # open cursor and execute the groupBy sum for the round + with connection.cursor() as cursor: + counter = 0 + curr_agg = {} + # execute to populate shared state for the round + cursor.execute(initial_query) # (we could potential do better here by sharing this temp table between rounds) + for _row in cursor.fetchall(): + if not curr_agg.get(_row[0]): + curr_agg[_row[0]] = {} + curr_agg[_row[0]][_row[1]] = _row[2] + + if len(curr_agg) == 0: + print(f'- done - No Contributions for CLR {clr_round.round_num}. Exiting') + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}\n") + return + + print(f"- starting current grant calc (free of predictions) at {round(time.time(),1)}") + curr_grants_clr, _ = calculate_clr_for_donation( + None, 0, cursor, total_pot, v_threshold + ) + + if what == 'slim': + # if we are only calculating slim CLR calculations, return here and save 97% compute power + print(f"- saving slim grant calc at {round(time.time(),1)}") + total_count = len(curr_grants_clr.items()) + for grant_id, grant_calc in curr_grants_clr.items(): + counter += 1 + if counter % 10 == 0 or True: + print(f"- {counter}/{total_count} grants iter, pk:{grant_id}, at {round(time.time(),1)}") + + # update latest calcs with current distribution + grant = clr_round.grants.using('default').get(pk=grant_id) + latest_calc = grant.clr_calculations.using('default').filter(latest=True, grantclr=clr_round).order_by('-pk').first() + if not latest_calc: + print(f"- - could not find latest clr calc for {grant_id} ") + continue + clr_prediction_curve = copy.deepcopy(latest_calc.clr_prediction_curve) + clr_prediction_curve[0][1] = grant_calc['clr_amount'] if grant_calc else 0.0 # update only the existing match estimate + print(clr_prediction_curve) + clr_round.record_clr_prediction_curve(grant, clr_prediction_curve) + grant.save() + # if we are only calculating slim CLR calculations, return here and save 97% compute power + print(f"- done calculating at {round(time.time(),1)}") + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}\n") + return + + # calculate clr given additional donations + total_count = grants.count() + + print(f"- starting grants iter at {round(time.time(),1)}") + # calculate each grant as a distinct input + for grant in grants: + # five potential additional donations plus the base case of 0 + potential_donations = [0, 1, 10, 100, 1000, 10000] + + # debug the run... + counter += 1 + if counter % 10 == 0 or True: + print(f"- {counter}/{total_count} grants iter, pk:{grant.id}, at {round(time.time(),1)}") + + # if no contributions have been made for this grant then the pairwise will fail and no match will be discovered + if not curr_agg.get(grant.id): + grants_clr = None + potential_clr = [0.0 for x in range(0, 6)] + else: + potential_clr = [] + for amount in potential_donations: + # no need to run the calculation multiple times for amount=0 (will always be the same result) + if amount == 0: + # use the current distribution calc + grants_clr = curr_grants_clr.get(grant.id) + predicted_clr = grants_clr['clr_amount'] if grants_clr else 0.0 + else: + # this is used when you want to count final distribution and ignore the prediction + if what == 'final': + # ignore the other ones + grants_clr = None + predicted_clr = 0.0 + else: + # calculate clr with each additional donation + grants_clr, predicted_clr = calculate_clr_for_donation( + grant.id, amount, cursor, total_pot, v_threshold + ) + # record each point of the predicition + potential_clr.append(predicted_clr) + + # save the result of the prediction + if save_to_db: + _grant = Grant.objects.get(pk=grant.id) + clr_prediction_curve = list(zip(potential_donations, potential_clr)) + base = clr_prediction_curve[0][1] + _grant.last_clr_calc_date = timezone.now() + _grant.next_clr_calc_date = timezone.now() + timezone.timedelta(minutes=60) + + # check that we have enough data to set the curve + can_estimate = True if base or clr_prediction_curve[1][1] or clr_prediction_curve[2][1] or clr_prediction_curve[3][1] else False + if can_estimate: + clr_prediction_curve = [[ele[0], ele[1], ele[1] - base] for ele in clr_prediction_curve ] + else: + clr_prediction_curve = [[0.0, 0.0, 0.0] for x in range(0, 6)] + print(clr_prediction_curve) + + # save the new predicition curve via the model + clr_round.record_clr_prediction_curve(_grant, clr_prediction_curve) + _grant.save() + + debug_output.append({'grant': grant.id, "clr_prediction_curve": (potential_donations, potential_clr), "grants_clr": grants_clr}) + + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}\n") + + return debug_output diff --git a/app/grants/clr3.py b/app/grants/clr3.py new file mode 100644 index 00000000000..27d821beebe --- /dev/null +++ b/app/grants/clr3.py @@ -0,0 +1,412 @@ +# -*- coding: utf-8 -*- +"""Define the Grants application configuration. + +Copyright (C) 2021 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 . + +""" +import copy + +from django.db import connection +from django.utils import timezone + +import numpy as np +from grants.models import Contribution, Grant, GrantCollection +from townsquare.models import SquelchProfile + +CLR_PERCENTAGE_DISTRIBUTED = 0 + + +def fetch_grants(clr_round, network='mainnet'): + grant_filters = clr_round.grant_filters + collection_filters = clr_round.collection_filters + + grants = clr_round.grants.filter(network=network, hidden=False, active=True, is_clr_eligible=True, link_to_new_grant=None) + + if grant_filters: + # Grant Filters (grant_type, category) + grants = grants.filter(**grant_filters) + elif collection_filters: + # Collection Filters + grant_ids = GrantCollection.objects.filter(**collection_filters).values_list('grants', flat=True) + grants = grants.filter(pk__in=grant_ids) + + return grants + + +def get_summed_contribs_query(grants, created_after, created_before, multiplier, network): + # only consider contribs from current grant set + grantIds = '' + for i in range(len(grants)): + grantIds += "'" + str(grants[i].id) + "'" + (', ' if i+1 != len(grants) else '') + + summedContribs = f''' + -- group by ... sum the contributions $ value for each user + SELECT + grants.use_grant_id as grant_id, + grants_contribution.profile_for_clr_id as user_id, + SUM((grants_contribution.normalized_data ->> 'amount_per_period_usdt')::FLOAT * {float(multiplier)}), + MAX(dashboard_profile.as_dict ->> 'trust_bonus')::FLOAT as trust_bonus + FROM grants_contribution + INNER JOIN dashboard_profile ON (grants_contribution.profile_for_clr_id = dashboard_profile.id) + INNER JOIN grants_subscription ON (grants_contribution.subscription_id = grants_subscription.id) + RIGHT JOIN ( + SELECT + grants_grant.id as grant_id, + ( + CASE + WHEN grants_grant.defer_clr_to_id IS NOT NULL THEN grants_grant.defer_clr_to_id + ELSE grants_grant.id + END + ) as use_grant_id + FROM grants_grant + ) grants ON ((grants_contribution.normalized_data ->> 'id')::FLOAT = grants.grant_id) + WHERE ( + grants_contribution.normalized_data ->> 'id' IN ({grantIds}) AND + grants_contribution.created_on >= '{created_after}' AND + grants_contribution.created_on <= '{created_before}' AND + grants_contribution.match = True AND + grants_subscription.network = '{network}' AND + grants_contribution.success = True AND + (grants_contribution.normalized_data ->> 'amount_per_period_usdt')::FLOAT >= 0 AND + NOT ( + grants_contribution.profile_for_clr_id IN ( + SELECT squelched.profile_id FROM townsquare_squelchprofile squelched WHERE squelched.active = True + ) AND grants_contribution.profile_for_clr_id IS NOT NULL + ) + ) + GROUP BY grants.use_grant_id, grants_contribution.profile_for_clr_id; + ''' + + return summedContribs + + +def get_totals_by_pair(contrib_dict): + ''' + gets pair totals between current round, current round + + args: + aggregated contributions by pair nested dict + { + grant_id (str): { + user_id (str): aggregated_amount (float) + } + } + + returns: + pair totals between current round + {user_id (str): {user_id (str): pair_total (float)}} + + ''' + tot_overlap = {} + + # start pairwise match + for _, contribz in contrib_dict.items(): + for k1, v1 in contribz.items(): + if k1 not in tot_overlap: + tot_overlap[k1] = {} + + # pairwise matches to current round + for k2, v2 in contribz.items(): + if k2 not in tot_overlap[k1]: + tot_overlap[k1][k2] = 0 + tot_overlap[k1][k2] += (v1 * v2) ** 0.5 + + return tot_overlap + + +def calculate_clr(aggregated_contributions, pair_totals, trust_dict, v_threshold, total_pot): + ''' + calculates the clr amount at the given threshold and total pot + args: + aggregated contributions by pair nested dict + { + grant_id (str): { + user_id (str): aggregated_amount (float) + } + } + pair_totals + {user_id (str): {user_id (str): pair_total (float)}} + trust_dict + {user_id (str): trust_score (float)} + v_threshold + float + total_pot + float + + returns: + total clr award by grant, analytics, normalized by the normalization factor + [{'id': proj, 'number_contributions': _num, 'contribution_amount': _sum, 'clr_amount': tot}] + saturation point + boolean + ''' + + bigtot = 0 + totals = {} + + for proj, contribz in aggregated_contributions.items(): + tot = 0 + _num = 0 + _sum = 0 + + # start pairwise matches + for k1, v1 in contribz.items(): + _num += 1 + _sum += v1 + + # pairwise matches to current round + for k2, v2 in contribz.items(): + if int(k2) > int(k1): + tot += ((v1 * v2) ** 0.5) / (pair_totals[k1][k2] / (v_threshold * max(trust_dict[k2], trust_dict[k1])) + 1) + + if type(tot) == complex: + tot = float(tot.real) + + bigtot += tot + totals[proj] = {'number_contributions': _num, 'contribution_amount': _sum, 'clr_amount': tot} + + global CLR_PERCENTAGE_DISTRIBUTED + + if bigtot >= total_pot: # saturation reached + # print(f'saturation reached. Total Pot: ${total_pot} | Total Allocated ${bigtot}. Normalizing') + CLR_PERCENTAGE_DISTRIBUTED = 100 + for key, t in totals.items(): + t['clr_amount'] = ((t['clr_amount'] / bigtot) * total_pot) + else: + CLR_PERCENTAGE_DISTRIBUTED = (bigtot / total_pot) * 100 + if bigtot == 0: + bigtot = 1 + percentage_increase = np.log(total_pot / bigtot) / 100 + for key, t in totals.items(): + t['clr_amount'] = t['clr_amount'] * (1 + percentage_increase) + return totals + + +def run_clr_calcs(curr_agg, trust_dict, v_threshold, total_pot): + ''' + clubbed function that runs all calculation functions + + args: + curr_agg : + { + grantId (int): { + profileId (str): amount (float) + } + } + trust_dict : + { + profileId (str): trust_bonus (float) + } + v_threshold : float + total_pot : float + + returns: + grants clr award amounts (dict) + ''' + + # get pair totals + ptots = get_totals_by_pair(curr_agg) + + # clr calcluation + totals = calculate_clr(curr_agg, ptots, trust_dict, v_threshold, total_pot) + + return totals + + +def calculate_clr_for_donation(curr_agg, trust_dict, grant_id, amount, v_threshold, total_pot): + ''' + clubbed function that runs all calculation functions and returns the result for a single grant_id + + args: + curr_agg : + { + grantId (int): { + profileId (str): amount (float) + } + } + trust_dict : + { + profileId (str): trust_bonus (float) + } + grant_id ; int + amount ; int + v_threshold : float + total_pot : float + + returns: + (grant clr award amounts (dict), clr_amount (float), number_contributions (int), contribution_amount (float)) + ''' + + # make sure contributions exist + if curr_agg.get(grant_id) or not grant_id: + # find grant in contributions list and add donation + if amount: + trust_dict['999999999999'] = 1 + curr_agg[grant_id]['999999999999'] = amount + + grants_clr = run_clr_calcs(curr_agg, trust_dict, v_threshold, total_pot) + + # find grant we added the contribution to and get the new clr amount + if grants_clr.get(grant_id): + grant_clr = grants_clr.get(grant_id) + return ( + grant_clr, + grant_clr['clr_amount'], + grant_clr['number_contributions'], + grant_clr['contribution_amount'] + ) + else: + grants_clr = None + + # print(f'info: no contributions found for grant {grant}') + return (grants_clr, 0.0, 0, 0.0) + + +def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainnet', only_grant_pk=None, what='full'): + import time + + # setup + clr_calc_start_time = timezone.now() + debug_output = [] + + # one-time data call + total_pot = float(clr_round.total_pot) + v_threshold = float(clr_round.verified_threshold) + + print(f"- starting fetch_grants at {round(time.time(),1)}") + # grants, contributions = fetch_data(clr_round, network) + grants = fetch_grants(clr_round, network) + + if only_grant_pk: + grants = grants.filter(pk=only_grant_pk) + + print(f"- starting get data and sum at {round(time.time(),1)}") + # collect contributions for clr_round into temp table + initial_query = get_summed_contribs_query(grants, clr_round.start_date, clr_round.end_date, clr_round.contribution_multiplier, network) + # open cursor and execute the groupBy sum for the round + with connection.cursor() as cursor: + counter = 0 + curr_agg = {} + trust_dict = {} + # execute to populate shared state for the round + cursor.execute(initial_query) # (we could potential do better here by sharing this temp table between rounds) + for _row in cursor.fetchall(): + if not curr_agg.get(_row[0]): + curr_agg[_row[0]] = {} + trust_dict[_row[1]] = _row[3] + curr_agg[_row[0]][_row[1]] = _row[2] + + if len(curr_agg) == 0: + print(f'- done - no Contributions for CLR {clr_round.round_num}. Exiting') + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}\n") + return + + print(f"- starting current grant calc (free of predictions) at {round(time.time(),1)}") + curr_grants_clr = run_clr_calcs(curr_agg, trust_dict, v_threshold, total_pot) + + if what == 'slim': + # if we are only calculating slim CLR calculations, return here and save 97% compute power + print(f"- saving slim grant calc at {round(time.time(),1)}") + total_count = len(curr_grants_clr.items()) + for pk, grant_calc in curr_grants_clr.items(): + counter += 1 + if counter % 10 == 0 or True: + print(f"- {counter}/{total_count} grants iter, pk:{pk}, at {round(time.time(),1)}") + + # update latest calcs with current distribution + grant_calc = curr_grants_clr[pk] + grant = clr_round.grants.using('default').get(pk=pk) + latest_calc = grant.clr_calculations.using('default').filter(latest=True, grantclr=clr_round).order_by('-pk').first() + if not latest_calc: + print(f"- - could not find latest clr calc for {grant.pk} ") + continue + clr_prediction_curve = copy.deepcopy(latest_calc.clr_prediction_curve) + clr_prediction_curve[0][1] = grant_calc['clr_amount'] # update only the existing match estimate + print(clr_prediction_curve) + clr_round.record_clr_prediction_curve(grant, clr_prediction_curve) + grant.save() + # if we are only calculating slim CLR calculations, return here and save 97% compute power + print(f"- done calculating at {round(time.time(),1)}") + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}\n") + return + + print(f"- starting grants iter at {round(time.time(),1)}") + # calculate clr given additional donations + total_count = grants.count() + for grant in grants: + # five potential additional donations plus the base case of 0 + potential_donations = [0, 1, 10, 100, 1000, 10000] + + # debug the run... + counter += 1 + if counter % 10 == 0 or True: + print(f"- {counter}/{total_count} grants iter, pk:{grant.id}, at {round(time.time(),1)}") + + # if no contributions have been made for this grant then the pairwise will fail and no match will be discovered + if not curr_agg.get(grant.id): + grants_clr = None + potential_clr = [0.0 for x in range(0, 6)] + else: + potential_clr = [] + for amount in potential_donations: + # no need to run the calculation multiple times for amount=0 (will always be the same result) + if amount == 0: + # use the current distribution calc + grants_clr = curr_grants_clr.get(grant.id) + predicted_clr = grants_clr['clr_amount'] if grants_clr else 0.0 + else: + # this is used when you want to count final distribution and ignore the prediction + if what == 'final': + # ignore the other ones + grants_clr = None + predicted_clr = 0.0 + else: + # calculate clr with each additional donation + grants_clr, predicted_clr, _, _ = calculate_clr_for_donation( + curr_agg, trust_dict, grant.id, amount, v_threshold, total_pot + ) + + # reset potential_donations + if amount and curr_agg.get(grant.id): + del curr_agg[grant.id]['999999999999'] + + # record each point of the predicition + potential_clr.append(predicted_clr) + + # save the result of the prediction + if save_to_db: + _grant = Grant.objects.get(pk=grant.id) + clr_prediction_curve = list(zip(potential_donations, potential_clr)) + base = clr_prediction_curve[0][1] + _grant.last_clr_calc_date = timezone.now() + _grant.next_clr_calc_date = timezone.now() + timezone.timedelta(minutes=60) + + # check that we have enough data to set the curve + can_estimate = True if base or clr_prediction_curve[1][1] or clr_prediction_curve[2][1] or clr_prediction_curve[3][1] else False + if can_estimate: + clr_prediction_curve = [[ele[0], ele[1], ele[1] - base] for ele in clr_prediction_curve ] + else: + clr_prediction_curve = [[0.0, 0.0, 0.0] for x in range(0, 6)] + print(clr_prediction_curve) + + # save the new predicition curve via the model + clr_round.record_clr_prediction_curve(_grant, clr_prediction_curve) + _grant.save() + + debug_output.append({'grant': grant.id, "clr_prediction_curve": (potential_donations, potential_clr), "grants_clr": grants_clr}) + + print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}\n") + + return debug_output diff --git a/app/grants/management/commands/analytics_clr2.py b/app/grants/management/commands/analytics_clr2.py new file mode 100644 index 00000000000..fa0f6f1bada --- /dev/null +++ b/app/grants/management/commands/analytics_clr2.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +"""Define the Grant subminer management command. + +Copyright (C) 2021 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 . + +""" + +from django.core.management.base import BaseCommand +from django.db import connection +from django.utils import timezone + +from dashboard.models import Profile +from grants.clr2 import calculate_clr_for_donation, fetch_grants, get_summed_contribs_query +from grants.models import GrantCLR + + +def analytics_clr(from_date=None, clr_round=None, network='mainnet'): + # setup + # clr_calc_start_time = timezone.now() + debug_output = [['grant_id', 'grant_title', 'number_contributions', 'contribution_amount', 'clr_amount']] + + # one-time data call + total_pot = float(clr_round.total_pot) + v_threshold = float(clr_round.verified_threshold) + + print(total_pot) + + grants = fetch_grants(clr_round, network) + # collect contributions for clr_round into temp table + initial_query = get_summed_contribs_query(grants, clr_round.start_date, clr_round.end_date, clr_round.contribution_multiplier, network) + # open cursor and execute the groupBy sum for the round + with connection.cursor() as cursor: + # execute to populate shared state for the round + cursor.execute(initial_query) + # calculate clr analytics output + for grant in grants: + _, clr_amount = calculate_clr_for_donation( + grant.id, + 0, + cursor, + total_pot, + v_threshold + ) + debug_output.append([grant.id, grant.title, grant.positive_round_contributor_count, float(grant.amount_received_in_round), clr_amount]) + + return debug_output + + + +class Command(BaseCommand): + + help = 'calculate clr base analytic results for all clr rounds or for a specific clr round' + + def add_arguments(self, parser): + parser.add_argument('network', type=str, default='mainnet', choices=['rinkeby', 'mainnet']) + parser.add_argument('clr_pk', type=str, default="all") + + + def handle(self, *args, **options): + + network = options['network'] + clr_pk = options['clr_pk'] + + if clr_pk == "all": + active_clr_rounds = GrantCLR.objects.filter(is_active=True) + else: + active_clr_rounds = GrantCLR.objects.filter(pk=clr_pk) + + if active_clr_rounds: + for clr_round in active_clr_rounds: + print(f"calculating CLR results for round: {clr_round.round_num} {clr_round.sub_round_slug}") + analytics = analytics_clr( + from_date=timezone.now(), + clr_round=clr_round, + network=network + ) + print(analytics) + print(f"finished CLR results for round: {clr_round.round_num} {clr_round.sub_round_slug}") + + else: + print("No active CLRs found") diff --git a/app/grants/management/commands/analytics_clr3.py b/app/grants/management/commands/analytics_clr3.py new file mode 100644 index 00000000000..6bc715e633e --- /dev/null +++ b/app/grants/management/commands/analytics_clr3.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +"""Define the Grant subminer management command. + +Copyright (C) 2021 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 . + +""" + +from django.core.management.base import BaseCommand +from django.db import connection +from django.utils import timezone + +from grants.clr3 import calculate_clr_for_donation, fetch_grants, get_summed_contribs_query +from grants.models import GrantCLR + + +def analytics_clr(from_date=None, clr_round=None, network='mainnet'): + # setup + # clr_calc_start_time = timezone.now() + debug_output = [['grant_id', 'grant_title', 'number_contributions', 'contribution_amount', 'clr_amount']] + + # one-time data call + total_pot = float(clr_round.total_pot) + v_threshold = float(clr_round.verified_threshold) + + print(total_pot) + + grants = fetch_grants(clr_round, network) + + # collect contributions for clr_round into temp table + initial_query = get_summed_contribs_query(grants, clr_round.start_date, clr_round.end_date, clr_round.contribution_multiplier, network) + + # open cursor and execute the groupBy sum for the round + with connection.cursor() as cursor: + curr_agg = {} + trust_dict = {} + # execute to populate shared state for the round + cursor.execute(initial_query) # (we could potential do better here by sharing this temp table between rounds) + for _row in cursor.fetchall(): + if not curr_agg.get(_row[0]): + curr_agg[_row[0]] = {} + + trust_dict[_row[1]] = _row[3] + curr_agg[_row[0]][_row[1]] = _row[2] + + # calculate clr analytics output + for grant in grants: + _, clr_amount, num_contribs, contrib_amount = calculate_clr_for_donation( + curr_agg, trust_dict, grant.id, 0, v_threshold, total_pot + ) + # debug_output.append([grant.id, grant.title, num_contribs, contrib_amount, clr_amount]) + debug_output.append([grant.id, grant.title, grant.positive_round_contributor_count, float(grant.amount_received_in_round), clr_amount]) + + return debug_output + + + +class Command(BaseCommand): + + help = 'calculate clr base analytic results for all clr rounds or for a specific clr round' + + def add_arguments(self, parser): + parser.add_argument('network', type=str, default='mainnet', choices=['rinkeby', 'mainnet']) + parser.add_argument('clr_pk', type=str, default="all") + + + def handle(self, *args, **options): + + network = options['network'] + clr_pk = options['clr_pk'] + + if clr_pk == "all": + active_clr_rounds = GrantCLR.objects.filter(is_active=True) + else: + active_clr_rounds = GrantCLR.objects.filter(pk=clr_pk) + + if active_clr_rounds: + for clr_round in active_clr_rounds: + print(f"calculating CLR results for round: {clr_round.round_num} {clr_round.sub_round_slug}") + analytics = analytics_clr( + from_date=timezone.now(), + clr_round=clr_round, + network=network + ) + print(analytics) + print(f"finished CLR results for round: {clr_round.round_num} {clr_round.sub_round_slug}") + + else: + print("No active CLRs found") diff --git a/app/grants/management/commands/estimate_clr.py b/app/grants/management/commands/estimate_clr.py index a425e1b2633..33cec050cae 100644 --- a/app/grants/management/commands/estimate_clr.py +++ b/app/grants/management/commands/estimate_clr.py @@ -45,7 +45,7 @@ def handle(self, *args, **options): clr_pk = options['clr_pk'] what = options['what'] sync = options['sync'] - print (network, clr_pk, what, sync) + print ('clr', network, clr_pk, what, sync) if clr_pk and clr_pk.isdigit(): active_clr_rounds = GrantCLR.objects.filter(pk=clr_pk) diff --git a/app/grants/management/commands/estimate_clr2.py b/app/grants/management/commands/estimate_clr2.py new file mode 100644 index 00000000000..ac395a9471a --- /dev/null +++ b/app/grants/management/commands/estimate_clr2.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""Define the Grant subminer management command. + +Copyright (C) 2021 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 . + +""" + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from grants.clr2 import predict_clr +from grants.models import GrantCLR +from grants.tasks import process_predict_clr + + +class Command(BaseCommand): + + help = 'calculate CLR estimates for all grants' + + def add_arguments(self, parser): + parser.add_argument('network', type=str, default='mainnet', choices=['rinkeby', 'mainnet']) + parser.add_argument('clr_pk', type=str, default="all") + parser.add_argument('what', type=str, default="full") + parser.add_argument('sync', type=str, default="false") + # slim = just run 0 contribution match upcate calcs + # full, run [0, 1, 10, 100, calcs across all grants] + + + def handle(self, *args, **options): + + network = options['network'] + clr_pk = options['clr_pk'] + what = options['what'] + sync = options['sync'] + print ('clr2', network, clr_pk, what, sync) + + if clr_pk and clr_pk.isdigit(): + active_clr_rounds = GrantCLR.objects.filter(pk=clr_pk) + else: + active_clr_rounds = GrantCLR.objects.filter(is_active=True) + + if active_clr_rounds: + for clr_round in active_clr_rounds: + if sync == 'true': + # run it sync -> useful for payout / debugging + predict_clr( + save_to_db=True, + from_date=timezone.now(), + clr_round=clr_round, + network=network, + what=what, + ) + else: + # runs it as celery task. + process_predict_clr( + save_to_db=True, + from_date=timezone.now(), + clr_round=clr_round, + network=network, + what=what, + ) + else: + print("No active CLRs found") diff --git a/app/grants/management/commands/estimate_clr3.py b/app/grants/management/commands/estimate_clr3.py new file mode 100644 index 00000000000..8941d53ae30 --- /dev/null +++ b/app/grants/management/commands/estimate_clr3.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""Define the Grant subminer management command. + +Copyright (C) 2021 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 . + +""" + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from grants.clr3 import predict_clr +from grants.models import GrantCLR +from grants.tasks import process_predict_clr + + +class Command(BaseCommand): + + help = 'calculate CLR estimates for all grants' + + def add_arguments(self, parser): + parser.add_argument('network', type=str, default='mainnet', choices=['rinkeby', 'mainnet']) + parser.add_argument('clr_pk', type=str, default="all") + parser.add_argument('what', type=str, default="full") + parser.add_argument('sync', type=str, default="false") + # slim = just run 0 contribution match upcate calcs + # full, run [0, 1, 10, 100, calcs across all grants] + + + def handle(self, *args, **options): + + network = options['network'] + clr_pk = options['clr_pk'] + what = options['what'] + sync = options['sync'] + print ('clr3', network, clr_pk, what, sync) + + if clr_pk and clr_pk.isdigit(): + active_clr_rounds = GrantCLR.objects.filter(pk=clr_pk) + else: + active_clr_rounds = GrantCLR.objects.filter(is_active=True) + + if active_clr_rounds: + for clr_round in active_clr_rounds: + if sync == 'true': + # run it sync -> useful for payout / debugging + predict_clr( + save_to_db=True, + from_date=timezone.now(), + clr_round=clr_round, + network=network, + what=what, + ) + else: + # runs it as celery task. + process_predict_clr( + save_to_db=True, + from_date=timezone.now(), + clr_round=clr_round, + network=network, + what=what, + ) + else: + print("No active CLRs found") diff --git a/app/grants/templates/grants/cart-vue.html b/app/grants/templates/grants/cart-vue.html index 361777bde0d..4ef27e7bfb0 100644 --- a/app/grants/templates/grants/cart-vue.html +++ b/app/grants/templates/grants/cart-vue.html @@ -70,6 +70,12 @@

    Grants Cart

    {% comment %} Main container {% endcomment %}
    + +
    Fetching cart data...
    @@ -152,6 +158,7 @@

    Grants Cart

    {% endbundle %} + diff --git a/app/grants/templates/grants/cart/eth.html b/app/grants/templates/grants/cart/eth.html index d2bb4f44ab4..6483b653b7d 100644 --- a/app/grants/templates/grants/cart/eth.html +++ b/app/grants/templates/grants/cart/eth.html @@ -148,8 +148,13 @@

    Summary

    -
    - zkSync checkout not supported due to [[ zkSyncUnsupportedTokens.join(', ') ]] +
    +
    + zkSync checkout not supported due to [[ zkSyncUnsupportedTokens.join(', ') ]] +
    +
    + Polygon checkout not supported due to [[ polygonUnsupportedTokens.join(', ') ]] +
    💡 Save ~[[ checkoutRecommendation.savingsInPercent ]]% @@ -161,6 +166,112 @@

    Summary

    + + + +
    + + + + + + + + + + + +
    +
    + + Summary
    diff --git a/app/retail/templates/mission.html b/app/retail/templates/mission.html index 3944c13680c..3f85b6f4fcf 100644 --- a/app/retail/templates/mission.html +++ b/app/retail/templates/mission.html @@ -199,6 +199,12 @@

    EthCC 2021 - It's all Coordination


    +

    EthCC 2021 - DoingGud

    + +
    +
    + +
    diff --git a/app/retail/templates/shared/head.html b/app/retail/templates/shared/head.html index bd17cfc4a8f..895df9b1582 100644 --- a/app/retail/templates/shared/head.html +++ b/app/retail/templates/shared/head.html @@ -42,7 +42,6 @@ - diff --git a/docs/API.md b/docs/API.md index ced24c28095..abef74911a2 100644 --- a/docs/API.md +++ b/docs/API.md @@ -3,7 +3,7 @@ ## Grants API * Get a full list of grants at `https://gitcoin.co/grants/grants.json` -* Get a list of contributors to each Gitcoin Grants Round at `https://gitcoin.co/grants/v1/api/export_addresses/roundX.json` where X is the round number, (1-7 supported as of Oct 2020) +* Get a list of contributors to each Gitcoin Grants Round at `https://gitcoin.co/grants/v1/api/export_addresses/roundX.json` where X is the round number, (1-10 supported as of Aug 2021) * Get a list of contributors to a Gitcoin Grant at `https://gitcoin.co/grants/v1/api/export_addresses/grantX.json` where X is the ID of the grant. You must be authenticated as a team member of the grant to access the data. * Get a list of contributors to a Gitcoin Grant at a specififc round `https://gitcoin.co/grants/v1/api/export_addresses/grantX_roundY.json` where X is the ID of the grant and Y is the round number. You must be authenticated as a team member of the grant to access the data. * We've got an `https://gitcoin.co/grants/v1/api/export_addresses/all.json` endpoint available for those who'd like to just get all addresses that've ever funded a Gitcoin Grant. diff --git a/docs/ENVIRONMENT_VARIABLES.md b/docs/ENVIRONMENT_VARIABLES.md index 75be7185a0e..48ef59adec0 100644 --- a/docs/ENVIRONMENT_VARIABLES.md +++ b/docs/ENVIRONMENT_VARIABLES.md @@ -143,7 +143,6 @@ If you opt to modify the port or listener interface, you must update your `launc | Variable | Description | Type | Default | | --- | --- | --- | --- | -| FAUCET_AMOUNT | The amount of ETH to be distributed for approved faucet requests. | `float` | .0005 | | GITTER_TOKEN | The Gitter chat API token. | `str` | False | diff --git a/package.json b/package.json index cc781e69252..0a8717f60ed 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,6 @@ "vue": "2.6.11", "vue-plyr": "^7.0.0", "vue-select": "3.10.8", - "vue-tel-input": "^5.5.0" + "vue-tel-input": "^4.4.1" } } diff --git a/pydocmd.yml b/pydocmd.yml index 9a499b186b6..192d27ed1e8 100644 --- a/pydocmd.yml +++ b/pydocmd.yml @@ -75,12 +75,6 @@ generate: - economy.models++ - economy/utils.md: - economy.utils++ - - faucet/admin.md: - - faucet.admin++ - - faucet/models.md: - - faucet.models++ - - faucet/views.md: - - faucet.views++ - gas/admin.md: - gas.admin++ - gas/models.md: @@ -254,10 +248,6 @@ pages: - Admin: economy/admin.md - Models: economy/models.md - Utilities: economy/utils.md - - Faucet: - - Admin: faucet/admin.md - - Models: faucet/models.md - - Views: faucet/views.md - Gas: - Admin: gas/admin.md - Models: gas/models.md diff --git a/requirements/base.txt b/requirements/base.txt index c572555ecda..d92969111af 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -55,7 +55,7 @@ svgutils==0.3.0 watchdog==0.9.0 Werkzeug[watchdog]==0.15.5 imageio -boto3==1.7.81 +boto3==1.18.22 django-storages==1.11.1 eth-account==0.2.2 django-classy-tags==0.8.0 @@ -99,7 +99,7 @@ django-queryset-csv django-proxy==1.2.1 ed25519 tweepy -attrs==19.3.0 +attrs==20.3.0 oogway==0.7.0 icalendar==4.0.7 duniterpy==0.61.0 @@ -116,3 +116,4 @@ libsass==0.20.1 graphqlclient==0.2.4 docutils==0.17.1 unidecode==1.2.0 +drf-flex-fields==0.9.1 \ No newline at end of file diff --git a/scripts/crontab b/scripts/crontab index d2559e800da..1ec1dee1f79 100644 --- a/scripts/crontab +++ b/scripts/crontab @@ -127,7 +127,6 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/us 10 * * * * cd gitcoin/coin; bash scripts/run_management_command.bash post_to_craigslist 1 >> /var/log/gitcoin/post_to_craigslist.log 2>&1 10 1 * * * cd gitcoin/coin; bash scripts/run_management_command.bash output_gas_viz >> /var/log/gitcoin/output_gas_viz.log 2>&1 25,45 * * * * cd gitcoin/coin; bash scripts/run_management_command.bash check_gh_ratelimit >> /var/log/gitcoin/gh_ratelimit.log 2>&1 -55 * * * * cd gitcoin/coin; bash scripts/run_management_command.bash process_faucet_requests >> /var/log/gitcoin/process_faucet_requests.log 2>&1 1 */4 * * * cd gitcoin/coin; bash scripts/run_management_command.bash grant_vitalik_shuffle > /var/log/gitcoin/grant_vitalik_shuffle.log 2>&1 1 */3 * * * cd gitcoin/coin; bash scripts/run_management_command.bash grant_collections_shuffle > /var/log/gitcoin/grant_collections_shuffle.log 2>&1 diff --git a/yarn.lock b/yarn.lock index 6d6c76b1580..9db567d5145 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1540,6 +1540,11 @@ autoprefixer@^9.0.0: postcss "^7.0.32" postcss-value-parser "^4.1.0" +awesome-phonenumber@^2.39.0: + version "2.57.0" + resolved "https://registry.yarnpkg.com/awesome-phonenumber/-/awesome-phonenumber-2.57.0.tgz#0d2986247553b0e3d97e459bb365a82c13a4f259" + integrity sha512-RWrCCQpnmkYeL3AGFdlUOpWkpkTauZm7FE9kgDz6xJG6PNUiiIm+rKI95wnre0TSV01PHvgFFwQZhDixPCM9ZA== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2261,11 +2266,16 @@ core-js-compat@^3.14.0, core-js-compat@^3.15.0: browserslist "^4.16.6" semver "7.0.0" -core-js@^3.10.1, core-js@^3.14.0, core-js@^3.8.1: +core-js@^3.10.1, core-js@^3.8.1: version "3.15.2" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.2.tgz#740660d2ff55ef34ce664d7e2455119c5bdd3d61" integrity sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q== +core-js@^3.6.4: + version "3.16.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.3.tgz#1f2d43c51a9ed014cc6c83440af14697ae4b75f2" + integrity sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -4517,11 +4527,6 @@ libp2p-crypto@~0.16.1: tweetnacl "^1.0.0" ursa-optional "~0.10.0" -libphonenumber-js@^1.9.6: - version "1.9.21" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.21.tgz#ae7ce30d68a4b697024287e108f391f3938e8b1e" - integrity sha512-hMt+UTcXjRj7ETZBCxdPSw362Lq16Drf4R9vYgw19WoJLLpi/gMwseW62tevBKfF3pfXfr5rNnbc/hSl7XgWnQ== - linkify-it@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" @@ -7471,21 +7476,21 @@ vue-select@3.10.8: resolved "https://registry.yarnpkg.com/vue-select/-/vue-select-3.10.8.tgz#5d0839cba228a9a5efa1a88e5436840eb3fd0c7a" integrity sha512-PnjtZWCTiSr04bs8ctPIiU41qnBK+oh/SOe6Pb4gElMMxofDFwUxiUe++mz0+84aTy4zrleGxtvVVyWbiPYBiw== -vue-tel-input@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/vue-tel-input/-/vue-tel-input-5.5.0.tgz#c62cf17f00ae2b2c320619de42b0038ed22f2738" - integrity sha512-NtO/4KI70jS/6j8zcguO0ZeRLeBc5dRGeTqkGcFwyIOuL7C8xajI9XAMqgY+der6jcuLIAk3Xm0o56ijm5jFxg== +vue-tel-input@^4.4.1: + version "4.4.2" + resolved "https://registry.yarnpkg.com/vue-tel-input/-/vue-tel-input-4.4.2.tgz#a673f706f1e452b862908b9d7a0496df0e114a35" + integrity sha512-fZm9/gE2MFAe+EoF/R66t9oc0U9oI87TiPTxv6pHG8MGhrc62a+uvPqyE/clNhdEJJ0slGzVqkelBuw23HHWtw== dependencies: - core-js "^3.14.0" - libphonenumber-js "^1.9.6" - vue "^2.6.14" + "@babel/runtime" "^7.8.4" + awesome-phonenumber "^2.39.0" + core-js "^3.6.4" vue@2.6.11: version "2.6.11" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== -vue@^2.6.12, vue@^2.6.14: +vue@^2.6.12: version "2.6.14" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==