# Copyright 2017 OmiseGO Pte Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import pytest import json import populus.wait from populus.wait import Wait import web3 as web3module from web3 import Web3, IPCProvider from processor import process from utils import get_contracts, Creator, Signer, theoretical_gas, Sender, AirdropException, AirdropOOGException, \ remove_estimate from constants import RESERVE_AIRDROP, GAS_LIMIT, BATCH_SIZE, GAS_PRICE, GAS_RESERVE, DEAD @pytest.fixture() def web3(): web3 = Web3(IPCProvider("/tmp/ethereum_dev_mode/geth.ipc")) web3.personal.unlockAccount(web3.eth.accounts[0], "") return web3 @pytest.fixture() def prepared_contracts(web3): airdropper, omg_token = get_contracts(web3) mint_tx = omg_token.transact().mint(airdropper.address, RESERVE_AIRDROP) Wait(web3).for_receipt(mint_tx) return airdropper, omg_token @pytest.fixture() def airdrops(): """ uses a pre-prepared json file with processed airdrops (see README.md) it is also a truncated list of airdrops, just enough for 2 uneven transactions """ with open("data/processed.json") as f: airdrops = json.loads(f.read()) return airdrops[0:BATCH_SIZE + 10] @pytest.fixture() def creator(web3, prepared_contracts): airdropper, omg_token = prepared_contracts creator = Creator(web3.eth.accounts[0], airdropper, omg_token, GAS_LIMIT, GAS_PRICE, GAS_RESERVE) return creator @pytest.fixture() def transactions(creator, airdrops): transactions = creator.create_txs(airdrops[0:100], BATCH_SIZE) return transactions @pytest.fixture() def signed(web3, transactions): signed = Signer(web3).sign_transactions(transactions) return signed @pytest.fixture() def input_file(): with open("data/balances_airdrop.json") as f: yield f @pytest.mark.slow def test_entire_flow(web3, prepared_contracts, creator, input_file): airdropper, omg_token = prepared_contracts airdrops = process(input_file.read()) transactions = creator.create_txs(airdrops, BATCH_SIZE) # this being a long-running test, the unlocking from web3 fixture might have expired web3.personal.unlockAccount(web3.eth.accounts[0], "") signed = Signer(web3).sign_transactions(transactions) Sender(web3).send_transactions(signed, transactions) check_entirely_airdropped(airdrops, omg_token) def test_return_from_contract(web3, prepared_contracts, airdrops): """ Paranoid test asserting that the full reserve of airdrop funds can be recovered by Sender """ airdropper, omg_token = prepared_contracts tx = airdropper.transact().multisend(omg_token.address, web3.eth.accounts[:1], [RESERVE_AIRDROP]) Wait(web3).for_receipt(tx) check_none_airdropped(airdrops, omg_token) assert omg_token.call().balanceOf(airdropper.address) == 0 assert omg_token.call().balanceOf(web3.eth.accounts[0]) == RESERVE_AIRDROP def test_small_flow(web3, prepared_contracts, creator, airdrops): _, omg_token = prepared_contracts transactions = creator.create_txs(airdrops, BATCH_SIZE) signed = Signer(web3).sign_transactions(transactions) Sender(web3).send_transactions(signed, transactions) check_entirely_airdropped(airdrops, omg_token) def test_batch_endings(creator, airdrops): """ Makes sure that the last batch isn't missed """ transactions = creator.create_txs(airdrops, BATCH_SIZE) assert len(transactions[0]['rawBatch']) == BATCH_SIZE assert len(transactions[1]['rawBatch']) == len(airdrops) - BATCH_SIZE assert len(transactions) == 2 def test_gas_expenses(creator, airdrops): """ Tests whether too expensive/too cheap batches are picked up during creation """ with pytest.raises(AirdropException): creator.create_txs(airdrops, BATCH_SIZE * 2) with pytest.raises(AirdropException): creator.create_txs(airdrops, BATCH_SIZE / 2) def test_gas_limit_makes_sense(): assert theoretical_gas(BATCH_SIZE) < GAS_LIMIT assert theoretical_gas(BATCH_SIZE) >= GAS_LIMIT * 0.9 def test_unverifiable_eth_account(web3, prepared_contracts, airdrops, mocker): """ Should check that when the eth balance at 3988888 doesn't mandate an airdrop, creation is interrupted """ airdropper, omg_token = prepared_contracts mocker.patch('web3.eth.Eth.getBalance') web3module.eth.Eth.getBalance.side_effect = [123] creator = Creator(web3.eth.accounts[0], airdropper, omg_token, GAS_LIMIT, GAS_PRICE, GAS_RESERVE, verify_eth=True) with pytest.raises(AirdropException): creator.create_txs(airdrops[:1], BATCH_SIZE) def test_verifiable_eth_account(web3, prepared_contracts, airdrops, mocker): """ Should check that when the eth balance at 3988888 mandates an airdrop, the creation succeeds """ airdropper, omg_token = prepared_contracts mocker.patch('web3.eth.Eth.getBalance') web3module.eth.Eth.getBalance.side_effect = [4274999801259164787792424L] creator = Creator(web3.eth.accounts[0], airdropper, omg_token, GAS_LIMIT, GAS_PRICE, GAS_RESERVE, verify_eth=True) creator.create_txs(airdrops[:1], BATCH_SIZE) def test_logging(web3, prepared_contracts, transactions, signed, mocker): _, omg_token = prepared_contracts mocker.patch('logging.info') Sender(web3).send_transactions(signed, transactions) assert len(logging.info.call_args_list) == 4 * len(signed) def test_logging_failed_send(web3, prepared_contracts, transactions, signed, mocker): _, omg_token = prepared_contracts mocker.patch('logging.info') mocker.patch('web3.eth.Eth.sendRawTransaction') web3module.eth.Eth.sendRawTransaction.side_effect = [Exception] with pytest.raises(Exception): Sender(web3).send_transactions(signed, transactions) assert len(logging.info.call_args_list) == 1 def test_logging_failed_wait(web3, prepared_contracts, transactions, signed, mocker): _, omg_token = prepared_contracts mocker.patch('logging.info') mocker.patch('populus.wait.Wait.for_receipt') populus.wait.Wait.for_receipt.side_effect = [Exception] with pytest.raises(Exception): Sender(web3).send_transactions(signed, transactions) assert len(logging.info.call_args_list) == 2 def test_disaster_recovery(web3, prepared_contracts, transactions, signed, airdrops): """ Assuming transactions got sent partially, are we able to resume with confidence? """ _, omg_token = prepared_contracts unsent, unsent_unsigned = Sender(web3).recover_unsent(signed, transactions) assert unsent == signed assert unsent_unsigned == transactions Sender(web3).send_transactions(signed[:1], transactions[:1]) # airdrop partially done by now check_entirely_airdropped(airdrops[0:BATCH_SIZE], omg_token) # recovery unsent, unsent_unsigned = Sender(web3).recover_unsent(signed, transactions) assert len(unsent) == 1 assert len(unsent_unsigned) == 1 assert unsent[0] == signed[1] assert unsent_unsigned[0] == transactions[1] Sender(web3).send_transactions(unsent, unsent_unsigned) check_entirely_airdropped(airdrops, omg_token) def test_recover_sent_airdrops(web3, prepared_contracts, transactions, signed, airdrops, creator): """ Assuming partially sent airdrops, when there's need to sign transactions again e.g. when it turned out that too little gas was allowed (unlikely) """ airdropper, omg_token = prepared_contracts Sender(web3).send_transactions(signed[:1], transactions[:1]) # airdrop partially done by now check_entirely_airdropped(airdrops[0:BATCH_SIZE], omg_token) not_airdropped = Sender(web3).recover_unsent_airdrops(airdrops, signed, airdropper, omg_token) assert not_airdropped == airdrops[BATCH_SIZE:] unsigned = creator.create_txs(not_airdropped, BATCH_SIZE) new_signed = Signer(web3).sign_transactions(unsigned) Sender(web3).send_transactions(new_signed, unsigned) check_entirely_airdropped(airdrops, omg_token) def test_oog_handling(web3, prepared_contracts, transactions, airdrops): """ Do we halt the sending when an oog occurs? """ _, omg_token = prepared_contracts transactions[0]['tx']['gas'] = web3.toHex(transactions[0]['gasEstimate'] - 1) signed = Signer(web3).sign_transactions(transactions) with pytest.raises(AirdropOOGException): Sender(web3).send_transactions(signed, transactions) check_none_airdropped(airdrops, omg_token) # check recovery works with OOG unsent, unsent_unsigned = Sender(web3).recover_unsent(signed, transactions) assert unsent == signed assert unsent_unsigned == transactions def test_secondary_oog_protection(web3, transactions, mocker): """ "Do we halt the sending when an oog occurs?" - continued. Check the secondary, double-checking protection """ transactions[0]['tx']['gas'] = web3.toHex(transactions[0]['gasEstimate'] - 1) signed = Signer(web3).sign_transactions(transactions) # check the secondary OOG-detection measure, by tricking the primary mocker.patch('utils.Sender._did_oog') Sender._did_oog.side_effect = [False, False] with pytest.raises(AirdropOOGException): Sender(web3).send_transactions(signed, transactions) def test_throw_in_contract_handling(web3, prepared_contracts, transactions, airdrops): _, omg_token = prepared_contracts # whoops, omg_token got paused! omg_token should throw now pause_tx_hash = omg_token.transact().pause() Wait(web3).for_receipt(pause_tx_hash) # need to bump nonce in the pre-prepared transactions for transaction in transactions: transaction['tx']['nonce'] = web3.toHex(web3.toDecimal(transaction['tx']['nonce']) + 1) signed = Signer(web3).sign_transactions(transactions) with pytest.raises(AirdropOOGException): Sender(web3).send_transactions(signed, transactions) check_none_airdropped(airdrops, omg_token) def test_check_address_before_send(web3, creator, airdrops, signed): """ Tests whether the final check throws, in case local data differs from signed transactions """ airdrops[0][0] = web3.eth.accounts[0] different_transactions = creator.create_txs(airdrops, BATCH_SIZE) with pytest.raises(AirdropException): Sender(web3).send_transactions(signed, different_transactions) def test_check_amount_before_send(web3, creator, airdrops, signed): """ as above """ airdrops[0][1] += 1 different_transactions = creator.create_txs(airdrops, BATCH_SIZE) with pytest.raises(AirdropException): Sender(web3).send_transactions(signed, different_transactions) def test_creator_lowercases(web3, creator, prepared_contracts, airdrops): """ Ensures that created transactions are resistant to different capitalizations of inputs and are checksum-comparable """ airdropper, omg_token = prepared_contracts def _get_creator(f): """ Gets a Creator object feeding it f-transformed addresses for sender & contracts """ f_airdropper, f_omg_token = get_contracts(web3, '0x' + f(airdropper.address)[2:], '0x' + f(omg_token.address)[2:]) return Creator('0x' + f(web3.eth.accounts[0])[2:], f_airdropper, f_omg_token, GAS_LIMIT, GAS_PRICE, GAS_RESERVE) lower_creator, upper_creator = map(_get_creator, [lambda s: s.lower(), lambda s: s.upper()]) assert creator.create_txs(airdrops, BATCH_SIZE) == lower_creator.create_txs(airdrops, BATCH_SIZE) assert creator.create_txs(airdrops, BATCH_SIZE) == upper_creator.create_txs(airdrops, BATCH_SIZE) def test_removing_estimates(transactions): assert all(map(lambda item: 'gasEstimate' in item, transactions)) removed = remove_estimate(transactions) assert all(map(lambda item: 'gasEstimate' not in item, removed)) # # HELPER FUNCTIONS def check_entirely_airdropped(airdrops, omg_token): """ Checks whether the balances of OMG indicate the airdrop succeeded """ # find the position of the DEAD account, which may appear twice in airdrops deadindices = filter(lambda i: airdrops[i][0] == DEAD, xrange(len(airdrops))) deadairdrop = sum([airdrops[i][1] for i in deadindices]) for airdrop in airdrops: if airdrop[0] != DEAD: # for airdrops other than one to the "0xdead" they must agree with the airdrop data assert omg_token.call().balanceOf(airdrop[0]) == airdrop[1] else: assert omg_token.call().balanceOf(airdrop[0]) == deadairdrop def check_none_airdropped(airdrops, omg_token): """ Checks whether the balances of OMG indicate no airdrop happened """ for airdrop in airdrops: assert omg_token.call().balanceOf(airdrop[0]) == 0