Skip to content

Commit

Permalink
Start improving missing contribution page (#8546)
Browse files Browse the repository at this point in the history
* Improving missing contribution page

* Update web3 setup
  • Loading branch information
mds1 authored Mar 11, 2021
1 parent ee3066e commit b8250d1
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 3 deletions.
95 changes: 92 additions & 3 deletions app/assets/v2/js/grants/ingest-missing-contributions.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,46 @@ Vue.component('grants-ingest-contributions', {
};
},

// Wrapper around web3's getTransactionReceipt so it can be used with await
async getTxReceipt(txHash) {
return new Promise(function(resolve, reject) {
web3.eth.getTransactionReceipt(txHash, (err, res) => {
if (err) {
return reject(err);
}
resolve(res);
});
});
},

// Asks user to sign a message as verification they own the provided address
async signMessage(userAddress) {
const baseMessage = 'Sign this message as verification that you control the provided wallet address'; // base message that will be signed
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}`;

// Get signature from user
const isValidSignature = (sig) => ethers.utils.isHexString(sig) && sig.length === 132; // used to verify signature
let signature = await signer.signMessage(message); // prompt to user is here, uses eth_sign

// Fallback to personal_sign if eth_sign isn't supported (e.g. for Status and other wallets)
if (!isValidSignature(signature)) {
signature = await ethersProvider.send(
'personal_sign',
[ ethers.utils.hexlify(ethers.utils.toUtf8Bytes(message)), userAddress.toLowerCase() ]
);
}

// Verify signature
if (!isValidSignature(signature)) {
throw new Error(`Invalid signature: ${signature}`);
}

return { signature, message };
},

async ingest(event) {
try {
event.preventDefault();
Expand All @@ -68,15 +108,56 @@ Vue.component('grants-ingest-contributions', {
return;
}

// Send POST requests to ingest contributions
// Make sure wallet is connected
let walletAddress;

if (web3) {
walletAddress = (await web3.eth.getAccounts())[0];
}
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;

// 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 (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 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);

// Send POST requests to ingest contributions
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,
signature,
message,
network: document.web3network || 'mainnet'
};
const postParams = {
Expand All @@ -86,8 +167,16 @@ Vue.component('grants-ingest-contributions', {
};

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

try {
const res = await fetch(url, postParams);

json = await res.json();
} catch (err) {
console.error(err);
throw new Error('Something went wrong. Please verify the form parameters and try again later');
}

// Notify user of success status, and clear form if successful
console.log('ingestion response: ', json);
Expand Down
1 change: 1 addition & 0 deletions app/grants/templates/grants/ingest-contributions.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ <h5 class="mt-4">Instructions</h5>
<div class="col-12 mb-3">
<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</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>
Expand Down
19 changes: 19 additions & 0 deletions app/grants/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from dashboard.utils import get_web3, has_tx_mined
from economy.models import Token as FTokens
from economy.utils import convert_amount, convert_token_to_usdt
from eth_account.messages import defunct_hash_message
from gas.utils import conf_time_spread, eth_usd_conv_rate, gas_advisories, recommend_min_gas_price_to_confirm_in_time
from grants.models import (
CartActivity, Contribution, Flag, Grant, GrantAPIKey, GrantBrandingRoutingPolicy, GrantCategory, GrantCLR,
Expand Down Expand Up @@ -3403,9 +3404,27 @@ def ingest_contributions(request):
profile = request.user.profile
txHash = request.POST.get('txHash')
userAddress = request.POST.get('userAddress')
signature = request.POST.get('signature')
message = request.POST.get('message')
network = request.POST.get('network')
ingestion_types = [] # after each series of ingestion, we append the ingestion_method to this array

# Setup web3
w3 = get_web3(network)

def verify_signature(signature, message, expected_address):
message_hash = defunct_hash_message(text=message)
recovered_address = w3.eth.account.recoverHash(message_hash, signature=signature)
if recovered_address.lower() != expected_address.lower():
raise Exception("Signature could not be verified")

if txHash != '':
receipt = w3.eth.getTransactionReceipt(txHash)
from_address = receipt['from']
verify_signature(signature, message, from_address)
if userAddress != '':
verify_signature(signature, message, userAddress)

def get_token(w3, network, address):
if (address == '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'):
# 0xEeee... is used to represent ETH in the BulkCheckout contract
Expand Down

0 comments on commit b8250d1

Please sign in to comment.