Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start improving missing contribution page #8546

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -3397,9 +3398,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