/ python / proof.py
proof.py
  1  #!/usr/bin/env python
  2  import base64
  3  import json
  4  import os
  5  import sys
  6  import subprocess
  7  from web3 import Web3
  8  from pathlib import Path
  9  from eth_account.messages import encode_defunct
 10  from web3.auto import w3
 11  from helpers.common import Metadata
 12  from helpers.merkle import MerkleTree
 13  
 14  SSH_KEYS_DIR = os.path.join(Path.home(), ".ssh")
 15  BIN_DIR= os.path.join(os.getcwd(), "./bin")
 16  IGNORED_SSH_DIR_FILES = ['.DS_Store']
 17  
 18  # Set PATH for python to find age
 19  env = os.environ.copy()
 20  env["PATH"] = BIN_DIR + os.pathsep + env["PATH"]
 21  
 22  def error(msg):
 23      print(f"\033[31m{msg}\033[0m")
 24      sys.exit(1)
 25  
 26  
 27  def parse_metadata(filename):
 28      with open(filename, 'r') as f:
 29          return Metadata.from_json(f.read())
 30  
 31  
 32  def ask_user_info(metadata):
 33      username = input(
 34          "Enter your github username so we can check if you are participating in the airdrop:\n")
 35      if username not in metadata.encryptedKeys:
 36          error("This Github account is not eligible for claiming")
 37  
 38      ethereumAddress = input(
 39          '''
 40  Ethereum wallet address is necessary to generate a proof that you will send through our web page.
 41  \033[33mImportant notice: you need to make a claim transaction from the entered address!\033[0m
 42  
 43  Enter the ethereum address to which you plan to receive the airdrop:
 44  ''')
 45  
 46      if not Web3.is_address(ethereumAddress):
 47          error("You entered an incorrect Ethereum address")
 48  
 49      return (username, ethereumAddress)
 50  
 51  
 52  def choose_ssh_key():
 53      files = []
 54      try:
 55          files = os.listdir(SSH_KEYS_DIR)
 56      except FileNotFoundError:
 57          pass
 58  
 59      sshKeys = []
 60      for f in files:
 61          if f in IGNORED_SSH_DIR_FILES:
 62              continue
 63  
 64          path = os.path.join(SSH_KEYS_DIR, f)
 65          if not is_ssh_key(path):
 66              continue
 67          sshKeys.append(path)
 68  
 69      if len(sshKeys) > 0:
 70          print(f"\nYour ssh keys in ~/.ssh:")
 71          for key in sshKeys:
 72              print(key)
 73  
 74      sshKeyPath = input(
 75          '''
 76  Now the script needs your ssh key to generate proof. Please, enter path for github SSH key:
 77  ''')
 78      pubKeyPath = sshKeyPath + ".pub"
 79  
 80      if not os.path.exists(sshKeyPath):
 81          error("Specified file does not exits")
 82      elif not is_ssh_key(sshKeyPath):
 83          error("Specified file is not a SSH private key")
 84      elif not os.path.isfile(pubKeyPath) or not os.path.exists(pubKeyPath):
 85          error(
 86              f"SSH public key ({pubKeyPath}) does not exist in current directory")
 87  
 88      pubKey = ""
 89      with open(pubKeyPath, 'r') as pubKeyFile:
 90          pubKey = " ".join(pubKeyFile.read().split(" ")[0:2])
 91  
 92      return pubKey.strip(), sshKeyPath
 93  
 94  
 95  def is_ssh_key(path):
 96      if not os.path.isfile(path):
 97          return False
 98  
 99      with open(path, 'r') as file:
100          try:
101              key = file.read()
102              return key.startswith("-----BEGIN")
103          except Exception as e:
104              print("Couldn't read the file:", file, "Reason:", e)
105              return False
106  
107  
108  def decrypt_temp_eth_account(sshPubKey, sshPrivKey, username, metadata):
109      if sshPubKey not in metadata.encryptedKeys[username]:
110          error("Specified SSH key is not eligible for claiming. Only RSA and Ed25519 keys added before our Github snapshot are supported for proof generation.")
111  
112      data = metadata.encryptedKeys[username][sshPubKey]
113      result = subprocess.run(["age",
114                               "--decrypt",
115                               "--identity",
116                               sshPrivKey],
117                              capture_output=True,
118                              input=data.encode(),
119                              env=env)
120      if result.returncode != 0:
121          age_stderr = result.stderr.replace('https://filippo.io/age/report', 'https://fluence.chat')
122          raise OSError(age_stderr)
123  
124      return w3.eth.account.from_key(result.stdout.decode())
125  
126  
127  def get_merkle_proof(metadata, tempETHAccount):
128      address = tempETHAccount.address.lower()
129      if address not in metadata.addresses:
130          raise ValueError("Invalid temp address")
131  
132      tree = MerkleTree(metadata.addresses)
133      index = metadata.addresses.index(address)
134      return index, tree.get_proof(index)
135  
136  
137  def main():
138      metadataPath = "metadata.json"
139      metadata = parse_metadata(metadataPath)
140  
141      print('''
142  Welcome to the proof generation script for Fluence Developer Reward Airdrop.
143  5% of the FLT supply is allocated to ~110,000 developers who contributed into open source web3 repositories during last year.
144  Public keys of selected Github accounts were added into a smart contract on Ethereum. Claim your allocation and help us build the decentralized internet together!
145  
146  Check if you are eligible and proceed with claiming
147  ''')
148  
149      username, receiverAddress = ask_user_info(metadata)
150      sshPubKey, sshKeyPath = choose_ssh_key()
151      tempETHAccount = decrypt_temp_eth_account(
152          sshPubKey, sshKeyPath, username, metadata
153      )
154      index, merkleProof = get_merkle_proof(metadata, tempETHAccount)
155      base64MerkleProof = base64.b64encode(
156          json.dumps(merkleProof).encode()
157      ).decode()
158  
159      sign = tempETHAccount.sign_message(encode_defunct(hexstr=receiverAddress))
160  
161      print("\nSuccess! Copy the line below and paste it in the fluence airdrop website.")
162      print(f"{index},{tempETHAccount.address.lower()},{sign.signature.hex()},{base64MerkleProof}")
163  
164  
165  if __name__ == '__main__':
166      main()