getcoins.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2020-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  
   6  import argparse
   7  import io
   8  import requests
   9  import subprocess
  10  import sys
  11  import xml.etree.ElementTree
  12  
  13  DEFAULT_GLOBAL_FAUCET = 'https://signetfaucet.com/claim'
  14  DEFAULT_GLOBAL_CAPTCHA = 'https://signetfaucet.com/captcha'
  15  GLOBAL_FIRST_BLOCK_HASH = '00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53'
  16  
  17  # braille unicode block
  18  BASE = 0x2800
  19  BIT_PER_PIXEL = [
  20      [0x01, 0x08],
  21      [0x02, 0x10],
  22      [0x04, 0x20],
  23      [0x40, 0x80],
  24  ]
  25  BW = 2
  26  BH = 4
  27  
  28  # imagemagick or compatible fork (used for converting SVG)
  29  CONVERT = 'convert'
  30  
  31  class PPMImage:
  32      '''
  33      Load a PPM image (Pillow-ish API).
  34      '''
  35      def __init__(self, f):
  36          if f.readline() != b'P6\n':
  37              raise ValueError('Invalid ppm format: header')
  38          line = f.readline()
  39          (width, height) = (int(x) for x in line.rstrip().split(b' '))
  40          if f.readline() != b'255\n':
  41              raise ValueError('Invalid ppm format: color depth')
  42          data = f.read(width * height * 3)
  43          stride = width * 3
  44          self.size = (width, height)
  45          self._grid = [[tuple(data[stride * y + 3 * x:stride * y + 3 * (x + 1)]) for x in range(width)] for y in range(height)]
  46  
  47      def getpixel(self, pos):
  48          return self._grid[pos[1]][pos[0]]
  49  
  50  def print_image(img, threshold=128):
  51      '''Print black-and-white image to terminal in braille unicode characters.'''
  52      x_blocks = (img.size[0] + BW - 1) // BW
  53      y_blocks = (img.size[1] + BH - 1) // BH
  54  
  55      for yb in range(y_blocks):
  56          line = []
  57          for xb in range(x_blocks):
  58              ch = BASE
  59              for y in range(BH):
  60                  for x in range(BW):
  61                      try:
  62                          val = img.getpixel((xb * BW + x, yb * BH + y))
  63                      except IndexError:
  64                          pass
  65                      else:
  66                          if val[0] < threshold:
  67                              ch |= BIT_PER_PIXEL[y][x]
  68              line.append(chr(ch))
  69          print(''.join(line))
  70  
  71  parser = argparse.ArgumentParser(description='Script to get coins from a faucet.', epilog='You may need to start with double-dash (--) when providing bitcoin-cli arguments.')
  72  parser.add_argument('-c', '--cmd', dest='cmd', default='bitcoin-cli', help='bitcoin-cli command to use')
  73  parser.add_argument('-f', '--faucet', dest='faucet', default=DEFAULT_GLOBAL_FAUCET, help='URL of the faucet')
  74  parser.add_argument('-g', '--captcha', dest='captcha', default=DEFAULT_GLOBAL_CAPTCHA, help='URL of the faucet captcha, or empty if no captcha is needed')
  75  parser.add_argument('-a', '--addr', dest='addr', default='', help='Bitcoin address to which the faucet should send')
  76  parser.add_argument('-p', '--password', dest='password', default='', help='Faucet password, if any')
  77  parser.add_argument('-n', '--amount', dest='amount', default='0.001', help='Amount to request (0.001-0.1, default is 0.001)')
  78  parser.add_argument('-i', '--imagemagick', dest='imagemagick', default=CONVERT, help='Path to imagemagick convert utility')
  79  parser.add_argument('bitcoin_cli_args', nargs='*', help='Arguments to pass on to bitcoin-cli (default: -signet)')
  80  
  81  args = parser.parse_args()
  82  
  83  if args.bitcoin_cli_args == []:
  84      args.bitcoin_cli_args = ['-signet']
  85  
  86  
  87  def bitcoin_cli(rpc_command_and_params):
  88      argv = [args.cmd] + args.bitcoin_cli_args + rpc_command_and_params
  89      try:
  90          return subprocess.check_output(argv).strip().decode()
  91      except FileNotFoundError:
  92          raise SystemExit(f"The binary {args.cmd} could not be found")
  93      except subprocess.CalledProcessError:
  94          cmdline = ' '.join(argv)
  95          raise SystemExit(f"-----\nError while calling {cmdline} (see output above).")
  96  
  97  
  98  if args.faucet.lower() == DEFAULT_GLOBAL_FAUCET:
  99      # Get the hash of the block at height 1 of the currently active signet chain
 100      curr_signet_hash = bitcoin_cli(['getblockhash', '1'])
 101      if curr_signet_hash != GLOBAL_FIRST_BLOCK_HASH:
 102          raise SystemExit('The global faucet cannot be used with a custom Signet network. Please use the global signet or setup your custom faucet to use this functionality.\n')
 103  else:
 104      # For custom faucets, don't request captcha by default.
 105      if args.captcha == DEFAULT_GLOBAL_CAPTCHA:
 106          args.captcha = ''
 107  
 108  if args.addr == '':
 109      # get address for receiving coins
 110      args.addr = bitcoin_cli(['getnewaddress', 'faucet', 'bech32'])
 111  
 112  data = {'address': args.addr, 'password': args.password, 'amount': args.amount}
 113  
 114  # Store cookies
 115  # for debugging: print(session.cookies.get_dict())
 116  session = requests.Session()
 117  
 118  if args.captcha != '': # Retrieve a captcha
 119      try:
 120          res = session.get(args.captcha)
 121          res.raise_for_status()
 122      except requests.exceptions.RequestException as e:
 123          raise SystemExit(f"Unexpected error when contacting faucet: {e}")
 124  
 125      # Size limitation
 126      svg = xml.etree.ElementTree.fromstring(res.content)
 127      if svg.attrib.get('width') != '150' or svg.attrib.get('height') != '50':
 128          raise SystemExit("Captcha size doesn't match expected dimensions 150x50")
 129  
 130      # Convert SVG image to PPM, and load it
 131      try:
 132          rv = subprocess.run([args.imagemagick, 'svg:-', '-depth', '8', 'ppm:-'], input=res.content, check=True, capture_output=True)
 133      except FileNotFoundError:
 134          raise SystemExit(f"The binary {args.imagemagick} could not be found. Please make sure ImageMagick (or a compatible fork) is installed and that the correct path is specified.")
 135  
 136      img = PPMImage(io.BytesIO(rv.stdout))
 137  
 138      # Terminal interaction
 139      print_image(img)
 140      print(f"Captcha from URL {args.captcha}")
 141      data['captcha'] = input('Enter captcha: ')
 142  
 143  try:
 144      res = session.post(args.faucet, data=data)
 145  except Exception:
 146      raise SystemExit(f"Unexpected error when contacting faucet: {sys.exc_info()[0]}")
 147  
 148  # Display the output as per the returned status code
 149  if res:
 150      # When the return code is in between 200 and 400 i.e. successful
 151      print(res.text)
 152  elif res.status_code == 404:
 153      print('The specified faucet URL does not exist. Please check for any server issues/typo.')
 154  elif res.status_code == 429:
 155      print('The script does not allow for repeated transactions as the global faucet is rate-limited to 1 request/IP/day. You can access the faucet website to get more coins manually')
 156  else:
 157      print(f'Returned Error Code {res.status_code}\n{res.text}\n')
 158      print('Please check the provided arguments for their validity and/or any possible typo.')
 159