Skip to content

Commit

Permalink
Merge branch 'stable'
Browse files Browse the repository at this point in the history
  • Loading branch information
octavioamu committed Mar 24, 2021
2 parents 3d42b0a + 0ddfd2e commit bf11b15
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 39 deletions.
40 changes: 40 additions & 0 deletions app/app/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.conf import settings

import random

class PrimaryDBRouter:
def db_for_read(self, model, **hints):
"""
Reads go to a randomly-chosen replica if backend node
Else go to default DB
"""
replicas = ['read_replica_1']
if settings.JOBS_NODE:
return random.choice(replicas)
if settings.CELERY_NODE:
return random.choice(replicas)
return 'default'

def db_for_write(self, model, **hints):
"""
Writes always go to primary.
"""
return 'default'

def allow_relation(self, obj1, obj2, **hints):
"""
Relations between objects are allowed if both objects are
in the primary/replica pool.
"""
db_set = {'default', 'read_replica_1'}
if obj1._state.db in db_set and obj2._state.db in db_set:
return True
return None

def allow_migrate(self, db, app_label, model_name=None, **hints):
"""
All non-auth models end up in this pool.
"""
if db == 'default':
return True
return False
15 changes: 14 additions & 1 deletion app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,15 @@

# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {'default': env.db()}
DATABASES = {
'default': env.db()
}
if ENV in ['prod']:
DATABASES = {
'default': env.db(),
'read_replica_1': env.db('READ_REPLICA_1_DATABASE_URL')
}
DATABASE_ROUTERS = ['app.db.PrimaryDBRouter']

# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
Expand Down Expand Up @@ -943,3 +951,8 @@ def callback(request):
# 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"}]'


JOBS_NODE = env.bool('JOBS_NODE', default=False)
CELERY_NODE = env.bool('CELERY_NODE', default=False)

