Skip to content

Commit

Permalink
Let users ingest missing contributions (#8274)
Browse files Browse the repository at this point in the history
* Setup new page for ingesting missing contributions

* Setup form and add input validation

* Add ingestion endpoint + UI status alerts

* Remove unnecessary authentication check in ingest_contributions_view

* Move missing-contributions JS file into grants folder
  • Loading branch information
mds1 authored Jan 26, 2021
1 parent 09e7a8c commit 891b0f8
Show file tree
Hide file tree
Showing 5 changed files with 550 additions and 1 deletion.
5 changes: 5 additions & 0 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,3 +880,8 @@ def callback(request):
# Match Payouts contract
MATCH_PAYOUTS_ABI = '[ { "inputs": [ { "internalType": "address", "name": "_owner", "type": "address" }, { "internalType": "address", "name": "_funder", "type": "address" }, { "internalType": "contract IERC20", "name": "_dai", "type": "address" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [], "name": "Finalized", "type": "event" }, { "anonymous": false, "inputs": [], "name": "Funded", "type": "event" }, { "anonymous": false, "inputs": [], "name": "FundingWithdrawn", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "PayoutAdded", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "PayoutClaimed", "type": "event" }, { "inputs": [ { "internalType": "address", "name": "_recipient", "type": "address" } ], "name": "claimMatchPayout", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "dai", "outputs": [ { "internalType": "contract IERC20", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "enablePayouts", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "finalize", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "funder", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "owner", "outputs": [ { "internalType": "address", "name": "", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "", "type": "address" } ], "name": "payouts", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "components": [ { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "internalType": "struct MatchPayouts.PayoutFields[]", "name": "_payouts", "type": "tuple[]" } ], "name": "setPayouts", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "state", "outputs": [ { "internalType": "enum MatchPayouts.State", "name": "", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "withdrawFunding", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ]'
MATCH_PAYOUTS_ADDRESS = '0xf2354570bE2fB420832Fb7Ff6ff0AE0dF80CF2c6'

# BulkCheckout contract
# BulkCheckout parameters
BULK_CHECKOUT_ADDRESS = "0x7d655c57f71464B6f83811C55D84009Cd9f5221C" # same address on mainnet and rinkeby
BULK_CHECKOUT_ABI = '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"address","name":"dest","type":"address"},{"indexed":true,"internalType":"address","name":"donor","type":"address"}],"name":"DonationSent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"dest","type":"address"}],"name":"TokenWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address payable","name":"dest","type":"address"}],"internalType":"struct BulkCheckout.Donation[]","name":"_donations","type":"tuple[]"}],"name":"donate","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"_dest","type":"address"}],"name":"withdrawEther","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_tokenAddress","type":"address"},{"internalType":"address","name":"_dest","type":"address"}],"name":"withdrawToken","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
150 changes: 150 additions & 0 deletions app/assets/v2/js/grants/ingest-missing-contributions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* @notice Vue component for ingesting contributions that were missed during checkout
* @dev See more at: https://github.com/gitcoinco/web/issues/7744
*/

let appIngestContributions;

Vue.component('grants-ingest-contributions', {
delimiters: [ '[[', ']]' ],

data: function() {
return {
form: {
txHash: undefined, // user transaction hash, used to ingest L1 donations
userAddress: undefined // user address, used to ingest zkSync (L2) donations
},
errors: {}, // keys are errors that occurred
submitted: false // true if form has been submitted and we are waiting on response
};
},

methods: {
checkForm() {
this.submitted = true;
this.errors = {};
let isValidTxHash;
let isValidAddress;

// Validate that at least one of txHash and userAddress is provided
const { txHash, userAddress } = this.form;
const isFormComplete = txHash || userAddress;

if (!isFormComplete) {
// Form was not filled out
this.$set(this.errors, 'invalidForm', 'Please enter a valid transaction hash or a valid wallet address');
} else {
// Form was filled out, so validate the inputs
isValidTxHash = txHash && txHash.length === 66 && txHash.startsWith('0x');
isValidAddress = ethers.utils.isAddress(userAddress);

if (txHash && !isValidTxHash) {
this.$set(this.errors, 'txHash', 'Please enter a valid transaction hash');
}
if (userAddress && !isValidAddress) {
this.$set(this.errors, 'address', 'Please enter a valid address');
}
}

if (Object.keys(this.errors).length) {
return false; // there are errors the user must correct
}

return {
txHash: isValidTxHash ? txHash : '',
// getAddress returns checksum address required by web3py, and throws if address is invalid
userAddress: isValidAddress ? ethers.utils.getAddress(userAddress) : ''
};
},

async ingest(event) {
try {
event.preventDefault();

// Return if form is not valid
const formParams = this.checkForm();

if (!formParams) {
return;
}

// Send POST requests to ingest contributions
const { txHash, userAddress } = formParams;
const csrfmiddlewaretoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const url = '/grants/ingest';
const headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' };
const payload = {
csrfmiddlewaretoken,
txHash,
userAddress,
network: document.web3network || 'mainnet'
};
const postParams = {
method: 'POST',
headers,
body: new URLSearchParams(payload)
};

// Send saveSubscription request
const res = await fetch(url, postParams);
const json = await res.json();

// Notify user of success status, and clear form if successful
console.log('ingestion response: ', json);
if (!json.success) {
console.log('ingestion failed');
this.submitted = false;
throw new Error('Your transactions could not be processed, please try again');
} else {
console.log('ingestion successful');
_alert('Your contributions have been added successfully!', 'success');
this.resetForm();
}
} catch (e) {
this.handleError(e);
}
},

resetForm() {
this.form.txHash = undefined;
this.form.userAddress = undefined;
this.errors = {};
this.submitted = false;
},

handleError(err) {
console.error(err); // eslint-disable-line no-console
let message = 'There was an error';

if (err.message)
message = err.message;
else if (err.msg)
message = err.msg;
else if (typeof err === 'string')
message = err;

_alert(message, 'error');
this.submitted = false;
}
},

watch: {
deep: true,
form: {
deep: true,
handler(newVal, oldVal) {
this.checkForm();
this.submitted = false;
this.errors = {};
}
}
}
});

if (document.getElementById('gc-grants-ingest-contributions')) {

appIngestContributions = new Vue({
delimiters: [ '[[', ']]' ],
el: '#gc-grants-ingest-contributions'
});
}
146 changes: 146 additions & 0 deletions app/grants/templates/grants/ingest-contributions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
{% comment %}
Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.

{% endcomment %}
{% load i18n static email_obfuscator add_url_schema avatar_tags %}
<!DOCTYPE html>
<html lang="en">

<head>
{% include 'shared/head.html' with slim=1 %}
{% include 'shared/cards.html' %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vue-select@latest/dist/vue-select.css">
<link rel="stylesheet" href="{% static "v2/css/grants/new.css" %}">
<link rel="stylesheet" href={% static "v2/css/tabs.css" %}>
</head>

<body class="interior {{ active }} grant g-font-muli">

{% include 'shared/tag_manager_2.html' %}
<div class="container-fluid header dash px-0">
{% include 'shared/top_nav.html' with class='d-md-flex' %}
{% include 'grants/nav.html' %}
</div>

<grants-ingest-contributions class="container-fluid bg-lightblue pb-5 pt-5" v-cloak id="gc-grants-ingest-contributions" inline-template>
<form action="" @submit="checkForm">
<div class="text-center">
<img style="width:6rem;" src="{% static "v2/images/grants/torchbearer.svg" %}">
</div>

<div class="container mt-3 mb-3 bg-white position-relative rounded col-lg-6 mx-auto">
<div class="row p-4 p-md-5">

<div class="col-12 text-center mb-4">
<h1 class="text-center font-title-xl">Add Missing Contributions</h1>
<p class="text-center font-smaller-1 text-black-60">
If you completed a Gitcoin Grants checkout, but don't see evidence of this in your
email or the Gitcoin interface, you can use this form to fix that!
</p>
</div>

{% csrf_token %}

<!-- Instructions -->
<div class="col-12 mb-2">
<h5 class="mt-4">Instructions</h5>
<hr>
</div>

<!-- Amount -->
<div class="col-12 mb-3">
<p class="font-body mb-1">
<ul>
<li>If you donated using L1 (Standard Checkout), please enter the transaction hash</li>
<li>If you donated using L2 (zkSync Checkout), please enter your wallet address</li>
<li>At least one of these two is required</li>
</ul>
</p>
</div>

<!-- Collect Information -->
<div class="col-12 mb-2">
<h5 class="mt-4">Contribution Data</h5>
<hr>
</div>

<!-- Transaction hash -->
<div class="col-12 mb-3">
<label class="font-caption letter-spacing text-black-60 text-uppercase">Transaction Hash</label>
<input id="amount" v-model="form.txHash" name="amount" class="form__input form__input-lg" />
</div>
<div class="col-12 text-danger" v-if="errors.txHash">
[[errors.txHash]]
</div>

<!-- User address -->
<div class="col-12 mb-3">
<label class="font-caption letter-spacing text-black-60 text-uppercase">Wallet Address</label>
<input id="amount" v-model="form.userAddress" name="amount" class="form__input form__input-lg" />
</div>

<div class="col-12 text-danger" v-if="errors.address">
[[errors.address]]
</div>

<div class="col-12 text-danger" v-if="errors.invalidForm">
[[errors.invalidForm]]
</div>

</div>
</div>

<div class="container mt-5">
<div class="row">
<div class="col-12 text-center">
<button class="btn btn-gc-blue btn-lg mb-3 px-5 btn-lg-padding" :disabled="submitted" type="submit" @click="ingest($event)">Add Contributions</button>
</div>
<div class="col-12 text-center" v-if="Object.keys(errors).length > 0">
Please verify forms errors and try again
</div>
<div class="col-12 text-center" v-else-if="submitted">
Processing your contributions. This may take a minute or two...
</div>
</div>
</div>

</form>
</grants-ingest-contributions>

{% include 'shared/bottom_notification.html' %}
{% include 'shared/footer.html' %}
{% include 'shared/current_profile.html' %}
{% include 'shared/analytics.html' %}
{% include 'grants/shared/shared_scripts.html' %}
{% include 'shared/footer_scripts.html' with vue=True ignore_inject_web3=1 %}

<script type="text/javascript" src="https://cdn.ethers.io/lib/ethers-5.0.umd.min.js"></script>
<script src="{% static "v2/js/grants/ingest-missing-contributions.js" %}"></script>

<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

<script src="{% static "v2/js/lib/ipfs-api.js" %}"></script>
<script src="{% static "v2/js/ipfs.js" %}"></script>
<script src="{% static "v2/js/abi.js" %}"></script>

<script src="{% static "v2/js/tokens.js" %}"></script>
<script src="{% static "v2/js/grants/shared.js" %}"></script>

<script src="{% static "v2/js/grants/new_match.js" %}"></script>

</body>

<html>
3 changes: 3 additions & 0 deletions app/grants/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
grants, grants_addr_as_json, grants_bulk_add, grants_by_grant_type, grants_cart_view, grants_info,
grants_stats_view, invoice, leaderboard, manage_ethereum_cart_data, new_matching_partner, profile, quickstart,
remove_grant_from_collection, save_collection, subscription_cancel, toggle_grant_favorite, verify_grant,
ingest_contributions_view, ingest_contributions
)

app_name = 'grants'
Expand Down Expand Up @@ -63,6 +64,7 @@
re_path(r'^new/?$', grant_new, name='new'),
re_path(r'^categories', grant_categories, name='grant_categories'),
path('<int:grant_id>/<slug:grant_slug>/fund', grant_fund, name='fund'),
path('ingest', ingest_contributions, name='ingest_contributions'),
path('bulk-fund', bulk_fund, name='bulk_fund'),
path('manage-ethereum-cart-data', manage_ethereum_cart_data, name='manage_ethereum_cart_data'),
path('get-ethereum-cart-data', get_ethereum_cart_data, name='get_ethereum_cart_data'),
Expand All @@ -84,6 +86,7 @@
),
path('cart/bulk-add/<str:grant_str>', grants_bulk_add, name='grants_bulk_add'),
path('cart', grants_cart_view, name='cart'),
path('add-missing-contributions', ingest_contributions_view, name='ingest_contributions_view'),
path('get-interrupted-contributions', get_interrupted_contributions, name='get_interrupted_contributions'),
path('<slug:grant_type>', grants_by_grant_type, name='grants_by_category2'),
path('<slug:grant_type>/', grants_by_grant_type, name='grants_by_category'),
Expand Down
Loading

0 comments on commit 891b0f8

Please sign in to comment.