From 4ccf9ab7e110f99100b54484d5da8e36befe984d Mon Sep 17 00:00:00 2001 From: Kirill Varlamov Date: Wed, 16 Jun 2021 13:34:04 +0300 Subject: [PATCH 01/12] Experiment with peewee + argparse subparsers --- airdrop.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ enumfield.py | 16 ++++++++ test_airdrop.py | 23 ++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 airdrop.py create mode 100644 enumfield.py create mode 100644 test_airdrop.py diff --git a/airdrop.py b/airdrop.py new file mode 100644 index 0000000..d1fbc7a --- /dev/null +++ b/airdrop.py @@ -0,0 +1,99 @@ +from enum import Enum +import peewee as pw +from enumfield import EnumField +from web3 import Web3 +DBFILE = 'db.sqlite' +db = pw.SqliteDatabase(DBFILE) + +class BaseModel(pw.Model): + class Meta: + database = db + + +class Config(BaseModel): + name = pw.CharField(max_length=255) + value = pw.CharField(max_length=255) + + +class Recipient(BaseModel): + address = pw.CharField(max_length=42) + amount = pw.DecimalField() + + +class Tx(BaseModel): + class TxStatus(Enum): + WAITING = 1 + READY_TO_SEND = 2 + UPLOADING = 3 + OCR = 4 + DONE = 5 + + nonce = pw.IntegerField() + recipient = pw.ForeignKeyField(Recipient, backref='txes') + status = EnumField(TxStatus) + + +db.connect() +db.create_tables([Config, Recipient, Tx]) + + +def add_recipient(args): + Recipient.create(address=args['address'], amount=args['amount']) + + +def show(args): + print('show current state', args) + + +def prepare_txes(args): + for i in Recipient.select(Recipient.address).join(Tx, on=(Tx.recipient == Recipient.id)): + print("ttt", i) + + +def init(args): + print('Initializing...') + + web3_url = Config.get_or_none(Config.name == 'web3_url') + if web3_url: + w3 = Web3(Web3.HTTPProvider(web3_url)) + else: + web3_url = input("Enter web3 node url: ") + Config(name='web3_url', value=web3_url).save() + w3 = Web3(Web3.HTTPProvider(web3_url)) + + if not w3.isConnected(): + print('Web3 is not connected') + + if not Config.get_or_none(Config.name == 'priv_key'): + priv_key = input("Enter private key in hex: ") + sender = w3.eth.account.from_key(priv_key) + print(f"Account: {sender.address}") + Config(name='priv_key', value=priv_key).save() + Config(name='sender_addr', value=sender.address).save() + + if not Config.get_or_none(Config.name == 'token_contract'): + token_contract = input("Enter token contract: ") + Config(name='token_contract', value=token_contract).save() + + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser(description='Make ERC-20 tokens airdrop') + subparsers = parser.add_subparsers() + + parser_init = subparsers.add_parser('init', help='Initialize db') + parser_init.set_defaults(func=init) + + parser_add_recipient = subparsers.add_parser('add', help='Add recipient') + parser_add_recipient.add_argument('to', nargs='?', type=str) + parser_add_recipient.add_argument('amount', nargs='?', type=float) + parser_add_recipient.set_defaults(func=add_recipient) + + parser_show = subparsers.add_parser('show', help='show application stats') + parser_show.set_defaults(func=show) + + parser_prepare = subparsers.add_parser('prepare', help='Prepare txes') + parser_prepare.set_defaults(func=prepare_txes) + + args = parser.parse_args() + args.func(args) diff --git a/enumfield.py b/enumfield.py new file mode 100644 index 0000000..c33432e --- /dev/null +++ b/enumfield.py @@ -0,0 +1,16 @@ +from peewee import IntegerField + + +class EnumField(IntegerField): + """ + This class enable an Enum like field for Peewee + """ + def __init__(self, choices, *args, **kwargs): + super(IntegerField, self).__init__(*args, **kwargs) + self.choices = choices + + def db_value(self, value): + return value.value + + def python_value(self, value): + return self.choices(value) diff --git a/test_airdrop.py b/test_airdrop.py new file mode 100644 index 0000000..7515fef --- /dev/null +++ b/test_airdrop.py @@ -0,0 +1,23 @@ +import airdrop as ad +from decimal import Decimal + + +def test_add_recepient(): + ad.Recipient.delete().execute() + ad.add_recipient({"address": "bla", "amount": Decimal('12.34')}) + ad.add_recipient({"address": "bla", "amount": Decimal('234.56')}) + assert ad.Recipient.get_by_id(1).address == "bla" + assert ad.Recipient.get_by_id(1).amount == Decimal('12.34') + assert ad.Recipient.get_by_id(2).address == "bla" + assert ad.Recipient.get_by_id(2).amount == Decimal('234.56') + + +def test_prepare_txes(): + ad.Recipient.delete().execute() + rcpt1 = ad.Recipient.create(address="weret", amount=Decimal('234.56')) + rcpt2 = ad.Recipient.create(address="wereg", amount=Decimal('234.56')) + rcpt3 = ad.Recipient.create(address="wereu", amount=Decimal('234.56')) + ad.Tx.create(recipient=rcpt1, nonce=1, status=ad.Tx.TxStatus.OCR) + ad.Tx.create(recipient=rcpt2, nonce=1, status=ad.Tx.TxStatus.OCR) + ad.Tx.create(recipient=rcpt3, nonce=1, status=ad.Tx.TxStatus.OCR) + ad.prepare_txes({}) From 826b0d278eaf97af2d6d3d5f465efabf7fcb2bed Mon Sep 17 00:00:00 2001 From: lazarev-alexander Date: Thu, 17 Jun 2021 20:05:34 +0300 Subject: [PATCH 02/12] feat: generate txes --- .gitignore | 43 ++++++++++++++++++ airdrop.py | 106 +++++++------------------------------------ dev_requirements.txt | 8 ++++ enumfield.py | 5 +- models.py | 38 ++++++++++++++++ requirements.txt | 39 ++++++++++++++++ 6 files changed, 148 insertions(+), 91 deletions(-) create mode 100644 .gitignore create mode 100644 dev_requirements.txt create mode 100644 models.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15a031d --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# dev env +.vscode/ +.idea + +# db +*.sqlite \ No newline at end of file diff --git a/airdrop.py b/airdrop.py index d1fbc7a..f47ac20 100644 --- a/airdrop.py +++ b/airdrop.py @@ -1,99 +1,27 @@ -from enum import Enum import peewee as pw -from enumfield import EnumField -from web3 import Web3 -DBFILE = 'db.sqlite' -db = pw.SqliteDatabase(DBFILE) - -class BaseModel(pw.Model): - class Meta: - database = db - - -class Config(BaseModel): - name = pw.CharField(max_length=255) - value = pw.CharField(max_length=255) - - -class Recipient(BaseModel): - address = pw.CharField(max_length=42) - amount = pw.DecimalField() - - -class Tx(BaseModel): - class TxStatus(Enum): - WAITING = 1 - READY_TO_SEND = 2 - UPLOADING = 3 - OCR = 4 - DONE = 5 - - nonce = pw.IntegerField() - recipient = pw.ForeignKeyField(Recipient, backref='txes') - status = EnumField(TxStatus) +from models import ( + db, + Recipient, + Config, + Tx, +) db.connect() db.create_tables([Config, Recipient, Tx]) -def add_recipient(args): - Recipient.create(address=args['address'], amount=args['amount']) - - -def show(args): - print('show current state', args) - - -def prepare_txes(args): - for i in Recipient.select(Recipient.address).join(Tx, on=(Tx.recipient == Recipient.id)): - print("ttt", i) - - -def init(args): - print('Initializing...') - - web3_url = Config.get_or_none(Config.name == 'web3_url') - if web3_url: - w3 = Web3(Web3.HTTPProvider(web3_url)) - else: - web3_url = input("Enter web3 node url: ") - Config(name='web3_url', value=web3_url).save() - w3 = Web3(Web3.HTTPProvider(web3_url)) - - if not w3.isConnected(): - print('Web3 is not connected') - - if not Config.get_or_none(Config.name == 'priv_key'): - priv_key = input("Enter private key in hex: ") - sender = w3.eth.account.from_key(priv_key) - print(f"Account: {sender.address}") - Config(name='priv_key', value=priv_key).save() - Config(name='sender_addr', value=sender.address).save() - - if not Config.get_or_none(Config.name == 'token_contract'): - token_contract = input("Enter token contract: ") - Config(name='token_contract', value=token_contract).save() +def generate_txes(): + recipients = Recipient.select( + Recipient.id, + Recipient.amount, + Recipient.address + ).where( + Recipient.id.not_in(Tx.select(Tx.recipient_id)) + ).execute() + for recipient in recipients: + Tx.create(nonce=0, recipient_id=recipient.id, status="WAITING") if __name__ == '__main__': - import argparse - parser = argparse.ArgumentParser(description='Make ERC-20 tokens airdrop') - subparsers = parser.add_subparsers() - - parser_init = subparsers.add_parser('init', help='Initialize db') - parser_init.set_defaults(func=init) - - parser_add_recipient = subparsers.add_parser('add', help='Add recipient') - parser_add_recipient.add_argument('to', nargs='?', type=str) - parser_add_recipient.add_argument('amount', nargs='?', type=float) - parser_add_recipient.set_defaults(func=add_recipient) - - parser_show = subparsers.add_parser('show', help='show application stats') - parser_show.set_defaults(func=show) - - parser_prepare = subparsers.add_parser('prepare', help='Prepare txes') - parser_prepare.set_defaults(func=prepare_txes) - - args = parser.parse_args() - args.func(args) + generate_txes() diff --git a/dev_requirements.txt b/dev_requirements.txt new file mode 100644 index 0000000..7338f37 --- /dev/null +++ b/dev_requirements.txt @@ -0,0 +1,8 @@ +-r requirements.txt + +autopep8==1.5.6 +flake8==3.9.1 +mccabe==0.6.1 +pycodestyle==2.7.0 +pyflakes==2.3.1 +toml==0.10.2 diff --git a/enumfield.py b/enumfield.py index c33432e..02969a0 100644 --- a/enumfield.py +++ b/enumfield.py @@ -5,12 +5,13 @@ class EnumField(IntegerField): """ This class enable an Enum like field for Peewee """ + def __init__(self, choices, *args, **kwargs): super(IntegerField, self).__init__(*args, **kwargs) self.choices = choices def db_value(self, value): - return value.value + return value def python_value(self, value): - return self.choices(value) + return self.choices[value] diff --git a/models.py b/models.py new file mode 100644 index 0000000..b4dd126 --- /dev/null +++ b/models.py @@ -0,0 +1,38 @@ +from enum import Enum + +import peewee as pw + +from enumfield import EnumField + +DBFILE = 'db.sqlite' +db = pw.SqliteDatabase(DBFILE) + + +class BaseModel(pw.Model): + class Meta: + database = db + + +class Config(BaseModel): + name = pw.CharField(max_length=255) + value = pw.CharField(max_length=255) + + +class Recipient(BaseModel): + address = pw.CharField(max_length=42) + amount = pw.DecimalField() + + +class Tx(BaseModel): + + choises = { + 'WAITING': 1, + 'READY_TO_SEND': 2, + 'UPLOADING': 3, + 'OCR': 4, + 'DONE': 5, + } + + nonce = pw.IntegerField() + recipient = pw.ForeignKeyField(Recipient, backref='txes') + status = EnumField(choices=choises) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4d66925 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,39 @@ +aiohttp==3.7.4.post0 +async-timeout==3.0.1 +attrs==21.2.0 +base58==2.1.0 +bitarray==1.2.2 +certifi==2021.5.30 +chardet==4.0.0 +cytoolz==0.11.0 +eth-abi==2.1.1 +eth-account==0.5.4 +eth-hash==0.3.1 +eth-keyfile==0.5.1 +eth-keys==0.3.3 +eth-rlp==0.2.1 +eth-typing==2.2.2 +eth-utils==1.10.0 +hexbytes==0.2.1 +idna==2.10 +ipfshttpclient==0.7.0 +jsonschema==3.2.0 +lru-dict==1.1.7 +multiaddr==0.0.9 +multidict==5.1.0 +netaddr==0.8.0 +parsimonious==0.8.1 +peewee==3.14.4 +protobuf==3.17.3 +pycryptodome==3.10.1 +pyrsistent==0.17.3 +requests==2.25.1 +rlp==2.0.1 +six==1.16.0 +toolz==0.11.1 +typing-extensions==3.10.0.0 +urllib3==1.26.5 +varint==1.0.2 +web3==5.20.0 +websockets==8.1 +yarl==1.6.3 From c5129e30b3930ab2832e4fb70ac8769e96b4eaca Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Mon, 5 Jul 2021 19:32:38 +0300 Subject: [PATCH 03/12] Add cli for airdrop and fix models --- airdrop.py | 241 ++++++++++++++++++++++++++++++++++++++++--- dev_requirements.txt | 8 -- enumfield.py | 17 --- models.py | 35 ++++--- tokens_abi/ERC20.abi | 1 + 5 files changed, 249 insertions(+), 53 deletions(-) mode change 100644 => 100755 airdrop.py delete mode 100644 dev_requirements.txt delete mode 100644 enumfield.py create mode 100644 tokens_abi/ERC20.abi diff --git a/airdrop.py b/airdrop.py old mode 100644 new mode 100755 index f47ac20..8ea3fad --- a/airdrop.py +++ b/airdrop.py @@ -1,4 +1,8 @@ -import peewee as pw +import sys +import json +from web3 import Web3 +from ast import literal_eval +from peewee import fn from models import ( db, @@ -7,21 +11,230 @@ Tx, ) -db.connect() -db.create_tables([Config, Recipient, Tx]) +def initialize(): + db.connect() + db.create_tables([Config, Recipient, Tx]) + Config.create(address="0x0000000000000000000000000000000000000000", + private_key="0000000000000000000000000000000000000000000000000000000000000000", + gas_price=10000000000, + web3_node="https://data-seed-prebsc-2-s1.binance.org:8545/") + print('Db was created!') -def generate_txes(): - recipients = Recipient.select( - Recipient.id, - Recipient.amount, - Recipient.address - ).where( - Recipient.id.not_in(Tx.select(Tx.recipient_id)) - ).execute() + +def import_key(): + private_key = input("Paste your private key in hex format: ") + + w3 = Web3(Web3.HTTPProvider(Config.get(1).web3_node, request_kwargs={"timeout": 20})) + address = w3.eth.account.from_key(private_key).address + config = Config.get(1) + config.address = address + config.private_key = private_key + config.save() + print(f'Sender address: {address}') + + +def set_node_address(node_address): + config = Config.get(1) + config.web3_node = node_address + config.save() + print(f'New node address: {node_address}') + + +def update_data(): + config = Config.get(1) + # balances + w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) + config.eth_balance = w3.eth.get_balance(config.address) + with open("tokens_abi/ERC20.abi", "r") as file: + erc20_abi = json.load(file) + token_contract = w3.eth.contract(address=Web3.toChecksumAddress("0x688ce8a97d5f1193261DB2271f542193D1dFd866"), abi=erc20_abi) + config.token_balance = token_contract.functions.balanceOf(config.address).call() + + # nonce + config.current_nonce = w3.eth.get_transaction_count(config.address) + config.save() + + # update tx nonces + txs = Tx.select().where(Tx.status == "SIGNED").order_by(Tx.nonce) + if txs and (txs[0].nonce != config.current_nonce): + nonce = config.current_nonce + for tx in txs: + tx.nonce = nonce + updated_raw_tx = literal_eval(tx.raw_tx) + updated_raw_tx['nonce'] = nonce + tx.raw_tx = updated_raw_tx + tx.signed_tx = w3.eth.account.sign_transaction(updated_raw_tx, config.private_key).rawTransaction + tx.save() + print(tx.raw_tx) + nonce += 1 + print('Balance and nonce have been updated.') + # TODO? update gasprice for signed txs? + + +def show(): + config = Config.get(1) + print(f"Sender address: {config.address}") + print(f"Sender ETH balance: {config.eth_balance} Wei") + print(f"Token balance: {config.token_balance} Wei") + print(f"Sender nonce: {config.current_nonce}") + print(f"Web3 endpoint: {config.web3_node}") + print(f"Gas Price: {config.gas_price}\n") + + print(f"Recipients:") + token_sum = Recipient.select(fn.SUM(Recipient.amount))[0].amount + recipients = Recipient.select() + print("Address Nonce Status Tokens") + for rec in recipients: + print(f"{rec.address} {rec.txes[0].nonce} {rec.txes[0].status} {rec.amount}") + + print("---------------------------------------------------------------") + print(f"Total {len(recipients)} recipients, {token_sum} ERC-20 Tokens.\n") + + +def add_recepient(recipient_address, amount): + if not Web3.isChecksumAddress(recipient_address): + print('Wrong recipient address. Try again.') + return + try: + float(amount) + assert float(amount) * 10**18 >= 1 + except ValueError: + print('Wrong amount') + return + except AssertionError: + print('Too less amount') + return + + recipient = Recipient.create(address=recipient_address, amount=amount) + Tx.create(recipient=recipient, status="NEW") + print(f"{recipient_address} was added with amount {amount}!") + + +def set_gas_price(new_gas_price): + config = Config.get(1) + config.gas_price = new_gas_price + config.save() + print(f'New gas price {new_gas_price}') + + +def sign(): + config = Config.get(1) + token_adr = "0x688ce8a97d5f1193261DB2271f542193D1dFd866" + w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) + with open("tokens_abi/ERC20.abi", "r") as file: + erc20_abi = json.load(file) + token = w3.eth.contract(address=Web3.toChecksumAddress(token_adr), abi=erc20_abi) + nonce = config.current_nonce + + recipients = Recipient.select(Recipient, Tx).join(Tx).where(Tx.status == 'NEW') + import pdb; pdb.set_trace() for recipient in recipients: - Tx.create(nonce=0, recipient_id=recipient.id, status="WAITING") + tx = recipient.txes[0] + raw_tx = token.functions.transfer( + recipient.address, + int(float(recipient.amount) * 10**18), + ).buildTransaction({ + "from": config.address, + "nonce": nonce, + "gasPrice": config.gas_price, + }) + tx.raw_tx = raw_tx + + signed_tx = w3.eth.account.sign_transaction(raw_tx, config.private_key).rawTransaction + tx.signed_tx = signed_tx + tx.nonce = nonce + tx.status = 'SIGNED' + tx.save() + + nonce += 1 + print('TXs have been signed.') + + +def send(): + config = Config.get(1) + w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) + + sending_tx = Tx.select().where(Tx.status == 'SIGNED').order_by(Tx.nonce, Tx.id).first() + try: + tx_hash = w3.eth.send_raw_transaction(sending_tx.signed_tx) + except ValueError: + print("Needs to update nonce. Use command 'update'.") + sending_tx.tx_hash = tx_hash + sending_tx.status = 'SENT' + sending_tx.save() + print(f"Tx with {sending_tx.nonce} nonce was sent. Waiting for receipt...") + + config.current_nonce += 1 + config.save() + + sending_tx.tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + sending_tx.status = "MINED" + sending_tx.save() + print('Tx was successfully mined!') + + +def get_receipt(): + config = Config.get(1) + w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) + + tx = Tx.select().where(Tx.status == "SENT").order_by(Tx.id).first() + tx.tx_hash = w3.eth.wait_for_transaction_receipt(tx.tx_hash) + tx.status = "MINED" + tx.save() + print(f"Tx with nonce {tx.nonce} was successfully mined!") + + +# def delete_recepient(recipient_address): +# recipient = Recipient.delete().where(Recipient.address == recipient_address) +# recipient.execute() +# print(f"Recipient {recipient_address} was deleted.") + + +def help(): + print(""" + init Starts project, calls at once. + import Import admin private key, returns user address. + web3 Specify web3 node address. + update Retrievs latest balances and user nonce. Also updates nonce for 'SIGNED' tx, if necceessary. + show Shows current status. + add
Adds recipient with amount. + gasprice Set new gasprice value for tx. + sign Signs all transactions. + send Sedns first signed tx. + receipt Queries receipt for transaction with status 'SENT' + help Returns this info + """) + +command_dict = { + "init": initialize, + 'import': import_key, + "web3": set_node_address, + "update": update_data, + "show": show, + "add": add_recepient, + "gasprice": set_gas_price, + "sign": sign, + "send": send, + "receipt": get_receipt, + # "delete": delete_recepient, + "help": help, +} + + +if len(sys.argv) < 2: + print("Enter one of commands: init, import, web3, update, show, add...)") +else: + # parse command + try: + command = sys.argv[1] + args = sys.argv[2:] + except IndexError: + print('Invalid command. Try again.') + # execute command + try: + command_dict[command](*args) + except TypeError: + print(f'Please specify all argument(s) for command "{command}"') -if __name__ == '__main__': - generate_txes() diff --git a/dev_requirements.txt b/dev_requirements.txt deleted file mode 100644 index 7338f37..0000000 --- a/dev_requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ --r requirements.txt - -autopep8==1.5.6 -flake8==3.9.1 -mccabe==0.6.1 -pycodestyle==2.7.0 -pyflakes==2.3.1 -toml==0.10.2 diff --git a/enumfield.py b/enumfield.py deleted file mode 100644 index 02969a0..0000000 --- a/enumfield.py +++ /dev/null @@ -1,17 +0,0 @@ -from peewee import IntegerField - - -class EnumField(IntegerField): - """ - This class enable an Enum like field for Peewee - """ - - def __init__(self, choices, *args, **kwargs): - super(IntegerField, self).__init__(*args, **kwargs) - self.choices = choices - - def db_value(self, value): - return value - - def python_value(self, value): - return self.choices[value] diff --git a/models.py b/models.py index b4dd126..4ef92f7 100644 --- a/models.py +++ b/models.py @@ -1,8 +1,5 @@ -from enum import Enum - import peewee as pw -from enumfield import EnumField DBFILE = 'db.sqlite' db = pw.SqliteDatabase(DBFILE) @@ -14,8 +11,14 @@ class Meta: class Config(BaseModel): - name = pw.CharField(max_length=255) - value = pw.CharField(max_length=255) + address = pw.CharField(max_length=42) + private_key = pw.CharField(max_length=64) + gas_price = pw.IntegerField() + web3_node = pw.CharField(max_length=255) + + current_nonce = pw.IntegerField(default=0) + eth_balance = pw.CharField(default=0) + token_balance = pw.CharField(default=0) class Recipient(BaseModel): @@ -25,14 +28,18 @@ class Recipient(BaseModel): class Tx(BaseModel): - choises = { - 'WAITING': 1, - 'READY_TO_SEND': 2, - 'UPLOADING': 3, - 'OCR': 4, - 'DONE': 5, - } + choices = ( + ('NEW', 'NEW'), + ('SIGNED', 'SIGNED'), + ('SENT', 'SENT'), + ('MINED', 'MINED'), + ) + + raw_tx = pw.TextField(default="") + signed_tx = pw.BlobField(default="") + tx_hash = pw.BlobField(default="") + tx_receipt = pw.TextField(default="") - nonce = pw.IntegerField() + nonce = pw.IntegerField(null=True) recipient = pw.ForeignKeyField(Recipient, backref='txes') - status = EnumField(choices=choises) + status = pw.CharField(choices=choices) diff --git a/tokens_abi/ERC20.abi b/tokens_abi/ERC20.abi new file mode 100644 index 0000000..13e246c --- /dev/null +++ b/tokens_abi/ERC20.abi @@ -0,0 +1 @@ +[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}] \ No newline at end of file From 54b705689c84cb5480b848bc49114a7c40837df0 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Wed, 7 Jul 2021 12:56:37 +0300 Subject: [PATCH 04/12] Add printing pretty table in show command --- airdrop.py | 17 +++++++++++------ pretty_table.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 pretty_table.py diff --git a/airdrop.py b/airdrop.py index 8ea3fad..e051de4 100755 --- a/airdrop.py +++ b/airdrop.py @@ -11,6 +11,8 @@ Tx, ) +from pretty_table import print_pretty_table + def initialize(): db.connect() @@ -84,12 +86,16 @@ def show(): print(f"Recipients:") token_sum = Recipient.select(fn.SUM(Recipient.amount))[0].amount recipients = Recipient.select() - print("Address Nonce Status Tokens") - for rec in recipients: - print(f"{rec.address} {rec.txes[0].nonce} {rec.txes[0].status} {rec.amount}") - print("---------------------------------------------------------------") - print(f"Total {len(recipients)} recipients, {token_sum} ERC-20 Tokens.\n") + header = [['Address', 'Tokens', 'Nonce', 'Status']] + values = [ + [rec.address, str(rec.amount), str(rec.txes[0].nonce), rec.txes[0].status] for rec in recipients + ] + data_to_print = header + values + print_pretty_table(data_to_print) + + print(f"\nTotal {len(recipients)} recipients, {token_sum} ERC-20 Tokens.\n") + def add_recepient(recipient_address, amount): @@ -128,7 +134,6 @@ def sign(): nonce = config.current_nonce recipients = Recipient.select(Recipient, Tx).join(Tx).where(Tx.status == 'NEW') - import pdb; pdb.set_trace() for recipient in recipients: tx = recipient.txes[0] raw_tx = token.functions.transfer( diff --git a/pretty_table.py b/pretty_table.py new file mode 100644 index 0000000..b7e6bfa --- /dev/null +++ b/pretty_table.py @@ -0,0 +1,21 @@ +def print_pretty_table(data, cell_sep=' | ', header_separator=True): + rows = len(data) + cols = len(data[0]) + + col_width = [] + for col in range(cols): + columns = [data[row][col] for row in range(rows)] + col_width.append(len(max(columns, key=len))) + + separator = "-+-".join('-' * n for n in col_width) + + for i, row in enumerate(range(rows)): + if i == 1 and header_separator: + print(separator) + + result = [] + for col in range(cols): + item = data[row][col].ljust(col_width[col]) + result.append(item) + + print(cell_sep.join(result)) From 1f2b4d43a7716b3af85355add86711fd96e60c88 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Wed, 7 Jul 2021 13:28:30 +0300 Subject: [PATCH 05/12] Add python interpreter definition at top --- airdrop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/airdrop.py b/airdrop.py index e051de4..4a6c600 100755 --- a/airdrop.py +++ b/airdrop.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + import sys import json from web3 import Web3 From 25a022fe25e1643b2f74ce7502269fb0c9be491c Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Wed, 7 Jul 2021 16:21:44 +0300 Subject: [PATCH 06/12] Add exception hadling --- airdrop.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/airdrop.py b/airdrop.py index 4a6c600..29dfb3e 100755 --- a/airdrop.py +++ b/airdrop.py @@ -5,6 +5,7 @@ from web3 import Web3 from ast import literal_eval from peewee import fn +import requests from models import ( db, @@ -39,6 +40,17 @@ def import_key(): def set_node_address(node_address): + try: + assert requests.get(node_address).status_code == 200 + except requests.exceptions.MissingSchema: + print(f"Wrong node URL. Perhaps you meant http://{node_address}?") + return + except requests.exceptions.ConnectionError: + print('Failed to establish a new connection. Try again or change URL.') + return + except AssertionError: + print('Wrong node URL. Try again.') + return config = Config.get(1) config.web3_node = node_address config.save() @@ -120,6 +132,11 @@ def add_recepient(recipient_address, amount): def set_gas_price(new_gas_price): + try: + int(new_gas_price) + except ValueError: + print("Wrong gas value. Try again") + return config = Config.get(1) config.gas_price = new_gas_price config.save() @@ -230,18 +247,19 @@ def help(): if len(sys.argv) < 2: - print("Enter one of commands: init, import, web3, update, show, add...)") + print("Command reqired. Input 'help' for additional info.") else: # parse command try: command = sys.argv[1] args = sys.argv[2:] except IndexError: - print('Invalid command. Try again.') + print("Invalid command. Try again or input 'help' for help.") # execute command try: command_dict[command](*args) except TypeError: - print(f'Please specify all argument(s) for command "{command}"') - + print(f'Please specify all neccessary argument(s) for command "{command}"') + except KeyError: + print("Invalid command. Try again or input 'help' for help.") From 286bff208506992c9adee4d30d4aefead4284e75 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Wed, 7 Jul 2021 17:10:11 +0300 Subject: [PATCH 07/12] Delete unused 'delete' method --- airdrop.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/airdrop.py b/airdrop.py index 29dfb3e..6a5baf5 100755 --- a/airdrop.py +++ b/airdrop.py @@ -82,10 +82,8 @@ def update_data(): tx.raw_tx = updated_raw_tx tx.signed_tx = w3.eth.account.sign_transaction(updated_raw_tx, config.private_key).rawTransaction tx.save() - print(tx.raw_tx) nonce += 1 print('Balance and nonce have been updated.') - # TODO? update gasprice for signed txs? def show(): @@ -209,11 +207,6 @@ def get_receipt(): print(f"Tx with nonce {tx.nonce} was successfully mined!") -# def delete_recepient(recipient_address): -# recipient = Recipient.delete().where(Recipient.address == recipient_address) -# recipient.execute() -# print(f"Recipient {recipient_address} was deleted.") - def help(): print(""" @@ -241,7 +234,6 @@ def help(): "sign": sign, "send": send, "receipt": get_receipt, - # "delete": delete_recepient, "help": help, } From ceed440807de2cd348064455a0a98cf936beadd0 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Wed, 7 Jul 2021 18:04:55 +0300 Subject: [PATCH 08/12] Add token definition, fix send exceptions --- airdrop.py | 34 ++++++++++++++++++++++++++++------ models.py | 1 + 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/airdrop.py b/airdrop.py index 6a5baf5..6de84a7 100755 --- a/airdrop.py +++ b/airdrop.py @@ -23,7 +23,8 @@ def initialize(): Config.create(address="0x0000000000000000000000000000000000000000", private_key="0000000000000000000000000000000000000000000000000000000000000000", gas_price=10000000000, - web3_node="https://data-seed-prebsc-2-s1.binance.org:8545/") + web3_node="https://data-seed-prebsc-2-s1.binance.org:8545/", + token="0x0000000000000000000000000000000000000000") print('Db was created!') @@ -39,6 +40,17 @@ def import_key(): print(f'Sender address: {address}') +def set_token(token_address): + if not Web3.isChecksumAddress(token_address): + print('Wrong token address. Try again.') + return + + config = Config.get(1) + config.token = token_address + config.save() + print(f"Now token for airdrop is {token_address}") + + def set_node_address(node_address): try: assert requests.get(node_address).status_code == 200 @@ -64,7 +76,7 @@ def update_data(): config.eth_balance = w3.eth.get_balance(config.address) with open("tokens_abi/ERC20.abi", "r") as file: erc20_abi = json.load(file) - token_contract = w3.eth.contract(address=Web3.toChecksumAddress("0x688ce8a97d5f1193261DB2271f542193D1dFd866"), abi=erc20_abi) + token_contract = w3.eth.contract(address=Web3.toChecksumAddress(config.token), abi=erc20_abi) config.token_balance = token_contract.functions.balanceOf(config.address).call() # nonce @@ -90,6 +102,7 @@ def show(): config = Config.get(1) print(f"Sender address: {config.address}") print(f"Sender ETH balance: {config.eth_balance} Wei") + print(f"Token address: {config.token}") print(f"Token balance: {config.token_balance} Wei") print(f"Sender nonce: {config.current_nonce}") print(f"Web3 endpoint: {config.web3_node}") @@ -99,9 +112,13 @@ def show(): token_sum = Recipient.select(fn.SUM(Recipient.amount))[0].amount recipients = Recipient.select() - header = [['Address', 'Tokens', 'Nonce', 'Status']] + header = [['Address', 'Tokens', 'Nonce', 'Status', 'Tx Hash']] values = [ - [rec.address, str(rec.amount), str(rec.txes[0].nonce), rec.txes[0].status] for rec in recipients + [rec.address, + str(rec.amount), + str(rec.txes[0].nonce), + rec.txes[0].status, + rec.txes[0].tx_hash.hex()] for rec in recipients ] data_to_print = header + values print_pretty_table(data_to_print) @@ -143,11 +160,10 @@ def set_gas_price(new_gas_price): def sign(): config = Config.get(1) - token_adr = "0x688ce8a97d5f1193261DB2271f542193D1dFd866" w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) with open("tokens_abi/ERC20.abi", "r") as file: erc20_abi = json.load(file) - token = w3.eth.contract(address=Web3.toChecksumAddress(token_adr), abi=erc20_abi) + token = w3.eth.contract(address=Web3.toChecksumAddress(config.token), abi=erc20_abi) nonce = config.current_nonce recipients = Recipient.select(Recipient, Tx).join(Tx).where(Tx.status == 'NEW') @@ -182,6 +198,10 @@ def send(): tx_hash = w3.eth.send_raw_transaction(sending_tx.signed_tx) except ValueError: print("Needs to update nonce. Use command 'update'.") + return + except AttributeError: + print("Nothing to send.") + return sending_tx.tx_hash = tx_hash sending_tx.status = 'SENT' sending_tx.save() @@ -212,6 +232,7 @@ def help(): print(""" init Starts project, calls at once. import Import admin private key, returns user address. + token
Set token address for airdrop. web3 Specify web3 node address. update Retrievs latest balances and user nonce. Also updates nonce for 'SIGNED' tx, if necceessary. show Shows current status. @@ -226,6 +247,7 @@ def help(): command_dict = { "init": initialize, 'import': import_key, + 'token': set_token, "web3": set_node_address, "update": update_data, "show": show, diff --git a/models.py b/models.py index 4ef92f7..1a58349 100644 --- a/models.py +++ b/models.py @@ -15,6 +15,7 @@ class Config(BaseModel): private_key = pw.CharField(max_length=64) gas_price = pw.IntegerField() web3_node = pw.CharField(max_length=255) + token = pw.CharField(max_length=42) current_nonce = pw.IntegerField(default=0) eth_balance = pw.CharField(default=0) From 73adac5ef9061a62c983a52e388015139b9ae407 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Wed, 7 Jul 2021 18:55:14 +0300 Subject: [PATCH 09/12] Fix getting tx receipt --- airdrop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airdrop.py b/airdrop.py index 6de84a7..43c1cdd 100755 --- a/airdrop.py +++ b/airdrop.py @@ -221,7 +221,7 @@ def get_receipt(): w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) tx = Tx.select().where(Tx.status == "SENT").order_by(Tx.id).first() - tx.tx_hash = w3.eth.wait_for_transaction_receipt(tx.tx_hash) + tx.tx_receipt= w3.eth.wait_for_transaction_receipt(tx.tx_hash) tx.status = "MINED" tx.save() print(f"Tx with nonce {tx.nonce} was successfully mined!") From 7a79f429dcb7d1168d369aed602b36598ba34ae0 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Thu, 8 Jul 2021 19:27:54 +0300 Subject: [PATCH 10/12] Fix exception handling, update moodel field --- airdrop.py | 154 ++++++++++++++++++++++++++++++++--------------------- models.py | 2 +- 2 files changed, 94 insertions(+), 62 deletions(-) diff --git a/airdrop.py b/airdrop.py index 43c1cdd..3d5910a 100755 --- a/airdrop.py +++ b/airdrop.py @@ -3,9 +3,9 @@ import sys import json from web3 import Web3 -from ast import literal_eval +from web3.exceptions import ContractLogicError, BadFunctionCallOutput from peewee import fn -import requests +from peewee import OperationalError from models import ( db, @@ -17,10 +17,20 @@ from pretty_table import print_pretty_table +def send_raw_tx(web3_endpoint, signed_tx): + w3 = Web3(Web3.HTTPProvider(web3_endpoint, request_kwargs={"timeout": 20})) + return w3.eth.send_raw_transaction(signed_tx) + + +def get_tx_receipt(web3_endpoint, tx_hash): + w3 = Web3(Web3.HTTPProvider(web3_endpoint, request_kwargs={"timeout": 20})) + return w3.eth.wait_for_transaction_receipt(tx_hash) + + def initialize(): db.connect() db.create_tables([Config, Recipient, Tx]) - Config.create(address="0x0000000000000000000000000000000000000000", + Config.create(address="0x0000000000000000000000000000000000000000", private_key="0000000000000000000000000000000000000000000000000000000000000000", gas_price=10000000000, web3_node="https://data-seed-prebsc-2-s1.binance.org:8545/", @@ -30,7 +40,7 @@ def initialize(): def import_key(): private_key = input("Paste your private key in hex format: ") - + w3 = Web3(Web3.HTTPProvider(Config.get(1).web3_node, request_kwargs={"timeout": 20})) address = w3.eth.account.from_key(private_key).address config = Config.get(1) @@ -44,7 +54,7 @@ def set_token(token_address): if not Web3.isChecksumAddress(token_address): print('Wrong token address. Try again.') return - + config = Config.get(1) config.token = token_address config.save() @@ -53,15 +63,10 @@ def set_token(token_address): def set_node_address(node_address): try: - assert requests.get(node_address).status_code == 200 - except requests.exceptions.MissingSchema: - print(f"Wrong node URL. Perhaps you meant http://{node_address}?") - return - except requests.exceptions.ConnectionError: - print('Failed to establish a new connection. Try again or change URL.') - return + w3 = Web3(Web3.HTTPProvider(node_address, request_kwargs={"timeout": 20})) + assert w3.isConnected() == True except AssertionError: - print('Wrong node URL. Try again.') + print('Wrong node URL or connection error. Try again.') return config = Config.get(1) config.web3_node = node_address @@ -76,23 +81,29 @@ def update_data(): config.eth_balance = w3.eth.get_balance(config.address) with open("tokens_abi/ERC20.abi", "r") as file: erc20_abi = json.load(file) + token_contract = w3.eth.contract(address=Web3.toChecksumAddress(config.token), abi=erc20_abi) - config.token_balance = token_contract.functions.balanceOf(config.address).call() - + try: + config.token_balance = token_contract.functions.balanceOf(config.address).call() + except BadFunctionCallOutput: + print("Error. Can't update token address. Check token address.") + return + # nonce config.current_nonce = w3.eth.get_transaction_count(config.address) config.save() - + # update tx nonces txs = Tx.select().where(Tx.status == "SIGNED").order_by(Tx.nonce) if txs and (txs[0].nonce != config.current_nonce): nonce = config.current_nonce for tx in txs: tx.nonce = nonce - updated_raw_tx = literal_eval(tx.raw_tx) + updated_raw_tx = json.loads(tx.raw_tx.replace("\'", "\"")) updated_raw_tx['nonce'] = nonce tx.raw_tx = updated_raw_tx - tx.signed_tx = w3.eth.account.sign_transaction(updated_raw_tx, config.private_key).rawTransaction + tx.signed_tx = w3.eth.account.sign_transaction(updated_raw_tx, + config.private_key).rawTransaction tx.save() nonce += 1 print('Balance and nonce have been updated.') @@ -107,16 +118,16 @@ def show(): print(f"Sender nonce: {config.current_nonce}") print(f"Web3 endpoint: {config.web3_node}") print(f"Gas Price: {config.gas_price}\n") - + print(f"Recipients:") token_sum = Recipient.select(fn.SUM(Recipient.amount))[0].amount recipients = Recipient.select() - + header = [['Address', 'Tokens', 'Nonce', 'Status', 'Tx Hash']] values = [ - [rec.address, - str(rec.amount), - str(rec.txes[0].nonce), + [rec.address, + str(rec.amount), + str(rec.txes[0].nonce), rec.txes[0].status, rec.txes[0].tx_hash.hex()] for rec in recipients ] @@ -124,7 +135,6 @@ def show(): print_pretty_table(data_to_print) print(f"\nTotal {len(recipients)} recipients, {token_sum} ERC-20 Tokens.\n") - def add_recepient(recipient_address, amount): @@ -144,7 +154,7 @@ def add_recepient(recipient_address, amount): recipient = Recipient.create(address=recipient_address, amount=amount) Tx.create(recipient=recipient, status="NEW") print(f"{recipient_address} was added with amount {amount}!") - + def set_gas_price(new_gas_price): try: @@ -165,26 +175,30 @@ def sign(): erc20_abi = json.load(file) token = w3.eth.contract(address=Web3.toChecksumAddress(config.token), abi=erc20_abi) nonce = config.current_nonce - + recipients = Recipient.select(Recipient, Tx).join(Tx).where(Tx.status == 'NEW') for recipient in recipients: tx = recipient.txes[0] - raw_tx = token.functions.transfer( - recipient.address, - int(float(recipient.amount) * 10**18), - ).buildTransaction({ - "from": config.address, - "nonce": nonce, - "gasPrice": config.gas_price, - }) + try: + raw_tx = token.functions.transfer( + recipient.address, + int(float(recipient.amount) * 10**18), + ).buildTransaction({ + "from": config.address, + "nonce": nonce, + "gasPrice": config.gas_price, + }) + except ValueError: + print('Not enough ETH or Token balance. Fill your balance and try again.') + return tx.raw_tx = raw_tx - + signed_tx = w3.eth.account.sign_transaction(raw_tx, config.private_key).rawTransaction tx.signed_tx = signed_tx tx.nonce = nonce tx.status = 'SIGNED' tx.save() - + nonce += 1 print('TXs have been signed.') @@ -194,23 +208,37 @@ def send(): w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) sending_tx = Tx.select().where(Tx.status == 'SIGNED').order_by(Tx.nonce, Tx.id).first() + try: - tx_hash = w3.eth.send_raw_transaction(sending_tx.signed_tx) - except ValueError: - print("Needs to update nonce. Use command 'update'.") + w3.eth.call(json.loads(sending_tx.raw_tx.replace("\'", "\""))) + except ContractLogicError: + print("""Contract logic error (probably insufficient token balance). Sending aborted.""") return except AttributeError: print("Nothing to send.") return + + try: + tx_hash = send_raw_tx(config.web3_node, sending_tx.signed_tx) + except ValueError as e: + if str(e) == "{'code': -32000, 'message': 'insufficient funds for gas * price + value'}": + print('Not enough ETH Balance. Fill your balance and try again.') + elif str(e) == "{'code': -32000, 'message': 'nonce too low'}": + print("Needs to update account nonce. Use command 'update'.") + return + except AttributeError as e: + print(e) + print("Nothing to send.") + return sending_tx.tx_hash = tx_hash sending_tx.status = 'SENT' sending_tx.save() print(f"Tx with {sending_tx.nonce} nonce was sent. Waiting for receipt...") - + config.current_nonce += 1 config.save() - sending_tx.tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + sending_tx.tx_receipt = get_tx_receipt(config.web3_node, tx_hash) sending_tx.status = "MINED" sending_tx.save() print('Tx was successfully mined!') @@ -219,7 +247,7 @@ def send(): def get_receipt(): config = Config.get(1) w3 = Web3(Web3.HTTPProvider(config.web3_node, request_kwargs={"timeout": 20})) - + tx = Tx.select().where(Tx.status == "SENT").order_by(Tx.id).first() tx.tx_receipt= w3.eth.wait_for_transaction_receipt(tx.tx_hash) tx.status = "MINED" @@ -227,7 +255,6 @@ def get_receipt(): print(f"Tx with nonce {tx.nonce} was successfully mined!") - def help(): print(""" init Starts project, calls at once. @@ -237,13 +264,14 @@ def help(): update Retrievs latest balances and user nonce. Also updates nonce for 'SIGNED' tx, if necceessary. show Shows current status. add
Adds recipient with amount. - gasprice Set new gasprice value for tx. + gasprice Set new gasprice value for tx in Wei. sign Signs all transactions. send Sedns first signed tx. receipt Queries receipt for transaction with status 'SENT' help Returns this info """) + command_dict = { "init": initialize, 'import': import_key, @@ -260,20 +288,24 @@ def help(): } -if len(sys.argv) < 2: - print("Command reqired. Input 'help' for additional info.") -else: - # parse command - try: - command = sys.argv[1] - args = sys.argv[2:] - except IndexError: - print("Invalid command. Try again or input 'help' for help.") - - # execute command - try: - command_dict[command](*args) - except TypeError: - print(f'Please specify all neccessary argument(s) for command "{command}"') - except KeyError: - print("Invalid command. Try again or input 'help' for help.") +if __name__ == "__main__": + + if len(sys.argv) < 2: + print("Command reqired. Input 'help' for additional info.") + else: + # parse command + try: + command = sys.argv[1] + args = sys.argv[2:] + except IndexError: + print("Invalid command. Try again or input 'help' for help.") + + # execute command + try: + command_dict[command](*args) + except TypeError: + print(f'Please specify all neccessary argument(s) for command "{command}"') + except KeyError: + print("Invalid command. Try again or input 'help' for help.") + except OperationalError: + print("Invalid command. Try again or input 'help' for help.") diff --git a/models.py b/models.py index 1a58349..9bb89ed 100644 --- a/models.py +++ b/models.py @@ -24,7 +24,7 @@ class Config(BaseModel): class Recipient(BaseModel): address = pw.CharField(max_length=42) - amount = pw.DecimalField() + amount = pw.DecimalField(max_digits=36, decimal_places=18) class Tx(BaseModel): From 0d5e46b0e6a4ec4de52e9c4bb5b759dfafb8c359 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Thu, 8 Jul 2021 19:30:22 +0300 Subject: [PATCH 11/12] Add tests and CI --- .github/workflows/ci_airdrop.yml | 28 +++ test_airdrop.py | 386 +++++++++++++++++++++++++++++-- 2 files changed, 392 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/ci_airdrop.yml diff --git a/.github/workflows/ci_airdrop.yml b/.github/workflows/ci_airdrop.yml new file mode 100644 index 0000000..b5245f5 --- /dev/null +++ b/.github/workflows/ci_airdrop.yml @@ -0,0 +1,28 @@ +name: airdrop_pytest + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run tests with pytest + run: | + pip install pytest + pytest diff --git a/test_airdrop.py b/test_airdrop.py index 7515fef..bd43a40 100644 --- a/test_airdrop.py +++ b/test_airdrop.py @@ -1,23 +1,365 @@ +import json +import peewee as pw import airdrop as ad -from decimal import Decimal - - -def test_add_recepient(): - ad.Recipient.delete().execute() - ad.add_recipient({"address": "bla", "amount": Decimal('12.34')}) - ad.add_recipient({"address": "bla", "amount": Decimal('234.56')}) - assert ad.Recipient.get_by_id(1).address == "bla" - assert ad.Recipient.get_by_id(1).amount == Decimal('12.34') - assert ad.Recipient.get_by_id(2).address == "bla" - assert ad.Recipient.get_by_id(2).amount == Decimal('234.56') - - -def test_prepare_txes(): - ad.Recipient.delete().execute() - rcpt1 = ad.Recipient.create(address="weret", amount=Decimal('234.56')) - rcpt2 = ad.Recipient.create(address="wereg", amount=Decimal('234.56')) - rcpt3 = ad.Recipient.create(address="wereu", amount=Decimal('234.56')) - ad.Tx.create(recipient=rcpt1, nonce=1, status=ad.Tx.TxStatus.OCR) - ad.Tx.create(recipient=rcpt2, nonce=1, status=ad.Tx.TxStatus.OCR) - ad.Tx.create(recipient=rcpt3, nonce=1, status=ad.Tx.TxStatus.OCR) - ad.prepare_txes({}) +from models import BaseModel, Config, Recipient, Tx + + +# Note. +# All of addresses are valid the for binance testnet (97). +# But no one transaction will happend due to using pytest monkeypatch in +# the methods send (tranfer) and get receipts. + + +DBFILE = "db_test.sqlite" + + +def delete_tables(): + BaseModel._meta.database.init(DBFILE) + Config.drop_table() + Recipient.drop_table() + Tx.drop_table() + + +def test_create_empty_db(): + BaseModel._meta.database.init(DBFILE) + db = pw.SqliteDatabase(DBFILE) + db.connect() + db.create_tables([Config, Recipient, Tx]) + + assert db.table_exists('config') + assert db.table_exists('recipient') + assert db.table_exists('tx') + assert db.get_tables() == ['config', 'recipient', 'tx'] + assert len(Config.select()) == 0 + assert len(Recipient.select()) == 0 + assert len(Tx.select()) == 0 + + delete_tables() + db.close() + + +def test_init_command(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + + ad.initialize() + assert db.get_tables() == ['config', 'recipient', 'tx'] + + config = Config.get(1) + assert config.address == "0x0000000000000000000000000000000000000000" + assert config.private_key == "0000000000000000000000000000000000000000000000000000000000000000" + assert config.gas_price == 10000000000 + assert config.web3_node == "https://data-seed-prebsc-2-s1.binance.org:8545/" + assert config.token == "0x0000000000000000000000000000000000000000" + assert config.current_nonce == 0 + assert config.eth_balance == '0' + assert config.token_balance == '0' + delete_tables() + db.close() + + +def test_import_command(monkeypatch): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + ad.initialize() + + monkeypatch.setattr('builtins.input', + lambda _: "a181ad022696f68244129bc35559d9fe28005d5289fca5961d3ce91dc29d13b3") + ad.import_key() + + config = Config.get(1) + assert config.address == "0xB0718e1085E1E34537ff9fdAeeC5Ec1AfFe1872c" + assert config.private_key == "a181ad022696f68244129bc35559d9fe28005d5289fca5961d3ce91dc29d13b3" + + # check again + assert config.gas_price == 10000000000 + assert config.web3_node == "https://data-seed-prebsc-2-s1.binance.org:8545/" + assert config.token == "0x0000000000000000000000000000000000000000" + assert config.current_nonce == 0 + assert config.eth_balance == '0' + assert config.token_balance == '0' + + db.close() + + +def test_token_command(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + ad.set_token("0x688ce8a97d5f1193261DB2271f542193D1dFd866") + + config = Config.get(1) + assert config.token == "0x688ce8a97d5f1193261DB2271f542193D1dFd866" + + db.close() + + +def test_token_command_with_wrong_address(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + token_before = Config.get(1).token + + ad.set_token("0x688ce8a97d5f1193261DB2271f542193D83742323234ac1dFd86693849") + assert Config.get(1).token == token_before + + ad.set_token("0xce8a97d5f1193261D") + assert Config.get(1).token == token_before + + ad.set_token("12e8a97d5f1193261D") + assert Config.get(1).token == token_before + + ad.set_token(12345678901234567890) + assert Config.get(1).token == token_before + + db.close() + + +def test_web3_command(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + ad.set_node_address("https://data-seed-prebsc-1-s3.binance.org:8545/") + + config = Config.get(1) + assert config.web3_node == "https://data-seed-prebsc-1-s3.binance.org:8545/" + + db.close() + + +def test_web3_command_dont_change_to_wrong_address(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + wen3_before = Config.get(1).web3_node + + ad.set_node_address("https://") + assert Config.get(1).web3_node == wen3_before + + ad.set_node_address("https://google.com/") + assert Config.get(1).web3_node == wen3_before + + ad.set_node_address("abcdefg") + assert Config.get(1).web3_node == wen3_before + + db.close() + + +def test_update_command(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + config = Config.get(1) + + # before update + assert config.current_nonce == 0 + assert int(config.eth_balance) == 0 + assert int(config.token_balance) == 0 + assert config.token == "0x688ce8a97d5f1193261DB2271f542193D1dFd866" + assert config.address == "0xB0718e1085E1E34537ff9fdAeeC5Ec1AfFe1872c" + assert config.private_key == "a181ad022696f68244129bc35559d9fe28005d5289fca5961d3ce91dc29d13b3" + + # updated + ad.update_data() + config = Config.get(1) + assert config.current_nonce > 0 + assert int(config.eth_balance) > 0 + assert int(config.token_balance) > 0 + + db.close() + + +def test_gasprice_command(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + # In 'init' project's default gasprice is 10000000000 + assert Config.get(1).gas_price == 10000000000 + + # Let's switch gas price to 20000000002 + ad.set_gas_price(20000000002) + assert Config.get(1).gas_price == 20000000002 + + # Let's switch gas price to 30000000003 + ad.set_gas_price("30000000003") + assert Config.get(1).gas_price == 30000000003 + + # Switching back + ad.set_gas_price(10000000000) + assert Config.get(1).gas_price == 10000000000 + + db.close() + + +def test_gasprice_command_with_wrong_value(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + gas_before = Config.get(1).gas_price + + ad.set_gas_price(" wrong value ") + assert Config.get(1).gas_price == gas_before + + ad.set_gas_price("3434340430d") + assert Config.get(1).gas_price == gas_before + + ad.set_gas_price("0.0000001") + assert Config.get(1).gas_price == gas_before + + db.close() + + +def test_add_command(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + adr1, amount1 = ("0x754a2bAe5b5eEE723409A1d0013377927Fd5F539", 1.234567890123456789) + adr2, amount2 = ("0x754a2bAe5b5eEE723409A1d0013377927Fd5F539", 0.123456789012345678) + adr3, amount3 = ("0x754a2bAe5b5eEE723409A1d0013377927Fd5F539", 0.012345678901234567) + + # before + assert len(Recipient.select()) == 0 + assert len(Tx.select()) == 0 + + # add one + ad.add_recepient(adr1, amount1) + assert len(Recipient.select()) == 1 + recipient1 = Recipient.get(1) + assert recipient1.address == adr1 + assert float(recipient1.amount) == amount1 + + assert len(Tx.select()) == 1 + tx1 = Tx.get(1) + assert tx1.raw_tx == "" + assert tx1.signed_tx == b'' + assert tx1.tx_hash == b'' + assert tx1.tx_receipt == "" + assert tx1.nonce == None + assert tx1.recipient.address == "0x754a2bAe5b5eEE723409A1d0013377927Fd5F539" + assert tx1.status == 'NEW' + + + # add two more + ad.add_recepient(adr2, amount2) + ad.add_recepient(adr3, amount3) + assert len(Recipient.select()) == 3 + recipient2 = Recipient.get(2) + recipient3 = Recipient.get(3) + assert recipient2.address == adr2 + assert float(recipient2.amount) == amount2 + assert recipient3.address == adr3 + assert float(recipient3.amount) == amount3 + + assert len(Tx.select()) == 3 + assert Tx.get(2).status == 'NEW' + assert Tx.get(3).status == 'NEW' + assert bool(Tx.get(2).signed_tx) == False + + db.close() + + +def test_sign_command(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + nonce_before = Config.get(1).current_nonce + ad.sign() + + tx1, tx2, tx3 = Tx.select() + assert tx1.status == 'SIGNED' + assert tx2.status == 'SIGNED' + assert tx3.status == 'SIGNED' + assert tx1.nonce == nonce_before + assert tx2.nonce == nonce_before + 1 + assert tx3.nonce == nonce_before + 2 + assert json.loads(tx1.raw_tx.replace("\'", "\""))['from'] == Config.get(1).address + assert json.loads(tx2.raw_tx.replace("\'", "\""))['from'] == Config.get(1).address + assert json.loads(tx3.raw_tx.replace("\'", "\""))['from'] == Config.get(1).address + assert json.loads(tx1.raw_tx.replace("\'", "\""))['to'] == Config.get(1).token + assert json.loads(tx2.raw_tx.replace("\'", "\""))['to'] == Config.get(1).token + assert json.loads(tx3.raw_tx.replace("\'", "\""))['to'] == Config.get(1).token + assert bool(json.loads(tx1.raw_tx.replace("\'", "\""))['data']) == True + assert bool(json.loads(tx2.raw_tx.replace("\'", "\""))['data']) == True + assert bool(json.loads(tx3.raw_tx.replace("\'", "\""))['data']) == True + assert bool(tx1.signed_tx) == True + assert bool(tx2.signed_tx) == True + assert bool(tx3.signed_tx) == True + + db.close() + + +def test_send_command(monkeypatch): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + db.connect() + + monkeypatch.setattr('airdrop.send_raw_tx', + lambda _, __: bytes.fromhex('593ebcf5700420b1')) + monkeypatch.setattr('airdrop.get_tx_receipt', + lambda _, __: {'blockNumber': 100, 'status': 1}) + ad.send() + + tx = Tx.get(1) + assert tx.tx_hash == bytes.fromhex('593ebcf5700420b1') + assert tx.status == 'MINED' + assert tx.tx_receipt == "{'blockNumber': 100, 'status': 1}" + + db.close() + + # db cleanup + delete_tables() + + +def test_dont_update_without_token(monkeypatch): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + + ad.initialize() + monkeypatch.setattr('builtins.input', + lambda _: "a181ad022696f68244129bc35559d9fe28005d5289fca5961d3ce91dc29d13b3") + ad.import_key() + + nonce_before = Config.get(1).current_nonce + token_balance_before = Config.get(1).token_balance + + ad.update_data() + assert nonce_before == Config.get(1).current_nonce + assert token_balance_before == Config.get(1).token_balance + + db.close() + delete_tables() + + +def test_add_with_wrong_inputs_dont_adds_data_to_db(): + db = pw.SqliteDatabase(DBFILE) + BaseModel._meta.database.init(DBFILE) + + ad.initialize() + + adr1, amount1 = ("0x0000000000000000000000000000hd1100000000", 1.2345) + adr2, amount2 = ("0x754a2bAe5b5eEE723409A1d0013377927Fd5F539", "") + adr3, amount3 = ("0x754a2bAe5b5eEE723409A1d0013377927Fd5F539", "0.00000000000000000001") + + # before + assert len(Recipient.select()) == 0 + assert len(Tx.select()) == 0 + + ad.add_recepient(adr1, amount1) + ad.add_recepient(adr2, amount2) + ad.add_recepient(adr3, amount3) + + # after + assert len(Recipient.select()) == 0 + assert len(Tx.select()) == 0 + + db.close() + delete_tables() From 7e874121a48e25302f324d6c28a6a4e983ba7231 Mon Sep 17 00:00:00 2001 From: Maxim Korotkikh Date: Wed, 7 Jul 2021 18:25:47 +0300 Subject: [PATCH 12/12] Add Readme file --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f19d0e..70d7223 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,59 @@ +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![Build Status - GitHub](https://github.com/OnGridSystems/erc20_airdrop_cli/workflows/airdrop_pytest/badge.svg)](https://github.com/OnGridSystems/erc20_airdrop_cli/actions?query=workflow%3Aairdrop_pytest) + + # Simple CLI app for ERC-20 Airdrops on Ethereum -* compose and verify the list of token recipients -* generate transactions -* execute them +Allows controllable ERC-20/BEP-20 token airdrops by the given list of recipients and amounts. Simplifies control of sending and eliminates human mistakes. The process of airdrop looks like this: + +* fill in the list of recipients and amounts for each +* verify the list, number of recepients and total sum +* generate and sign transactions +* top up sending account with tokens and ETH/BNB +* execute (send) them + + +## Installation + +Clone repositiry. +Create venv: + +```sh +python -m venv venv +``` + +Install dependencies: +```sh +pip install -r requirements.txt +``` + + +## Using app + +At any time run ```./airdrop.py help``` to call help menu. +To show current status ```./airdrop.py show```. + +1. ```./airdrop.py init``` to initialize database - creates SQLite fine in the directory +2. ```./airdrop.py import``` to set sender's private key +3. ```./airdrop.py token ``` to set token address for airdrop. +4. ```./airdrop.py update``` to update balances and current account's nonce. You can run it anytime. + +5. Add recipietns for airdrop, one recipient per command. Amounts are in decimal format. If you specify 1.49, it means you'll send 1490000000000000000 of token units (decimals=18 assumed). + +```./airdrop.py add
``` + + +6. Sign **all** your transactions ```./airdrop.py sign``` + +7. Send transactions on the wire, one at a time ```./airdrop.py sign``` - sends **first** SIGNED transaction and it becomes SENT. And now you can check transaction hash in show menu. + +8. If the execution was interrupted and the receipt was not received. You can request it: +```./airdrop.py receipt``` + + +#### Testing + +```sh +pip install pytest +pytest +```