60 changes: 34 additions & 26 deletions app/assets/v2/js/grants/ingest-missing-contributions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Vue.component('grants-ingest-contributions', {
return {
form: {
txHash: undefined, // user transaction hash, used to ingest L1 donations
userAddress: undefined // user address, used to ingest zkSync (L2) donations
userAddress: undefined, // user address, used to ingest zkSync (L2) donations
handle: undefined // user to ingest under -- ignored unless you are a staff
},
errors: {}, // keys are errors that occurred
submitted: false // true if form has been submitted and we are waiting on response
Expand All @@ -26,30 +27,37 @@ Vue.component('grants-ingest-contributions', {
let isValidTxHash;
let isValidAddress;

// Validate that at least one of txHash and userAddress is provided
// Validate that only one of txHash and userAddress is provided. We only allow one because (1) that will be the
// most common use case, and (2) later when we update cart.js to fallback to manual ingestion if regular POSTing
// fails, that will make DRYing the manual ingestion code simpler. Reference implementation of DRY code for
// fallback to manual ingestion here: https://github.com/gitcoinco/web/pull/8563
const { txHash, userAddress } = this.form;
const isFormComplete = txHash || userAddress;
const isFormComplete = Boolean(txHash) != Boolean(userAddress);

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

if (txHash && !isValidTxHash) {
this.$set(this.errors, 'txHash', 'Please enter a valid transaction hash');
}
if (userAddress && !isValidAddress) {
} else if (userAddress && !isValidAddress) {
this.$set(this.errors, 'address', 'Please enter a valid address');
}
if (document.contxt.is_staff && !this.form.handle) {
this.$set(this.errors, 'handle', 'Since you are staff, you must enter the handle of the profile to ingest for');
}
}

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

// Returns the values. We use an empty string to tell the backend when we're not ingesting (e.g. empty txHash
// means we're only validating based on an address)
return {
txHash: isValidTxHash ? txHash : '',
// getAddress returns checksum address required by web3py, and throws if address is invalid
Expand Down Expand Up @@ -98,52 +106,51 @@ Vue.component('grants-ingest-contributions', {
},

async ingest(event) {
let txHash;
let userAddress;

try {
event.preventDefault();

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

if (!formParams) {
return;
return; // return if form is not valid
}

// Make sure wallet is connected
let walletAddress;
let walletAddress = selectedAccount;

if (web3) {
walletAddress = (await web3.eth.getAccounts())[0];
if (!walletAddress) {
initWallet();
await onConnect();
walletAddress = selectedAccount;
}

if (!walletAddress) {
throw new Error('Please connect a wallet');
}

// TODO if user is staff, add a username field and bypass the below checks

// Parse out provided form inputs
const { txHash, userAddress } = formParams;
({ txHash, userAddress } = formParams);

// If user entered an address, verify that it matches the user's connected wallet address
if (userAddress && ethers.utils.getAddress(userAddress) !== ethers.utils.getAddress(walletAddress)) {
throw new Error('Provided wallet address does not match connected wallet address');
}

// If user entered an tx hash, verify that the tx's from address matches the connected wallet address
let fromAddress;

// If user entered an tx hash, verify that the tx's from address matches the connected wallet address
if (txHash) {
const receipt = await this.getTxReceipt(txHash);

if (!receipt) {
throw new Error('Transaction hash not found. Are you sure this transaction was confirmed?');
}
fromAddress = receipt.from;

if (ethers.utils.getAddress(fromAddress) !== ethers.utils.getAddress(walletAddress)) {
throw new Error('Sender of the provided transaction does not match connected wallet address');
if (ethers.utils.getAddress(receipt.from) !== ethers.utils.getAddress(walletAddress)) {
throw new Error('Sender of the provided transaction does not match connected wallet address. Please contact Gitcoin and we can ingest your contributions for you');
}
}



// If we are here, the provided form data is valid. However, someone could just POST directly to the endpoint,
// so to workaround that we ask the user for a signature, and the backend will verify that signature
const { signature, message } = await this.signMessage(walletAddress);
Expand All @@ -158,7 +165,8 @@ Vue.component('grants-ingest-contributions', {
userAddress,
signature,
message,
network: document.web3network || 'mainnet'
network: document.web3network || 'mainnet',
handle: this.form.handle
};
const postParams = {
method: 'POST',
Expand Down
8 changes: 8 additions & 0 deletions app/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4543,6 +4543,14 @@ def ips(self):
ips.append(login.ip_address)
return ips

@property
def last_known_ip(self):
ips = self.ips
if len(ips) > 0:
return ips[0]
return ''


@property
def locations(self):
from app.utils import get_location_from_ip
Expand Down
92 changes: 92 additions & 0 deletions app/grants/management/commands/update_contributor_address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
"""
For any Subscription objects that have a contributor address of "N/A", put in the transaction's from address
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/>.
"""

from django.core.management.base import BaseCommand
import requests
from dashboard.utils import get_web3
from grants.models import Subscription
from web3 import Web3


class Command(BaseCommand):

help = "Inserts missing subscriptions and contributions into the database"

def add_arguments(self, parser):
parser.add_argument('network',
default='mainnet',
type=str,
help="Network can be mainnet or rinkeby"
)

def handle(self, *args, **options):
# Parse inputs / setup
network = options['network']
w3 = get_web3(network)
if network != 'mainnet' and network != 'rinkeby':
raise Exception('Invalid network: Must be mainnet or rinkeby')

# Get array of subscriptions with N/A contributor address
bad_subscriptions = Subscription.objects.filter(contributor_address="N/A")

# For each one, find the from address and use that to replace the N/A
for subscription in bad_subscriptions:
try:
tx_hash = subscription.split_tx_id
if tx_hash[0:8].lower() == 'sync-tx:':
# zkSync transaction, so use zkSync's API: https://zksync.io/api/v0.1.html#transaction-details
tx_hash = tx_hash.replace('sync-tx:', '0x')
base_url = 'https://rinkeby-api.zksync.io/api/v0.1' if network == 'rinkeby' else 'https://api.zksync.io/api/v0.1'
r = requests.get(f"{base_url}/transactions_all/{tx_hash}")
r.raise_for_status()
tx_data = r.json() # zkSync transaction data
if not tx_data:
print(f'Skipping, zkSync receipt not found for transaction {subscription.split_tx_id}')
continue
from_address = tx_data['from']
elif len(tx_hash) == 66:
# Standard L1 transaction
receipt = w3.eth.getTransactionReceipt(tx_hash)
if not receipt:
print(f'Skipping, L1 receipt not found for transaction {subscription.split_tx_id}')
continue
from_address = receipt['from']
else:
print(f'Skipping unknown transaction hash format, could not parse {subscription.split_tx_id}')
continue

if not from_address:
print(f'Skipping invalid from address {from_address} for transaction hash {subscription.split_tx_id}')

# Note: This approach does not guarantee the correct contributor address. Because we are using the sender
# of the transaction as the contributor, we get the wrong address for users with wallet's that use
# a relayer or meta-transactions, such as Argent. In those cases, the relayer address is incorrectly
# listed as the sender. A more robust approach would take a non-trivial amount of work since it
# requires recognizing relayed transaction and parsing them to find the wallet address, and there's no
# universal standard for relayed transaction format
from_address = Web3.toChecksumAddress(from_address)
subscription.contributor_address = from_address
subscription.save()
except Exception as e:
print(f'Skipping: Error when fetching from_address for transaction hash {subscription.split_tx_id}')
print(e)
print("\n")
5 changes: 5 additions & 0 deletions app/grants/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,11 @@ def presave_contrib(sender, instance, **kwargs):
'tx_id': ele.tx_id,
}

if instance.subscription.contributor_profile:
scp = instance.subscription.contributor_profile
instance.normalized_data['handle'] = scp.handle
instance.normalized_data['last_known_ip'] = scp.last_known_ip


def next_month():
"""Get the next month time."""
Expand Down
8 changes: 8 additions & 0 deletions app/grants/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,11 @@ def process_grant_creation_admin_email(self, grant_id):
new_grant_admin(grant)
except Exception as e:
print(e)


@app.shared_task(bind=True, max_retries=3)
def save_contribution(self, contrib_id):
from grants.models import Contribution
contrib = Contribution.objects.get(pk=contrib_id)
contrib.save()

28 changes: 22 additions & 6 deletions app/grants/templates/grants/ingest-contributions.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ <h5 class="mt-4">Instructions</h5>
<p class="font-body mb-1">
<ul>
<li>Make sure your wallet is connected to mainnet with the same address you used to checkout</li>
<li>If you donated using L1 (Standard Checkout), please enter the transaction hash. There is no need to also enter your wallet for L1 transactions. *Do not enter a transaction hash for L2 transactions*</li>
<li>If you donated using L2 (zkSync Checkout), please enter your wallet address, *Do not enter a transaction hash for L2 transactions*</li>
<li>At least one of these two is required</li>
<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>Only enter one of these at a time</li>
</ul>
</p>
</div>
Expand All @@ -81,7 +81,7 @@ <h5 class="mt-4">Contribution Data</h5>

<!-- Transaction hash -->
<div class="col-12 mb-3">
<label class="font-caption letter-spacing text-black-60 text-uppercase">L1 Transaction Hash</label>
<label class="font-caption letter-spacing text-black-60 text-uppercase">Transaction Hash (for Standard Checkout on L1)</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">
Expand All @@ -90,14 +90,30 @@ <h5 class="mt-4">Contribution Data</h5>

<!-- User address -->
<div class="col-12 mb-3">
<label class="font-caption letter-spacing text-black-60 text-uppercase">Wallet Address for L2 transactions</label>
<label class="font-caption letter-spacing text-black-60 text-uppercase">Wallet Address (for zkSync Checkout)</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>

<!-- Profile to ingest for, only usable by staff -->
{% if is_staff %}
<div class="col-12 mb-3">
<label class="font-caption letter-spacing text-black-60 text-uppercase">Profile Handle</label>
<p>
<span class="font-weight-bold">NOTE</span>:
This field is only viewable to staff. Enter the username of the user you are ingesting for
<span class="font-weight-bold">OR</span>
enter your own username to ingest your own contributions.
</p>
<input id="amount" v-model="form.handle" name="amount" class="form__input form__input-lg" />
</div>
<div class="col-12 text-danger" v-if="errors.handle">
[[errors.handle]]
</div>
{% endif %}

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

0 comments on commit bf11b15

Please sign in to comment.