# strtool.py

# python basics
import copy
import json
import os
import requests
import time
import sys
import pkgutil
from decimal import *
from functools import cache
import traceback

# low level web3
from web3 import Web3, middleware
from web3.gas_strategies.time_based import glacial_gas_price_strategy, slow_gas_price_strategy, medium_gas_price_strategy, fast_gas_price_strategy
from web3.middleware import geth_poa_middleware
from web3._utils.abi import get_abi_output_types
import eth_abi

# high level web3
from multicaller import multicaller

# strtool modules
from . import balancerErrors as be
from .enums.stablePoolJoinExitKind import StablePoolJoinKind, StablePhantomPoolJoinKind, StablePoolExitKind
from .enums.weightedPoolJoinExitKind import WeightedPoolJoinKind, WeightedPoolExitKind


class Suppressor(object):
    def __enter__(self):
        self.stdout = sys.stdout
        self.stderr = sys.stderr
        sys.stdout = self
        sys.stderr = self

    def __exit__(self, type, value, traceback):
        sys.stdout = self.stdout
        sys.stderr = self.stderr
        if type is not None:
            a = 0
            # Do normal exception handling

    def write(self, x): pass


def getLongestStringLength(array):
    maxLength = 0
    for a in array:
        if len(a) > maxLength:
            maxLength = len(a)
    return(maxLength)


def padWithSpaces(myString, endLength):
    stringLength = len(myString)
    extraSpace = endLength - stringLength

    outputString = myString + "".join([" "]*extraSpace)
    return(outputString)


class strtool(object):

    """
    Balancer Protocol Python API
    Interface with Balancer V2 Smart contracts directly from Python
    """

    DELEGATE_OWNER = '0xBA1BA1ba1BA1bA1bA1Ba1BA1ba1BA1bA1ba1ba1B'
    ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'

    # Constants
    INFINITE = 2 ** 256 - 1  # for infinite unlock
    MAX_UINT_112 = 2**112 - 1  # for stablephantom max bpt

    # Environment variable names
    envVarEtherscan = "KEY_API_ETHERSCAN"
    envVarInfura = "KEY_API_INFURA"
    envVarPrivate = "KEY_PRIVATE"
    envVarCustomRPC = "STRTOOL_CUSTOM_RPC"

    # Etherscan API call management
    lastEtherscanCallTime = 0
    etherscanMaxRate = 5.0  # hz
    etherscanSpeedDict = {
        "slow": "SafeGasPrice",
        "average": "ProposeGasPrice",
        "fast": "FastGasPrice"
    }
    speedDict = {
        "glacial": glacial_gas_price_strategy,
        "slow": slow_gas_price_strategy,
        "average": medium_gas_price_strategy,
        "fast": fast_gas_price_strategy
    }

    # Network parameters
    networkParams = {
        "mainnet":	{"id": 1,		"blockExplorerUrl": "etherscan.io",					"balFrontend": "app.balancer.fi/#/"},
        "ropsten":	{"id": 3,		"blockExplorerUrl": "ropsten.etherscan.io"},
        "rinkeby":	{"id": 4,		"blockExplorerUrl": "rinkeby.etherscan.io"},
        "goerli":	{"id": 5,		"blockExplorerUrl": "goerli.etherscan.io"},
        "optimism":	{"id": 10,		"blockExplorerUrl": "optimistic.etherscan.io"},
        "kovan":	{"id": 42,		"blockExplorerUrl": "kovan.etherscan.io",			"balFrontend": "kovan.balancer.fi/#/"},
        "bnb":		{"id": 56,		"blockExplorerUrl": "bscscan.com", },
        "bnb-test":	{"id": 97,		"blockExplorerUrl": "testnet.bscscan.com", },
        "polygon":	{"id": 137,		"blockExplorerUrl": "polygonscan.com",				"balFrontend": "polygon.balancer.fi/#/"},
        "fantom":	{"id": 250,		"blockExplorerUrl": "ftmscan.com",					"balFrontend": "app.beets.fi/#/"},
        "arbitrum":	{"id": 42161,	"blockExplorerUrl": "arbiscan.io",					"balFrontend": "arbitrum.balancer.fi/#/"}
    }

    # ABIs and Deployment Addresses
    abis = {}
    deploymentAddresses = {}
    contractDirectories = {
        # ===== Vault Infra =====
        "Vault": {
            "directory": "20230323-vault"
        },
        "BalancerHelpers": {
            "directory": "20230323-vault"
        },
        "Authorizer": {
            "directory": "20230323-authorizer"
        },

        # ====== Pools and Associated Contracts ======
        "WeightedPoolFactory": {
            "directory": "20230323-weighted-pool-v3"
        },
        "ComposableStablePoolFactory": {
            "directory": "20230323-composable-stable-pool-v3"
        }
    }

    decimals = {}

    UserBalanceOpKind = {
        "DEPOSIT_INTERNAL": 0,
        "WITHDRAW_INTERNAL": 1,
        "TRANSFER_INTERNAL": 2,
        "TRANSFER_EXTERNAL": 3
    }
    inverseUserBalanceOpKind = {
        0: "DEPOSIT_INTERNAL",
        1: "WITHDRAW_INTERNAL",
        2: "TRANSFER_INTERNAL",
        3: "TRANSFER_EXTERNAL"
    }

    def __init__(self, network=None, verbose=True, customConfigFile=None, manualEnv={}):
        super(strtool, self).__init__()

        self.verbose = verbose
        if self.verbose:
            print()
            print("==============================================================")
            print("============== Initializing Balancer Python API ==============")
            print("==============================================================")
            print()

        if network is None:
            print("No network set. Defaulting to kovan")
            network = "kovan"
        else:
            print("Network is set to", network)
        self.network = network.lower()

        # set high decimal precision
        getcontext().prec = 28

        # grab parameters from env vars if they exist
        self.infuraApiKey = os.environ.get(self.envVarInfura)
        self.customRPC = os.environ.get(self.envVarCustomRPC)
        self.etherscanApiKey = os.environ.get(self.envVarEtherscan)
        self.privateKey = os.environ.get(self.envVarPrivate)

        # override params with manually passed args if they exist
        if "infuraApiKey" in manualEnv.keys():
            self.infuraApiKey = manualEnv["infuraApiKey"]
        if "customRPC" in manualEnv.keys():
            self.customRPC = manualEnv["customRPC"]
        if "etherscanApiKey" in manualEnv.keys():
            self.etherscanApiKey = manualEnv["etherscanApiKey"]
        if "privateKey" in manualEnv.keys():
            self.privateKey = manualEnv["privateKey"]

        if self.infuraApiKey is None and self.customRPC is None:
            self.ERROR(
                "You need to add your KEY_API_INFURA or STRTOOL_CUSTOM_RPC environment variables\n")
            self.ERROR(
                "!! If you are using L2, you must use STRTOOL_CUSTOM_RPC !!")
            print("\t\texport " + self.envVarInfura + "=<yourInfuraApiKey>")
            print("\t\t\tOR")
            print("\t\texport " + self.envVarCustomRPC + "=<yourCustomRPC>")
            print("\n\t\tNOTE: if you set " + self.envVarCustomRPC +
                  ", it will override your Infura API key!")
            quit()

        if self.etherscanApiKey is None or self.privateKey is None:
            self.ERROR(
                "You need to add your keys to the your environment variables")
            print("\t\texport " + self.envVarEtherscan + "=<yourEtherscanApiKey>")
            print("\t\texport " + self.envVarPrivate + "=<yourPrivateKey>")
            quit()

        endpoint = self.customRPC
        if endpoint is None:
            endpoint = self.infuraApiKey

        self.endpoint = endpoint
        self.web3 = Web3(Web3.HTTPProvider(endpoint))

        acct = self.web3.eth.account.privateKeyToAccount(self.privateKey)
        self.web3.eth.default_account = acct.address
        self.address = acct.address

        # initialize gas block caches
        self.currGasPriceSpeed = None
        self.web3.middleware_onion.add(middleware.time_based_cache_middleware)
        self.web3.middleware_onion.add(
            middleware.latest_block_based_cache_middleware)
        self.web3.middleware_onion.add(middleware.simple_cache_middleware)

        # add support for PoA chains
        self.web3.middleware_onion.inject(geth_poa_middleware, layer=0)

        if self.verbose:
            print("Initialized account", self.web3.eth.default_account)
            print("Connected to web3 at", endpoint)

        usingCustomConfig = (not customConfigFile is None)
        customConfig = None
        if usingCustomConfig:

            # load custom config file if it exists, quit if not
            if not os.path.isfile(customConfigFile):
                self.ERROR("Custom config file" +
                           customConfigFile + " not found!")
                quit()
            else:
                with open(customConfigFile, 'r') as f:
                    customConfig = json.load(f)

            # ensure all required fields are in the customConfig
            requiredFields = ["contracts", "networkParams"]
            hasAllRequirements = True
            for req in requiredFields:
                if not req in customConfig.keys():
                    hasAllRequirements = False
            if not hasAllRequirements:
                self.ERROR("Not all custom fields are in the custom config!")
                print("You must include:")
                for req in requiredFields:
                    print("\t"+req)
                print()
                quit()

            # add network params for network
            currNetworkParams = {
                "id":				customConfig["networkParams"]["id"],
                "blockExplorerUrl":	customConfig["networkParams"]["blockExplorerUrl"]
            }

            if "balFrontend" in customConfig["networkParams"].keys():
                currNetworkParams["balFrontend"] = customConfig["networkParams"]["balFrontend"]
            self.networkParams[self.network] = currNetworkParams

        self.mc = multicaller.multicaller(_chainId=self.networkParams[self.network]["id"],
                                          _web3=self.web3,
                                          _maxRetries=5,
                                          _verbose=False,
                                          _allowFailure=True)

        # reset for the edge case in which we're iterating through multiple networks
        self.deploymentAddresses = {}
        missingContracts = []
        for contractType in self.contractDirectories.keys():
            subdir = self.contractDirectories[contractType]["directory"]

            # get contract abi from deployment
            abiPath = os.path.join('deployments', subdir,
                                   "abi", contractType + '.json')
            try:
                f = pkgutil.get_data(__name__, abiPath).decode()
                currAbi = json.loads(f)
                self.abis[contractType] = currAbi
            except BaseException as error:
                self.ERROR('Error accessing file: {}'.format(abiPath))
                self.ERROR('{}'.format(error))

            # get deployment address for given network
            try:
                if usingCustomConfig:
                    currAddress = self.web3.toChecksumAddress(
                        customConfig["contracts"][contractType])
                else:
                    deploymentPath = os.path.join(
                        'deployments', subdir, "output", self.network + '.json')
                    f = pkgutil.get_data(__name__, deploymentPath).decode()
                    currData = json.loads(f)
                    currAddress = self.web3.toChecksumAddress(
                        currData[contractType])
                self.deploymentAddresses[contractType] = currAddress
            except BaseException as error:
                missingContracts.append(contractType)

        print("Available contracts on", self.network)
        for element in self.deploymentAddresses.keys():
            address = self.deploymentAddresses[element]
            print("\t" + address + "\t" + element)

        print()
        print("Missing contracts on", self.network +
              ": [" + ", ".join(missingContracts) + "]")

        print()
        print("==============================================================")

    # ======================
    # ====Color Printing====
    # ======================
    def WARN(self, text):
        WARNING_BEGIN = '\033[93m'
        WARNING_END = '\033[0m'
        print(WARNING_BEGIN + "[WARNING] " + text + WARNING_END)

    def ERROR(self, text):
        ERROR_BEGIN = '\033[91m'
        ERROR_END = '\033[0m'
        print(ERROR_BEGIN + "[ERROR] " + text + ERROR_END)

    def GOOD(self, text):
        GOOD_BEGIN = '\033[92m'
        GOOD_END = '\033[0m'
        print(GOOD_BEGIN + text + GOOD_END)

    # =====================
    # ===Transaction Fns===
    # =====================
    def buildTx(self, fn, gasFactor, gasSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        chainIdNetwork = self.networkParams[self.network]["id"]

        # Get nonce if not overridden
        if nonceOverride > -1:
            nonce = nonceOverride
        else:
            nonce = self.web3.eth.get_transaction_count(
                self.web3.eth.default_account)

        # Calculate gas estimate if not overridden
        if gasEstimateOverride > -1:
            gasEstimate = gasEstimateOverride
        else:
            try:
                gasEstimate = int(fn.estimateGas() * gasFactor)
            except BaseException as error:
                descriptiveError = be.handleException(error)
                self.ERROR("Transaction failed at gas estimation!")
                self.ERROR(descriptiveError)
                return(None)

        # Get gas price from Etherscan if not overridden
        if gasPriceGweiOverride > -1:
            gasPriceGwei = gasPriceGweiOverride
        else:
            gasPriceGwei = self.getGasPrice(gasSpeed)

        print("\tGas Estimate:\t", gasEstimate)
        print("\tGas Price:\t", gasPriceGwei, "Gwei")
        print("\tNonce:\t\t", nonce)

        # build transaction
        data = fn.buildTransaction({'chainId': chainIdNetwork,
                                    'gas': gasEstimate,
                                    'gasPrice': self.web3.toWei(gasPriceGwei, 'gwei'),
                                    'nonce': nonce,
                                    })
        return(data)

    def sendTx(self, tx, isAsync=False):
        signedTx = self.web3.eth.account.sign_transaction(tx, self.privateKey)
        txHash = self.web3.eth.send_raw_transaction(
            signedTx.rawTransaction).hex()

        print()
        print("Sending transaction, view progress at:")
        print("\thttps://" +
              self.networkParams[self.network]["blockExplorerUrl"]+"/tx/"+txHash)

        if not isAsync:
            self.waitForTx(txHash)
        return(txHash)

    def waitForTx(self, txHash, timeOutSec=120):
        txSuccessful = True
        print()
        print("Waiting for tx:", txHash)
        try:
            receipt = self.web3.eth.wait_for_transaction_receipt(
                txHash, timeout=timeOutSec)
            if not receipt["status"] == 1:
                txSuccessful = False
        except BaseException as error:
            print('Transaction timeout: {}'.format(error))
            return(False)

        # Race condition: add a small delay to avoid getting the last nonce
        time.sleep(0.5)

        print("\tTransaction accepted by network!")
        if not txSuccessful:
            self.ERROR("Transaction failed!")
            return(False)
        print("\tTransaction was successful!\n")

        return(True)

    def getTxReceipt(self, txHash, delay, maxRetries):
        for i in range(maxRetries):
            try:
                receipt = self.web3.eth.getTransactionReceipt(txHash)
                print("Retrieved receipt!")
                return(receipt)
            except Exception as e:
                print(e)
                print("Transaction not found yet, will check again in",
                      delay, "seconds")
                time.sleep(delay)
        self.ERROR("Transaction not found in" + str(maxRetries) + "retries.")
        return(False)

    # =====================
    # ====ERC20 methods====
    # =====================
    @cache
    def erc20GetContract(self, tokenAddress):
        # Read files packaged in module
        abiPath = os.path.join('abi/ERC20.json')
        f = pkgutil.get_data(__name__, abiPath).decode()
        abi = json.loads(f)

        token = self.web3.eth.contract(
            self.web3.toChecksumAddress(tokenAddress), abi=abi)
        return(token)

    @cache
    def erc20GetDecimals(self, tokenAddress):

        # keep the manually maintained cache since the
        # multicaller function can populate it too
        if tokenAddress in self.decimals.keys():
            return(self.decimals[tokenAddress])

        if tokenAddress == self.ZERO_ADDRESS:
            self.decimals[tokenAddress] = 18
            return(18)

        token = self.erc20GetContract(tokenAddress)
        decimals = token.functions.decimals().call()
        return(decimals)

    def erc20GetBalanceStandard(self, tokenAddress, address=None):
        if address is None:
            address = self.address
        token = self.erc20GetContract(tokenAddress)
        decimals = self.erc20GetDecimals(tokenAddress)
        standardBalance = Decimal(token.functions.balanceOf(
            address).call()) * Decimal(10**(-decimals))
        return(standardBalance)

    def erc20GetAllowanceStandard(self, tokenAddress, allowedAddress):
        token = self.erc20GetContract(tokenAddress)
        decimals = self.erc20GetDecimals(tokenAddress)
        standardAllowance = Decimal(token.functions.allowance(
            self.address, allowedAddress).call()) * Decimal(10**(-decimals))
        return(standardAllowance)

    def erc20BuildFunctionSetAllowance(self, tokenAddress, allowedAddress, allowance):
        token = self.erc20GetContract(tokenAddress)
        approveFunction = token.functions.approve(allowedAddress, allowance)
        return(approveFunction)

    def erc20SignAndSendNewAllowance(self,
                                     tokenAddress,
                                     allowedAddress,
                                     allowance,
                                     gasFactor,
                                     gasSpeed,
                                     nonceOverride=-1,
                                     gasEstimateOverride=-1,
                                     gasPriceGweiOverride=-1,
                                     isAsync=False):
        fn = self.erc20BuildFunctionSetAllowance(
            tokenAddress, allowedAddress, allowance)
        tx = self.buildTx(fn, gasFactor, gasSpeed, nonceOverride,
                          gasEstimateOverride, gasPriceGweiOverride)
        txHash = self.sendTx(tx, isAsync)
        return(txHash)

    def erc20HasSufficientBalance(self, tokenAddress, amountToUse):
        balance = self.erc20GetBalanceStandard(tokenAddress)

        print("Token:", tokenAddress)
        print("\tNeed:", float(amountToUse))
        print("\tWallet has:", float(balance))

        sufficient = (float(balance) >= float(amountToUse))
        if not sufficient:
            self.ERROR("Insufficient Balance!")
        else:
            print("\tWallet has sufficient balance.")
        print()
        return(sufficient)

    def erc20HasSufficientBalances(self, tokens, amounts):
        if not len(tokens) == len(amounts):
            self.ERROR("Array length mismatch with " + str(len(tokens)) +
                       " tokens and " + str(len(amounts)) + " amounts.")
            return(False)
        numElements = len(tokens)
        sufficientBalance = True
        for i in range(numElements):
            token = tokens[i]
            amount = amounts[i]
            currentHasSufficientBalance = self.erc20HasSufficientBalance(
                token, amount)
            sufficientBalance &= currentHasSufficientBalance
        return(sufficientBalance)

    def erc20HasSufficientAllowance(self, tokenAddress, allowedAddress, amount):
        currentAllowance = self.erc20GetAllowanceStandard(
            tokenAddress, allowedAddress)
        balance = self.erc20GetBalanceStandard(tokenAddress)

        print("Token:", tokenAddress)
        print("\tCurrent Allowance:", currentAllowance)
        print("\tCurrent Balance:", balance)
        print("\tAmount to Spend:", amount)

        sufficient = (currentAllowance >= Decimal(amount))

        if not sufficient:
            print("\tInsufficient allowance!")
            print("\tWill need to unlock", tokenAddress)
        else:
            print("\tWallet has sufficient allowance.")
        print()
        return(sufficient)

    def erc20EnforceSufficientAllowance(self,
                                        tokenAddress,
                                        allowedAddress,
                                        targetAllowance,
                                        amount,
                                        gasFactor,
                                        gasSpeed,
                                        nonceOverride,
                                        gasEstimateOverride,
                                        gasPriceGweiOverride,
                                        isAsync):
        if not self.erc20HasSufficientAllowance(tokenAddress, allowedAddress, amount):
            if targetAllowance == -1 or targetAllowance == self.INFINITE:
                targetAllowance = self.INFINITE
            else:
                decimals = self.erc20GetDecimals(tokenAddress)
                targetAllowance = Decimal(
                    targetAllowance) * Decimal(10**decimals)
            targetAllowance = int(targetAllowance)
            print("Insufficient Allowance: Increasing to", targetAllowance)
            txHash = self.erc20SignAndSendNewAllowance(tokenAddress, allowedAddress, targetAllowance, gasFactor,
                                                       gasSpeed, nonceOverride=nonceOverride, isAsync=isAsync, gasPriceGweiOverride=gasPriceGweiOverride)
            return(txHash)
        return(None)

    def erc20EnforceSufficientVaultAllowance(self, tokenAddress, targetAllowance, amount, gasFactor, gasSpeed, nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1, isAsync=False):
        return(self.erc20EnforceSufficientAllowance(tokenAddress, self.deploymentAddresses["Vault"], targetAllowance, amount, gasFactor, gasSpeed, nonceOverride, gasEstimateOverride, gasPriceGweiOverride, isAsync))

    def erc20GetTargetAllowancesFromPoolData(self, poolDescription):
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolDescription["tokens"].keys()))
        allowances = []
        for token in tokens:
            targetAllowance = -1
            if "allowance" in poolDescription["tokens"][token].keys():
                targetAllowance = poolDescription["tokens"][token]["allowance"]
            if targetAllowance == -1:
                targetAllowance = self.INFINITE
            allowances.append(targetAllowance)
        return(tokens, allowances)

    def erc20AsyncEnforceSufficientVaultAllowances(self, tokens, targetAllowances, amounts, gasFactor, gasSpeed, nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        if not len(tokens) == len(targetAllowances):
            self.ERROR("Array length mismatch with " + str(len(tokens)) +
                       " tokens and " + str(len(targetAllowances)) + " targetAllowances.")
            return(False)

        nonce = self.web3.eth.get_transaction_count(
            self.web3.eth.default_account)
        txHashes = []
        numElements = len(tokens)
        for i in range(numElements):
            token = tokens[i]
            targetAllowance = targetAllowances[i]
            amount = amounts[i]
            txHash = self.erc20EnforceSufficientVaultAllowance(
                token, targetAllowance, amount, gasFactor, gasSpeed, nonceOverride=nonce, isAsync=True)
            if not txHash is None:
                txHashes.append(txHash)
                nonce += 1

        for txHash in txHashes:
            self.waitForTx(txHash)
        return(True)

    # =====================
    # ======Etherscan======
    # =====================
    def generateEtherscanApiUrl(self):
        etherscanUrl = self.networkParams[self.network]["blockExplorerUrl"]
        separator = "."
        if self.network in ["kovan", "rinkeby", "goerli", "optimism"]:
            separator = "-"
        elif self.network in ["bnb-test"]:
            etherscanUrl = self.networkParams["bnb-test"]["blockExplorerUrl"]
        urlFront = "https://api" + separator + etherscanUrl
        return(urlFront)

    def callEtherscan(self, url, maxRetries=3, verbose=False):
        urlFront = self.generateEtherscanApiUrl()
        url = urlFront + url + self.etherscanApiKey
        if verbose:
            print("Calling:", url)

        count = 0
        while count < maxRetries:
            try:
                dt = (time.time() - self.lastEtherscanCallTime)
                if dt < 1.0/self.etherscanMaxRate:
                    time.sleep((1.0/self.etherscanMaxRate - dt) * 1.1)

                # faking a user-agent resolves the 403 (forbidden) errors on api-kovan.etherscan.io
                headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36', "Upgrade-Insecure-Requests": "1",
                           "DNT": "1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate"}
                r = requests.get(url, headers=headers)
                if verbose:
                    print("\t", r)
                self.lastEtherscanCallTime = time.time()
                data = r.json()
                if verbose:
                    print("\t", data)
                return(data)
            except Exception as e:
                print("Exception:", e)
                count += 1
                delaySec = 2
                if verbose:
                    self.WARN("Etherscan failed " + str(count) +
                              " times. Retrying in " + str(delaySec) + " seconds...")
                time.sleep(delaySec)
        self.ERROR("Etherscan failed " + str(count) + " times.")
        return(False)

    def getGasPriceEtherscanGwei(self, speed, verbose=False):
        if not speed in self.etherscanSpeedDict.keys():
            self.ERROR("Speed entered is:" + speed)
            self.ERROR("Speed must be one of the following options:")
            for s in self.etherscanSpeedDict.keys():
                print("\t" + s)
            return(False)

        urlString = "/api?module=gastracker&action=gasoracle&apikey="  # + self.etherscanApiKey
        response = self.callEtherscan(urlString, verbose=verbose)
        return(response["result"][self.etherscanSpeedDict[speed]])

    def getTransactionsByAddress(self, address, internal=False, startblock=0, verbose=False):
        if verbose:
            print("\tQuerying data after block", startblock)

        internalString = ""
        if internal:
            internalString = "internal"

        url = []
        url.append(
            "/api?module=account&action=txlist{}&address=".format(internalString))
        url.append(address)
        url.append(
            "&startblock={}&endblock=99999999&sort=asc&apikey=".format(startblock))
        urlString = "".join(url)
        txns = self.callEtherscan(urlString, verbose=verbose)

        if int(txns["status"]) == 0:
            self.ERROR("Etherscan query failed. Please try again.")
            return(False)
        elif int(txns["status"]) == 1:
            return(txns["result"])

    def getTransactionByHash(self, txHash, verbose=False):
        urlString = "/api?module=proxy&action=eth_getTransactionByHash&txhash={}&apikey=".format(
            txHash)
        txns = self.callEtherscan(urlString, verbose=verbose)

        if verbose:
            print(txns)

        if txns == False:
            return(False)
        return(txns)

    def isContractVerified(self, poolId, verbose=False):
        address = self.balPooldIdToAddress(poolId)
        url = "/api?module=contract&action=getabi&address={}&apikey=".format(
            address)
        results = self.callEtherscan(url, verbose=verbose)
        if verbose:
            print(results)
        isUnverified = (results["result"] ==
                        "Contract source code not verified")
        return(not isUnverified)

    def getGasPricePolygon(self, speed):
        if speed in self.etherscanSpeedDict.keys():
            etherscanGasSpeedNamesToPolygon = {
                "slow": "safeLow",
                "average": "standard",
                "fast": "fast"
            }
            speed = etherscanGasSpeedNamesToPolygon[speed]

        allowedSpeeds = ["safeLow", "standard", "fast", "fastest"]
        if speed not in allowedSpeeds:
            self.ERROR("Speed entered is:" + speed)
            self.ERROR("Speed must be one of the following options:")
            for s in allowedSpeeds:
                print("\t" + s)
            return(False)

        r = requests.get("https://gasstation-mainnet.matic.network/")
        prices = r.json()
        return(prices[speed])

    def getGasPrice(self, speed):
        allowedSpeeds = list(self.speedDict.keys())
        if speed not in allowedSpeeds:
            self.ERROR("Speed entered is:" + speed)
            self.ERROR("Speed must be one of the following options:")
            for s in allowedSpeeds:
                print("\t" + s)
            return(False)

        if not speed == self.currGasPriceSpeed:
            self.currGasPriceSpeed = speed
            self.web3.eth.set_gas_price_strategy(self.speedDict[speed])

        gasPrice = self.web3.eth.generateGasPrice() * 1e-9
        return(gasPrice)

    def balSortTokens(self, tokensIn):
        # tokens need to be sorted as lowercase, but if they're provided as checksum, then
        # the checksum format strings are still the keys outside of this function, so they
        # must be preserved as they're input
        lowerTokens = [t.lower() for t in tokensIn]
        lowerToOriginal = {}
        for i in range(len(tokensIn)):
            lowerToOriginal[lowerTokens[i]] = tokensIn[i]
        lowerTokens.sort()

        # get checksum tokens, translated sorted lower tokens back to their original format
        checksumTokens = [self.web3.toChecksumAddress(t) for t in lowerTokens]
        sortedInputTokens = [lowerToOriginal[f] for f in lowerTokens]

        return(sortedInputTokens, checksumTokens)

    def balWeightsEqualOne(self, poolData):
        tokenData = poolData["tokens"]
        tokens = tokenData.keys()

        weightSum = Decimal(0.0)
        for token in tokens:
            weightSum += Decimal(tokenData[token]["weight"])

        weightEqualsOne = (weightSum == Decimal(1.0))
        if not weightEqualsOne:
            self.ERROR("Token weights add up to " +
                       str(weightSum) + ", but they must add up to 1.0")
            self.ERROR(
                "If you are passing more than 16 digits of precision, you must pass the value as a string")
        return(weightEqualsOne)

    def balConvertTokensToWei(self, tokens, amounts):
        rawTokens = []
        if not len(tokens) == len(amounts):
            self.ERROR("Array length mismatch with " + str(len(tokens)) +
                       " tokens and " + str(len(amounts)) + " amounts.")
            return(False)
        numElements = len(tokens)
        for i in range(numElements):
            token = tokens[i]
            rawValue = amounts[i]
            decimals = self.erc20GetDecimals(token)
            if rawValue == self.INFINITE or rawValue == self.MAX_UINT_112:
                decimals = 0
            raw = int(Decimal(rawValue) * Decimal(10**decimals))
            rawTokens.append(raw)
        return(rawTokens)

    def balSetOwner(self, poolData):
        owner = self.ZERO_ADDRESS
        if "owner" in poolData.keys():
            ownerAddress = poolData["owner"]
            if not len(ownerAddress) == 42:
                self.ERROR(
                    "Entry for \"owner\" must be a 42 character Ethereum address beginning with \"0x\"")
                return(False)
            owner = self.web3.toChecksumAddress(ownerAddress)
        return(owner)

    def balCreateFnWeightedPoolFactory(self, poolData):
        factory = self.balLoadContract("WeightedPoolFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))

        intWithDecimalsWeights = [
            int(Decimal(poolData["tokens"][t]["weight"]) * Decimal(1e18)) for t in tokens]
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        rateProviders = [self.web3.toChecksumAddress(
            poolData["tokens"][token]["rateProvider"]) for token in tokens]

        if not self.balWeightsEqualOne(poolData):
            return(False)

        owner = self.balSetOwner(poolData)

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  intWithDecimalsWeights,
                                                  rateProviders,
                                                  swapFeePercentage,
                                                  owner)
        return(createFunction)

    def balCreateFnWeightedPool2TokensFactory(self, poolData):
        factory = self.balLoadContract("WeightedPool2TokensFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))

        if not len(tokens) == 2:
            self.ERROR("WeightedPool2TokensFactory requires 2 tokens, but", len(
                tokens), "were given.")
            return(False)

        if not self.balWeightsEqualOne(poolData):
            return(False)

        intWithDecimalsWeights = [
            int(Decimal(poolData["tokens"][t]["weight"]) * Decimal(1e18)) for t in tokens]
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))

        owner = self.balSetOwner(poolData)

        oracleEnabled = False
        if "oracleEnabled" in poolData.keys():
            oracleEnabled = poolData["oracleEnabled"]
            if isinstance(oracleEnabled, str):
                if oracleEnabled.lower() == "true":
                    oracleEnabled = True
                else:
                    oracleEnabled = False

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  intWithDecimalsWeights,
                                                  swapFeePercentage,
                                                  oracleEnabled,
                                                  owner)
        return(createFunction)

    def balCreateFnStablePoolFactory(self, poolData):
        factory = self.balLoadContract("StablePoolFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))

        owner = self.balSetOwner(poolData)

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  int(poolData["amplificationParameter"]),
                                                  swapFeePercentage,
                                                  owner)
        return(createFunction)

    def balCreateFnLBPoolFactory(self, poolData):
        return(self.balCreateFnLBPFactory(poolData, "LiquidityBootstrappingPoolFactory"))

    def balCreateFnNoProtocolFeeLiquidityBootstrappingPoolFactory(self, poolData):
        return(self.balCreateFnLBPFactory(poolData, "NoProtocolFeeLiquidityBootstrappingPoolFactory"))

    def balCreateFnLBPFactory(self, poolData, factoryName):
        factory = self.balLoadContract(factoryName)
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))

        if not self.balWeightsEqualOne(poolData):
            return(False)

        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        intWithDecimalsWeights = [
            int(Decimal(poolData["tokens"][t]["weight"]) * Decimal(1e18)) for t in tokens]
        owner = self.balSetOwner(poolData)

        if not owner == self.address:
            self.WARN("!!! You are not the owner for your LBP !!!")
            self.WARN("You:\t\t" + self.address)
            self.WARN("Pool Owner:\t" + owner)

            print()
            self.WARN("Only the pool owner can add liquidity. If you do not control " +
                      owner + " then you will not be able to add liquidity!")
            self.WARN("If you DO control " + owner +
                      ", you will need to use the \"INIT\" join type from that address")
            cancelTimeSec = 30
            self.WARN("If the owner mismatch is was unintentional, you have " +
                      str(cancelTimeSec) + " seconds to cancel with Ctrl+C.")
            time.sleep(cancelTimeSec)

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  intWithDecimalsWeights,
                                                  swapFeePercentage,
                                                  owner,
                                                  poolData["swapEnabledOnStart"])
        return(createFunction)

    def balCreateFnMetaStablePoolFactory(self, poolData):
        factory = self.balLoadContract("MetaStablePoolFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        owner = self.balSetOwner(poolData)

        rateProviders = [self.web3.toChecksumAddress(
            poolData["tokens"][token]["rateProvider"]) for token in tokens]
        priceRateCacheDurations = [
            int(poolData["tokens"][token]["priceRateCacheDuration"]) for token in tokens]

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  int(poolData["amplificationParameter"]),
                                                  rateProviders,
                                                  priceRateCacheDurations,
                                                  swapFeePercentage,
                                                  poolData["oracleEnabled"],
                                                  owner)
        return(createFunction)

    def balCreateFnInvestmentPoolFactory(self, poolData):
        factory = self.balLoadContract("InvestmentPoolFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        intWithDecimalsWeights = [
            int(Decimal(poolData["tokens"][t]["weight"]) * Decimal(1e18)) for t in tokens]
        managementFeePercentage = int(
            Decimal(poolData["managementFeePercent"]) * Decimal(1e16))
        # Deployed factory doesn't allow asset managers
        # assetManagers = [0 for i in range(0,len(tokens))]
        owner = self.balSetOwner(poolData)
        if not owner == self.address:
            self.WARN("!!! You are not the owner for your Investment Pool !!!")
            self.WARN("You:\t\t" + self.address)
            self.WARN("Pool Owner:\t" + owner)

            print()
            self.WARN(
                "Only the pool owner can call permissioned functions, such as changing weights or the management fee.")
            self.WARN(
                owner + " should either be you, or a multi-sig or other contract that you control and can call permissioned functions from.")
            cancelTimeSec = 30
            self.WARN("If the owner mismatch is was unintentional, you have " +
                      str(cancelTimeSec) + " seconds to cancel with Ctrl+C.")
            time.sleep(cancelTimeSec)

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  intWithDecimalsWeights,
                                                  swapFeePercentage,
                                                  owner,
                                                  poolData["swapEnabledOnStart"],
                                                  managementFeePercentage)
        return(createFunction)

    def balCreateFnManagedPoolFactory(self, poolData):
        self.WARN(
            "!!! You are using the Managed Pool Factory without a controller !!!")
        self.WARN(
            "You are currently using a factory to deploy a managed pool without a factory-provided controller contract.")
        self.WARN(
            "It is highly recommended that you use a factory that pairs a controller with a pool")
        self.WARN(
            "While this will be a valid pool, the owner will have a dangerous level of power over the pool")
        self.WARN("It *is* technically possible to add a controller contract as `owner`, but using a factory-paired one provides more guarantees")

        factory = self.balLoadContract("ManagedPoolFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        intWithDecimalsWeights = [
            int(Decimal(poolData["tokens"][t]["weight"]) * Decimal(1e18)) for t in tokens]
        assetManagers = [poolData["tokens"][t]["assetManager"] for t in tokens]
        managementAumFeePercentage = int(
            Decimal(poolData["managementAumFeePercentage"]) * Decimal(1e16))

        owner = self.balSetOwner(poolData)
        if not owner == self.address:
            self.WARN("!!! You are not the owner for your Managed Pool !!!")
            self.WARN("You:\t\t" + self.address)
            self.WARN("Pool Owner:\t" + owner)

            print()
            self.WARN(
                "Only the pool owner can call permissioned functions, such as changing weights or the management fee.")
            self.WARN(
                owner + " should either be you, or a multi-sig or other contract that you control and can call permissioned functions from.")
            cancelTimeSec = 30
            self.WARN("If the owner mismatch is was unintentional, you have " +
                      str(cancelTimeSec) + " seconds to cancel with Ctrl+C.")
            time.sleep(cancelTimeSec)

        createFunction = factory.functions.create(
            (
                poolData["name"],
                poolData["symbol"],
                checksumTokens,
                intWithDecimalsWeights,
                assetManagers,
                swapFeePercentage,
                poolData["swapEnabledOnStart"],
                poolData["mustAllowlistLPs"],
                managementAumFeePercentage,
                int(poolData["aumFeeId"])
            ),
            owner
        )
        return(createFunction)

    def balCreateFnStablePhantomPoolFactory(self, poolData):
        factory = self.balLoadContract("StablePhantomPoolFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        owner = self.balSetOwner(poolData)

        rateProviders = [self.web3.toChecksumAddress(
            poolData["tokens"][token]["rateProvider"]) for token in tokens]
        tokenRateCacheDurations = [
            int(poolData["tokens"][token]["tokenRateCacheDuration"]) for token in tokens]

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  int(poolData["amplificationParameter"]),
                                                  rateProviders,
                                                  tokenRateCacheDurations,
                                                  swapFeePercentage,
                                                  owner)
        return(createFunction)

    def balCreateFnComposableStablePoolFactory(self, poolData):
        factory = self.balLoadContract("ComposableStablePoolFactory")
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        owner = self.balSetOwner(poolData)

        rateProviders = [self.web3.toChecksumAddress(
            poolData["tokens"][token]["rateProvider"]) for token in tokens]
        tokenRateCacheDurations = [
            int(poolData["tokens"][token]["tokenRateCacheDuration"]) for token in tokens]
        exemptFromYieldProtocolFeeFlags = [bool(
            poolData["tokens"][token]["exemptFromYieldProtocolFeeFlags"]) for token in tokens]

        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  checksumTokens,
                                                  int(poolData["amplificationParameter"]),
                                                  rateProviders,
                                                  tokenRateCacheDurations,
                                                  exemptFromYieldProtocolFeeFlags,
                                                  swapFeePercentage,
                                                  owner)
        return(createFunction)

    def balCreateFnLinearPoolFactory(self, poolData, factoryName):
        factory = self.balLoadContract(factoryName)
        (tokens, checksumTokens) = self.balSortTokens(
            list(poolData["tokens"].keys()))
        swapFeePercentage = int(
            Decimal(poolData["swapFeePercent"]) * Decimal(1e16))
        owner = self.balSetOwner(poolData)

        mainToken = None
        wrappedToken = None
        for token in poolData["tokens"].keys():
            if poolData["tokens"][token]["isWrappedToken"]:
                wrappedToken = token
            else:
                mainToken = token

        if mainToken == wrappedToken:
            self.ERROR(
                "AaveLinearPool must have one wrappedToken and one mainToken. Please check your inputs. Quitting...")
            return(False)

        upperTarget = int(poolData["upperTarget"])
        createFunction = factory.functions.create(poolData["name"],
                                                  poolData["symbol"],
                                                  self.web3.toChecksumAddress(
                                                      mainToken),
                                                  self.web3.toChecksumAddress(
                                                      wrappedToken),
                                                  upperTarget,
                                                  swapFeePercentage,
                                                  owner,
                                                  int(poolData["protocolId"]))
        return(createFunction)

    def balCreateFnAaveLinearPoolFactory(self, poolData):
        return(self.balCreateFnLinearPoolFactory(poolData, "AaveLinearPoolFactory"))

    def balCreateFnERC4626LinearPoolFactory(self, poolData):
        return(self.balCreateFnLinearPoolFactory(poolData, "ERC4626LinearPoolFactory"))

    def balCreatePoolInFactory(self, poolDescription, gasFactor, gasPriceSpeed, nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        createFunction = None
        poolFactoryName = poolDescription["poolType"] + "Factory"

        # list of all supported pool factories
        # NOTE: when you add a pool factory to this list, be sure to
        # 		add it to the printout of supported factories below
        if poolFactoryName == "WeightedPoolFactory":
            createFunction = self.balCreateFnWeightedPoolFactory(
                poolDescription)
        if poolFactoryName == "WeightedPool2TokensFactory":
            createFunction = self.balCreateFnWeightedPool2TokensFactory(
                poolDescription)
        if poolFactoryName == "StablePoolFactory":
            createFunction = self.balCreateFnStablePoolFactory(poolDescription)
        if poolFactoryName == "LiquidityBootstrappingPoolFactory":
            createFunction = self.balCreateFnLBPoolFactory(poolDescription)
        if poolFactoryName == "MetaStablePoolFactory":
            createFunction = self.balCreateFnMetaStablePoolFactory(
                poolDescription)
        if poolFactoryName == "InvestmentPoolFactory":
            createFunction = self.balCreateFnInvestmentPoolFactory(
                poolDescription)
        if poolFactoryName == "ManagedPoolFactory":
            createFunction = self.balCreateFnManagedPoolFactory(
                poolDescription)
        if poolFactoryName == "StablePhantomPoolFactory":
            createFunction = self.balCreateFnStablePhantomPoolFactory(
                poolDescription)
        if poolFactoryName == "ComposableStablePoolFactory":
            createFunction = self.balCreateFnComposableStablePoolFactory(
                poolDescription)
        if poolFactoryName == "AaveLinearPoolFactory":
            createFunction = self.balCreateFnAaveLinearPoolFactory(
                poolDescription)
        if poolFactoryName == "ERC4626LinearPoolFactory":
            createFunction = self.balCreateFnERC4626LinearPoolFactory(
                poolDescription)
        if poolFactoryName == "NoProtocolFeeLiquidityBootstrappingPoolFactory":
            createFunction = self.balCreateFnNoProtocolFeeLiquidityBootstrappingPoolFactory(
                poolDescription)
        if createFunction is None:
            print("No pool factory found with name:", poolFactoryName)
            print("Currently supported pool types are:")
            print("\tWeightedPool")
            print("\tWeightedPool2Token")
            print("\tStablePool")
            print("\tLiquidityBootstrappingPool")
            print("\tMetaStablePool")
            print("\tInvestmentPool")
            print("\tStablePhantomPool")
            print("\tComposableStablePoolFactory")
            print("\tAaveLinearPool")
            print("\tERC4626LinearPoolFactory")
            print("\tNoProtocolFeeLiquidityBootstrappingPoolFactory")
            return(False)

        if not createFunction:
            self.ERROR("Pool creation failed.")
            return(False)
        print("Pool function created, generating transaction...")
        tx = self.buildTx(createFunction, gasFactor, gasPriceSpeed,
                          nonceOverride, gasEstimateOverride, gasPriceGweiOverride)
        print("Transaction Generated!")
        txHash = self.sendTx(tx)
        return(txHash)

    def balGetPoolIdFromHash(self, txHash):
        receipt = self.getTxReceipt(txHash, delay=2, maxRetries=5)

        # PoolRegistered event lives in the Vault
        vault = self.balLoadContract("Vault")

        with Suppressor():
            logs = vault.events.PoolRegistered().processReceipt(receipt)
            poolId = logs[0]['args']['poolId'].hex()

        print("Your pool ID is:")
        print("\t0x" + str(poolId))
        return(poolId)

    def balFindPoolFactory(self, poolId):
        contractNames = self.deploymentAddresses.keys()
        # can't simply use "PoolFactory" b/c of WeightedPool2TokensFactory
        factoryNames = [c for c in contractNames if (
            "Factory" in c) and ("Pool" in c)]

        self.mc.reset()
        for factoryName in factoryNames:
            factory = self.balLoadContract(factoryName)
            poolAddress = self.balPooldIdToAddress(poolId)
            self.mc.addCall(factory.address, factory.abi,
                            "isPoolFromFactory", args=[poolAddress])
        data = self.mc.execute()

        foundFactoryName = []
        numFound = 0
        for f, d in zip(factoryNames, data[0]):
            if d[0]:
                foundFactoryName.append(f)
                numFound += 1

        if numFound == 1:
            return(foundFactoryName[0])
        else:
            self.ERROR("Was expecting 1 factory, got " + str(numFound))
            self.ERROR(",".join(foundFactoryName))
            self.ERROR("Checked the following factories:\n\t\t" +
                       "\n\t\t".join(factoryNames))
            return(None)

    def balGetJoinKindEnum(self, poolId, joinKind):
        factoryName = self.balFindPoolFactory(poolId)

        usingWeighted = factoryName in ["WeightedPoolFactory", "WeightedPool2TokensFactory", "LiquidityBootstrappingPoolFactory",
                                        "InvestmentPoolFactory", "NoProtocolFeeLiquidityBootstrappingPoolFactory", "ManagedPoolFactory"]
        usingStable = factoryName in [
            "StablePoolFactory", "MetaStablePoolFactory"]
        usingStablePhantom = factoryName in [
            "StablePhantomPoolFactory", "ComposableStablePoolFactory"]

        if usingWeighted:
            joinKindEnum = WeightedPoolJoinKind[joinKind]
        elif usingStable:
            joinKindEnum = StablePoolJoinKind[joinKind]
        elif usingStablePhantom:
            joinKindEnum = StablePhantomPoolJoinKind[joinKind]
        else:
            self.ERROR("PoolType " + str(factoryName) +
                       " not supported for JoinKind: " + joinKind)
            return(None)
        return(joinKindEnum)

    def balGetTokensAndAmounts(self, joinDescription):
        (sortedTokens, checksumTokens) = self.balSortTokens(
            list(joinDescription["tokens"].keys()))
        amountKey = "amount"
        if not amountKey in joinDescription["tokens"][list(joinDescription["tokens"].keys())[0]].keys():
            amountKey = "initialBalance"
        amountsBySortedTokens = [joinDescription["tokens"]
                                 [token][amountKey] for token in sortedTokens]
        maxAmountsIn = self.balConvertTokensToWei(
            sortedTokens, amountsBySortedTokens)
        return(checksumTokens, maxAmountsIn)

    def balGetTokensAndAmountsComposable(self, joinDescription):
        (checksumTokens, maxAmountsIn) = self.balGetTokensAndAmounts(joinDescription)

        poolAddress = self.balPooldIdToAddress(joinDescription["poolId"])
        poolAddress = self.web3.toChecksumAddress(poolAddress)

        composableAmount = None

        userDataMaxAmountIn = copy.deepcopy(maxAmountsIn)

        for i in range(len(checksumTokens)):
            if checksumTokens[i] == poolAddress:
                composableAmount = maxAmountsIn[i]
                del checksumTokens[i]
                del maxAmountsIn[i]
                del userDataMaxAmountIn[i]
                break

        checksumTokens.insert(0, poolAddress)
        maxAmountsIn.insert(0, composableAmount)

        return(checksumTokens, maxAmountsIn, userDataMaxAmountIn)

    def balDoJoinPool(self, poolId, address, joinPoolRequestTuple, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        vault = self.balLoadContract("Vault")
        joinPoolFunction = vault.functions.joinPool(
            poolId, address, address, joinPoolRequestTuple)
        tx = self.buildTx(joinPoolFunction, gasFactor, gasPriceSpeed,
                          nonceOverride, gasEstimateOverride, gasPriceGweiOverride)
        print("Transaction Generated!")
        txHash = self.sendTx(tx)
        return(txHash)

    def balDoQueryJoinPool(self, poolId, address, joinPoolRequestTuple):
        bh = self.balLoadContract("BalancerHelpers")
        (bptOut, amountsIn) = bh.functions.queryJoin(
            poolId, address, address, joinPoolRequestTuple).call()
        return((bptOut, amountsIn))

    def balFormatJoinPoolInit(self, joinDescription):
        (checksumTokens, maxAmountsIn) = self.balGetTokensAndAmounts(
            copy.deepcopy(joinDescription))

        poolId = joinDescription["poolId"]
        factory = self.balFindPoolFactory(poolId)

        userDataMaxAmountsIn = maxAmountsIn
        if factory in ["ManagedPoolFactory"]:
            (checksumTokens, maxAmountsIn, userDataMaxAmountsIn) = self.balGetTokensAndAmountsComposable(
                copy.deepcopy(joinDescription))

        joinKindEnum = self.balGetJoinKindEnum(
            poolId, joinDescription["joinKind"])
        userDataEncoded = eth_abi.encode_abi(['uint256', 'uint256[]'],
                                             [int(joinKindEnum), userDataMaxAmountsIn])
        address = self.web3.toChecksumAddress(self.web3.eth.default_account)
        joinPoolRequestTuple = (checksumTokens, maxAmountsIn, userDataEncoded.hex(
        ), joinDescription["fromInternalBalance"])
        return(poolId, address, joinPoolRequestTuple)

    def balFormatJoinPoolExactTokensInForBptOut(self, joinDescription):
        (checksumTokens, maxAmountsIn) = self.balGetTokensAndAmounts(
            copy.deepcopy(joinDescription))
        poolId = joinDescription["poolId"]
        joinKindEnum = self.balGetJoinKindEnum(
            poolId, joinDescription["joinKind"])
        userDataEncoded = eth_abi.encode_abi(['uint256', 'uint256[]'],
                                             [int(joinKindEnum), maxAmountsIn])
        address = self.web3.toChecksumAddress(self.web3.eth.default_account)
        joinPoolRequestTuple = (checksumTokens, maxAmountsIn, userDataEncoded.hex(
        ), joinDescription["fromInternalBalance"])
        return(poolId, address, joinPoolRequestTuple)

    def balFormatJoinPoolAllTokensInForExactBptOut(self, joinDescription):
        (checksumTokens, maxAmountsIn) = self.balGetTokensAndAmounts(
            copy.deepcopy(joinDescription))
        poolId = joinDescription["poolId"]
        poolAddress = self.balPooldIdToAddress(poolId)
        bptAmountOut = self.balConvertTokensToWei(
            [poolAddress], [joinDescription["bptAmountOut"]])[0]

        joinKindEnum = self.balGetJoinKindEnum(
            poolId, joinDescription["joinKind"])
        userDataEncoded = eth_abi.encode_abi(['uint256', 'uint256'],
                                             [int(joinKindEnum), bptAmountOut])
        address = self.web3.toChecksumAddress(self.web3.eth.default_account)
        joinPoolRequestTuple = (checksumTokens, maxAmountsIn, userDataEncoded.hex(
        ), joinDescription["fromInternalBalance"])
        return(poolId, address, joinPoolRequestTuple)

    def balFormatJoinPoolTokenInForExactBptOut(self, joinDescription):
        (checksumTokens, maxAmountsIn) = self.balGetTokensAndAmounts(
            copy.deepcopy(joinDescription))

        index = -1
        counter = -1
        for amt in maxAmountsIn:
            counter += 1
            if amt > 0:
                if index == -1:
                    index = counter
                else:
                    self.ERROR(
                        "Multiple tokens have amounts for a single token join! Only one token can have a non-zero amount!")
                    return(False)
        if index == -1:
            self.ERROR(
                "No tokens have amounts. You must have one token with a non-zero amount!")
            return(False)

        poolId = joinDescription["poolId"]
        poolAddress = self.balPooldIdToAddress(poolId)
        bptAmountOut = self.balConvertTokensToWei(
            [poolAddress], [joinDescription["bptAmountOut"]])[0]

        joinKindEnum = self.balGetJoinKindEnum(
            poolId, joinDescription["joinKind"])
        userDataEncoded = eth_abi.encode_abi(['uint256', 'uint256', 'uint256'],
                                             [int(joinKindEnum), bptAmountOut, index])

        address = self.web3.toChecksumAddress(self.web3.eth.default_account)
        joinPoolRequestTuple = (checksumTokens, maxAmountsIn, userDataEncoded.hex(
        ), joinDescription["fromInternalBalance"])

        return(poolId, address, joinPoolRequestTuple)

    def balJoinPool(self, joinDescription, query=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        joinKind = joinDescription["joinKind"]
        poolId = None
        address = None
        joinPoolRequestTuple = None

        if joinKind == "INIT":
            (poolId, address, joinPoolRequestTuple) = self.balFormatJoinPoolInit(
                joinDescription)
        elif joinKind == "EXACT_TOKENS_IN_FOR_BPT_OUT":
            (poolId, address, joinPoolRequestTuple) = self.balFormatJoinPoolExactTokensInForBptOut(
                joinDescription)
        elif joinKind == "TOKEN_IN_FOR_EXACT_BPT_OUT":
            tempJoinDescription = copy.deepcopy(joinDescription)
            if query:
                for token in tempJoinDescription["tokens"].keys():
                    if tempJoinDescription["tokens"][token]["amount"] == 0.0:
                        tempJoinDescription["tokens"][token]["amount"] = self.INFINITE
            (poolId, address, joinPoolRequestTuple) = self.balFormatJoinPoolTokenInForExactBptOut(
                tempJoinDescription)
        elif joinKind == "ALL_TOKENS_IN_FOR_EXACT_BPT_OUT":
            tempJoinDescription = copy.deepcopy(joinDescription)
            if query:
                for token in tempJoinDescription["tokens"].keys():
                    tempJoinDescription["tokens"][token]["amount"] = self.INFINITE
            (poolId, address, joinPoolRequestTuple) = self.balFormatJoinPoolAllTokensInForExactBptOut(
                tempJoinDescription)
            print((poolId, address, joinPoolRequestTuple))

        if query:
            (bptOut, amountsIn) = self.balDoQueryJoinPool(
                poolId, address, joinPoolRequestTuple)
            bptAddress = self.balPooldIdToAddress(poolId)
            outputData = {}
            outputData["bptOut"] = {
                "token": bptAddress,
                "decimals": self.erc20GetDecimals(bptAddress),
                "amount": bptOut
            }
            outputData["amountsIn"] = {}
            for token, amount in zip(joinPoolRequestTuple[0], amountsIn):
                outputData["amountsIn"][token] = {
                    "token": token,
                    "decimals": self.erc20GetDecimals(token),
                    "amount": amount
                }
            return(outputData)
        else:
            txHash = self.balDoJoinPool(poolId, address, joinPoolRequestTuple, gasFactor=gasFactor, gasPriceSpeed=gasPriceSpeed,
                                        nonceOverride=nonceOverride, gasEstimateOverride=gasEstimateOverride, gasPriceGweiOverride=gasPriceGweiOverride)
            return(txHash)

    def balRegisterPoolWithVault(self, poolDescription, poolId, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        self.WARN(
            "\"balRegisterPoolWithVault\" is deprecated. Please use \"balJoinPoolInit\".")
        self.balJoinPoolInit(poolDescription, poolId, gasFactor, gasPriceSpeed,
                             nonceOverride, gasEstimateOverride, gasPriceGweiOverride)

    def balJoinPoolInit(self, poolDescription, poolId, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):

        if poolDescription["poolType"] in ["AaveLinearPool", "ERC4626LinearPool"]:
            slippageTolerancePercent = 1
            txHash = self.balLinearPoolInitJoin(poolDescription, poolId, slippageTolerancePercent=slippageTolerancePercent, gasFactor=gasFactor,
                                                gasPriceSpeed=gasPriceSpeed, nonceOverride=nonceOverride, gasEstimateOverride=gasEstimateOverride, gasPriceGweiOverride=gasPriceGweiOverride)
            return(txHash)

        # StablePhantomPools need their own BPT as one of the provided tokens with a limit of MAX_UINT_112
        if poolDescription["poolType"] in ["StablePhantomPool", "ComposableStablePool", "ManagedPool"]:
            initialBalancesNoBpt = [poolDescription["tokens"][token]["initialBalance"]
                                    for token in poolDescription["tokens"].keys()]
            phantomBptAddress = self.balPooldIdToAddress(poolId)
            poolDescription["tokens"][phantomBptAddress] = {
                "initialBalance": self.MAX_UINT_112}

        poolDescription["joinKind"] = "INIT"
        return(self.balJoinPool(poolDescription, False, gasFactor, gasPriceSpeed, nonceOverride, gasEstimateOverride, gasPriceGweiOverride))

    def balLinearPoolInitJoin(self, poolDescription, poolId, slippageTolerancePercent=1, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):

        phantomBptAddress = self.balPooldIdToAddress(poolId)

        batchSwap = {}
        batchSwap["kind"] = 0
        batchSwap["assets"] = list(poolDescription["tokens"].keys())
        batchSwap["swaps"] = []

        numTokens = len(batchSwap["assets"])
        for i in range(numTokens):
            token = batchSwap["assets"][i]
            swap = {}
            swap["poolId"] = "0x" + poolId
            swap["assetInIndex"] = i
            swap["assetOutIndex"] = numTokens
            swap["amount"] = poolDescription["tokens"][token]["initialBalance"]
            if float(swap["amount"]) > 0.0:
                batchSwap["swaps"].append(swap)

        # add the phantomBpt to the assets/limits list now that we've crafted the swap steps
        batchSwap["assets"].append(phantomBptAddress)
        batchSwap["limits"] = [0] * len(batchSwap["assets"])  # for now

        batchSwap["funds"] = {}
        batchSwap["funds"]["sender"] = self.address
        batchSwap["funds"]["recipient"] = self.address
        batchSwap["funds"]["fromInternalBalance"] = False
        batchSwap["funds"]["toInternalBalance"] = False
        batchSwap["deadline"] = "999999999999999999"

        estimates = self.balQueryBatchSwap(batchSwap)

        checksumTokens = [self.web3.toChecksumAddress(
            t) for t in batchSwap["assets"]]
        for i in range(len(batchSwap["assets"])):
            asset = checksumTokens[i]
            slippageToleranceFactor = slippageTolerancePercent/100.0
            if estimates[asset] < 0:
                slippageToleranceFactor *= -1.0
            batchSwap["limits"][i] = estimates[asset] * \
                (1.0 + slippageToleranceFactor)

        txHash = self.balDoBatchSwap(batchSwap, isAsync=False, gasFactor=gasFactor, gasPriceSpeed=gasPriceSpeed,
                                     nonceOverride=nonceOverride, gasEstimateOverride=gasEstimateOverride, gasPriceGweiOverride=gasPriceGweiOverride)
        return(txHash)

    def balVaultWeth(self):
        vault = self.balLoadContract("Vault")
        wethAddress = vault.functions.WETH().call()
        return(wethAddress)

    def balBalancerHelpersGetVault(self):
        bh = self.balLoadContract("BalancerHelpers")
        vaultAddress = bh.functions.vault().call()
        return(vaultAddress)

    def balVaultGetPoolTokens(self, poolId):
        vault = self.balLoadContract("Vault")
        output = vault.functions.getPoolTokens(poolId).call()
        tokens = output[0]
        balances = output[1]
        lastChangeBlock = output[2]
        return (tokens, balances, lastChangeBlock)

    def balVaultGetInternalBalance(self, tokens, address=None):
        if address is None:
            address = self.web3.eth.default_account

        vault = self.balLoadContract("Vault")
        (sortedTokens, checksumTokens) = self.balSortTokens(tokens)
        balances = vault.functions.getInternalBalance(
            address, checksumTokens).call()
        numElements = len(sortedTokens)
        internalBalances = {}
        for i in range(numElements):
            token = checksumTokens[i]
            decimals = self.erc20GetDecimals(token)
            internalBalances[token] = Decimal(
                balances[i]) * Decimal(10**(-decimals))
        return(internalBalances)

    def balVaultDoManageUserBalance(self, kind, token, amount, sender, recipient, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        if self.verbose:
            print("Managing User Balance")
            print("\tKind:\t\t", self.inverseUserBalanceOpKind[kind])
            print("\tToken:\t\t", str(token))
            print("\tAmount:\t\t", str(amount))
            print("\tSender:\t\t", str(sender))
            print("\tRecipient:\t", str(recipient))
        manageUserBalanceFn = self.balVaultBuildManageUserBalanceFn(
            kind, token, amount, sender, recipient)

        print()
        print("Building ManageUserBalance")
        tx = self.buildTx(manageUserBalanceFn, gasFactor, gasPriceSpeed,
                          nonceOverride, gasEstimateOverride, gasPriceGweiOverride)
        txHash = self.sendTx(tx, isAsync)
        return(txHash)

    def balVaultBuildManageUserBalanceFn(self, kind, token, amount, sender, recipient):
        kind = kind
        asset = self.web3.toChecksumAddress(token)
        amount = self.balConvertTokensToWei([token], [amount])[0]
        sender = self.web3.toChecksumAddress(sender)
        recipient = self.web3.toChecksumAddress(recipient)
        inputTupleList = [(kind, asset, amount, sender, recipient)]

        vault = self.balLoadContract("Vault")
        manageUserBalanceFn = vault.functions.manageUserBalance(inputTupleList)
        return(manageUserBalanceFn)

    @cache
    def balLoadContract(self, contractName):
        contract = self.web3.eth.contract(
            address=self.deploymentAddresses[contractName], abi=self.abis[contractName])
        return(contract)

    @cache
    def balLoadArbitraryContract(self, address, abi):
        contract = self.web3.eth.contract(
            address=address, abi=self.mc.stringToList(abi))
        return(contract)

    @cache
    def balPoolGetAbi(self, poolType):

        if not "Pool" in poolType:
            poolType = poolType + "Pool"

        deploymentFolder = self.contractDirectories[poolType +
                                                    "Factory"]["directory"]
        abiPath = os.path.join(
            "deployments/", deploymentFolder, "abi", poolType + ".json")
        f = pkgutil.get_data(__name__, abiPath).decode()
        poolAbi = json.loads(f)
        return(poolAbi)

    @cache
    def balPooldIdToAddress(self, poolId):
        if not "0x" in poolId:
            poolId = "0x" + poolId
        poolAddress = self.web3.toChecksumAddress(poolId[:42])
        return(poolAddress)

    def balGetPoolCreationData(self, poolId, verbose=False, inputHash=None):
        address = self.balPooldIdToAddress(poolId)
        if inputHash is None:
            txns = self.getTransactionsByAddress(
                address, internal=True, verbose=verbose)
        else:
            txns = self.getTransactionByHash(inputHash, verbose=verbose)

        poolTypeByContract = {}
        for poolType in self.deploymentAddresses.keys():
            deploymentAddress = self.deploymentAddresses[poolType].lower()
            poolTypeByContract[deploymentAddress] = poolType

        poolFactoryType = None
        if inputHash is None:
            for txn in txns:
                if txn["from"].lower() in poolTypeByContract.keys():
                    poolFactoryType = poolTypeByContract[txn["from"].lower()]
                    txHash = txn["hash"]
                    stamp = txn["timeStamp"]
                    break
        else:
            txn = txns["result"]
            if verbose:
                print()
                print(txn)
            poolFactoryType = poolTypeByContract[txn["to"].lower()]
            txHash = txn["hash"]
            stamp = self.web3.eth.get_block(
                int(txn["blockNumber"], 16))["timestamp"]

        return(address, poolFactoryType, txHash, stamp)

    def balGetPoolFactoryCreationTime(self, address):
        txns = self.getTransactionsByAddress(address)
        return(txns[0]["timeStamp"])

    def getInputData(self, txHash):
        transaction = self.web3.eth.get_transaction(txHash)
        return(transaction.input)

    def balGeneratePoolCreationArguments(self, poolId, verbose=False, creationHash=None):
        if self.network in ["arbitrum"]:
            self.ERROR("Automated pool verification doesn't work on " + self.network +
                       " yet. Please try the method outlined in the docs using Tenderly.")
            return(False)

        # query etherscan for internal transactions to find pool factory, pool creation time, and creation hash
        (address, poolFactoryType, txHash, stampPool) = self.balGetPoolCreationData(
            poolId, verbose=verbose, inputHash=creationHash)

        # get the input data used to generate the pool
        inputData = self.getInputData(txHash)

        # decode those ^ inputs according to the relevant pool factory ABI
        poolFactoryContract = self.balLoadContract(poolFactoryType)
        decodedPoolData = poolFactoryContract.decode_function_input(inputData)[
            1]

        # get pool factory creation time to calculate pauseWindowDuration
        stampFactory = self.balGetPoolFactoryCreationTime(
            poolFactoryContract.address)

        # make sure arguments exist/are proper types to be encoded
        if "weights" in decodedPoolData.keys():
            for i in range(len(decodedPoolData["weights"])):
                decodedPoolData["weights"][i] = int(
                    decodedPoolData["weights"][i])
        if "priceRateCacheDuration" in decodedPoolData.keys():
            for i in range(len(decodedPoolData["priceRateCacheDuration"])):
                decodedPoolData["priceRateCacheDuration"][i] = int(
                    decodedPoolData["priceRateCacheDuration"][i])
        if poolFactoryType == "InvestmentPoolFactory" and not "assetManagers" in decodedPoolData.keys():
            decodedPoolData["assetManagers"] = []
            for i in range(len(decodedPoolData["weights"])):
                decodedPoolData["assetManagers"].append(self.ZERO_ADDRESS)

        # times for pause/buffer
        daysToSec = 24*60*60  # hr * min * sec
        pauseDays = 90
        bufferPeriodDays = 30

        # calculate proper durations
        pauseWindowDurationSec = max(
            (pauseDays*daysToSec) - (int(stampPool) - int(stampFactory)), 0)
        bufferPeriodDurationSec = bufferPeriodDays * daysToSec
        if pauseWindowDurationSec == 0:
            bufferPeriodDurationSec = 0

        poolType = poolFactoryType.replace("Factory", "")
        poolAbi = self.balPoolGetAbi(poolType)

        structInConstructor = False
        if poolType == "WeightedPool":
            zero_ams = [self.ZERO_ADDRESS] * len(decodedPoolData["tokens"])
            args = [(decodedPoolData["name"],
                     decodedPoolData["symbol"],
                     decodedPoolData["tokens"],
                     decodedPoolData["normalizedWeights"],
                     decodedPoolData["rateProviders"],
                     zero_ams,
                     int(decodedPoolData["swapFeePercentage"])),
                    self.deploymentAddresses["Vault"],
                    self.deploymentAddresses["ProtocolFeePercentagesProvider"],
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["owner"]]
        elif poolType == "WeightedPool2Tokens":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["tokens"][0],
                    decodedPoolData["tokens"][1],
                    int(decodedPoolData["weights"][0]),
                    int(decodedPoolData["weights"][1]),
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["oracleEnabled"],
                    decodedPoolData["owner"]]
            structInConstructor = True
        elif poolType == "StablePool":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["tokens"],
                    int(decodedPoolData["amplificationParameter"]),
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["owner"]]
        elif poolType == "MetaStablePool":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["tokens"],
                    decodedPoolData["rateProviders"],
                    decodedPoolData["priceRateCacheDuration"],
                    int(decodedPoolData["amplificationParameter"]),
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["oracleEnabled"],
                    decodedPoolData["owner"]]
            structInConstructor = True
        elif poolType == "LiquidityBootstrappingPool":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["tokens"],
                    decodedPoolData["weights"],
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["owner"],
                    decodedPoolData["swapEnabledOnStart"]]
        elif poolType == "InvestmentPool":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["tokens"],
                    decodedPoolData["weights"],
                    decodedPoolData["assetManagers"],
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["owner"],
                    decodedPoolData["swapEnabledOnStart"],
                    int(decodedPoolData["managementSwapFeePercentage"])]
            structInConstructor = True
        elif poolType == "StablePhantomPool":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["tokens"],
                    decodedPoolData["rateProviders"],
                    decodedPoolData["tokenRateCacheDurations"],
                    int(decodedPoolData["amplificationParameter"]),
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["owner"]]
            structInConstructor = True
        elif poolType == "AaveLinearPool":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["mainToken"],
                    decodedPoolData["wrappedToken"],
                    int(decodedPoolData["upperTarget"]),
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["owner"]]
        elif poolType == "ERC4626LinearPool":
            args = [self.deploymentAddresses["Vault"],
                    decodedPoolData["name"],
                    decodedPoolData["symbol"],
                    decodedPoolData["mainToken"],
                    decodedPoolData["wrappedToken"],
                    int(decodedPoolData["upperTarget"]),
                    int(decodedPoolData["swapFeePercentage"]),
                    int(pauseWindowDurationSec),
                    int(bufferPeriodDurationSec),
                    decodedPoolData["owner"]]
        else:
            self.ERROR("PoolType " + poolType + " not found!")
            return(False)

        # encode constructor data
        poolContract = self.balLoadArbitraryContract(
            address, self.mc.listToString(poolAbi))
        if structInConstructor:
            args = (tuple(args),)
        data = poolContract._encode_constructor_data(args=args)
        encodedData = data[2:]  # cut off the 0x

        command = "yarn hardhat verify-contract --id {} --name {} --address {} --network {} --key {} --args {}"
        output = command.format(self.contractDirectories[poolFactoryType]["directory"],
                                poolType,
                                address,
                                self.network,
                                self.etherscanApiKey,
                                encodedData)
        return(output)

    def balStablePoolGetAmplificationParameter(self, poolId):
        poolAddress = self.web3.toChecksumAddress(poolId[:42])
        pool = self.web3.eth.contract(
            address=poolAddress, abi=self.balPoolGetAbi("StablePool"))
        (value, isUpdating, precision) = pool.functions.getAmplificationParameter().call()
        return(value, isUpdating, precision)

    def balStablePoolStartAmplificationParameterUpdate(self, poolId, rawEndValue, endTime, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        poolAddress = self.web3.toChecksumAddress(poolId[:42])
        pool = self.web3.eth.contract(
            address=poolAddress, abi=self.balPoolGetAbi("StablePool"))

        owner = pool.functions.getOwner().call()
        if not self.address == owner:
            self.ERROR(
                "You are not the pool owner; this transaction will fail.")
            return(False)

        fn = pool.functions.startAmplificationParameterUpdate(
            rawEndValue, endTime)
        tx = self.buildTx(fn, gasFactor, gasPriceSpeed, nonceOverride,
                          gasEstimateOverride, gasPriceGweiOverride)
        txHash = self.sendTx(tx, isAsync)
        return(txHash)

    # https://dev.balancer.fi/references/contracts/apis/pools/weightedpool2tokens#gettimeweightedaverage
    def balOraclePoolGetTimeWeightedAverage(self, poolId, queries):
        poolAddress = self.web3.toChecksumAddress(poolId[:42])
        pool = self.web3.eth.contract(
            address=poolAddress, abi=self.balPoolGetAbi("WeightedPool2Tokens"))
        results = pool.functions.getTimeWeightedAverage(queries).call()
        return(results)

    def balSwapIsFlashSwap(self, swapDescription):
        for amount in swapDescription["limits"]:
            if not float(amount) == 0.0:
                return(False)
        return(True)

    def balReorderTokenDicts(self, tokens):
        originalIdxToSortedIdx = {}
        sortedIdxToOriginalIdx = {}
        tokenAddressToIdx = {}
        for i in range(len(tokens)):
            tokenAddressToIdx[tokens[i]] = i
        sortedTokens = tokens
        sortedTokens.sort()
        for i in range(len(sortedTokens)):
            originalIdxToSortedIdx[tokenAddressToIdx[sortedTokens[i]]] = i
            sortedIdxToOriginalIdx[i] = tokenAddressToIdx[sortedTokens[i]]
        return(sortedTokens, originalIdxToSortedIdx, sortedIdxToOriginalIdx)

    def balSwapGetUserData(self, poolType):
        userDataNull = eth_abi.encode_abi(['uint256'], [0])
        userData = userDataNull
        # for weightedPools, user data is just null, but in the future there may be userData to pass to pools for swaps
        # if poolType == "someFuturePool":
        # 	userData = "something else";
        return(userData)

    def balDoSwap(self, swapDescription, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        swapFn = self.balCreateFnSwap(swapDescription)
        tx = self.buildTx(swapFn, gasFactor, gasPriceSpeed,
                          nonceOverride, gasEstimateOverride, gasPriceGweiOverride)
        txHash = self.sendTx(tx, isAsync)
        return(txHash)

    def balDoBatchSwap(self, swapDescription, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
        batchSwapFn = self.balCreateFnBatchSwap(swapDescription)
        tx = self.buildTx(batchSwapFn, gasFactor, gasPriceSpeed,
                          nonceOverride, gasEstimateOverride, gasPriceGweiOverride)
        txHash = self.sendTx(tx, isAsync)
        return(txHash)

    def balCreateFnSwap(self, swapDescription):
        kind = int(swapDescription["kind"])
        limitedToken = None
        amountToken = None
        if kind == 0:  # GIVEN_IN
            amountToken = swapDescription["assetIn"]
            limitedToken = swapDescription["assetOut"]
        elif kind == 1:  # GIVEN_OUT
            amountToken = swapDescription["assetOut"]
            limitedToken = swapDescription["assetIn"]

        amountWei = int(Decimal(
            swapDescription["amount"]) * 10 ** Decimal(self.erc20GetDecimals(amountToken)))
        limitWei = int(Decimal(
            swapDescription["limit"]) * 10 ** Decimal(self.erc20GetDecimals(limitedToken)))

        swapStruct = (
            swapDescription["poolId"],
            kind,
            self.web3.toChecksumAddress(swapDescription["assetIn"]),
            self.web3.toChecksumAddress(swapDescription["assetOut"]),
            amountWei,
            self.balSwapGetUserData(None)
        )
        fundStruct = (
            self.web3.toChecksumAddress(swapDescription["fund"]["sender"]),
            swapDescription["fund"]["fromInternalBalance"],
            self.web3.toChecksumAddress(swapDescription["fund"]["recipient"]),
            swapDescription["fund"]["toInternalBalance"]
        )
        vault = self.balLoadContract("Vault")
        singleSwapFunction = vault.functions.swap(
            swapStruct,
            fundStruct,
            limitWei,
            int(swapDescription["deadline"])
        )
        return(singleSwapFunction)

    def balFormatBatchSwapData(self, swapDescription):
        (sortedTokens, originalIdxToSortedIdx,
         sortedIdxToOriginalIdx) = self.balReorderTokenDicts(swapDescription["assets"])
        numTokens = len(sortedTokens)

        # reorder the limits to refer to properly sorted tokens
        reorderedLimits = []
        for i in range(numTokens):
            currLimitStandard = float(
                swapDescription["limits"][sortedIdxToOriginalIdx[i]])
            decimals = self.erc20GetDecimals(sortedTokens[i])
            currLimitRaw = int(Decimal(currLimitStandard)
                               * Decimal(10**(decimals)))
            reorderedLimits.append(currLimitRaw)

        kind = int(swapDescription["kind"])
        assets = [self.web3.toChecksumAddress(token) for token in sortedTokens]

        swapsTuples = []
        for swap in swapDescription["swaps"]:
            idxSortedIn = originalIdxToSortedIdx[int(swap["assetInIndex"])]
            idxSortedOut = originalIdxToSortedIdx[int(swap["assetOutIndex"])]

            idxTokenAmount = idxSortedIn
            if kind == 1:
                idxTokenAmount = idxSortedOut

            decimals = self.erc20GetDecimals(sortedTokens[idxTokenAmount])
            amount = int(Decimal(swap["amount"]) * Decimal(10**(decimals)))

            swapsTuple = (swap["poolId"],
                          idxSortedIn,
                          idxSortedOut,
                          amount,
                          self.balSwapGetUserData(None))
            swapsTuples.append(swapsTuple)

        funds = (self.web3.toChecksumAddress(swapDescription["funds"]["sender"]),
                 swapDescription["funds"]["fromInternalBalance"],
                 self.web3.toChecksumAddress(
                     swapDescription["funds"]["recipient"]),
                 swapDescription["funds"]["toInternalBalance"])
        intReorderedLimits = [int(element) for element in reorderedLimits]
        deadline = int(swapDescription["deadline"])
        return(kind, swapsTuples, assets, funds, intReorderedLimits, deadline)

    def balCreateFnBatchSwap(self, swapDescription):
        (kind, swapsTuples, assets, funds, intReorderedLimits,
         deadline) = self.balFormatBatchSwapData(swapDescription)
        vault = self.balLoadContract("Vault")
        batchSwapFunction = vault.functions.batchSwap(kind,
                                                      swapsTuples,
                                                      assets,
                                                      funds,
                                                      intReorderedLimits,
                                                      deadline)
        return(batchSwapFunction)

    def balQueryBatchSwaps(self, originalSwapsDescription):
        swapsDescription = copy.deepcopy(originalSwapsDescription)
        vault = self.balLoadContract("Vault")
        for swapDescription in swapsDescription:

            # do deep copy to avoid modifying the swapDescription in place, breaking index remappings
            deepCopySwapDescription = copy.deepcopy(swapDescription)
            (kind, swapsTuples, assets, funds, intReorderedLimits,
             deadline) = self.balFormatBatchSwapData(deepCopySwapDescription)
            args = [kind, swapsTuples, assets, funds]
            self.mc.addCall(vault.address, vault.abi,
                            "queryBatchSwap", args=args)
        data = self.mc.execute()

        outputs = []
        for swapDescription, outputData, successfulCall in zip(swapsDescription, data[0], data[1]):
            output = {}
            if successfulCall:
                amounts = list(outputData[0])
                for asset, amount in zip(assets, amounts):
                    if successfulCall:
                        decimals = self.erc20GetDecimals(asset)
                        output[asset] = amount * 10**(-decimals)
                    else:
                        output[asset] = None
                outputs.append(output)
            else:
                for asset in assets:
                    output[asset] = None
                outputs.append(output)

        return(outputs, data[1])

    def balQueryBatchSwap(self, originalSwapDescription):
        swapDescription = copy.deepcopy(originalSwapDescription)
        (kind, swapsTuples, assets, funds, intReorderedLimits,
         deadline) = self.balFormatBatchSwapData(swapDescription)
        vault = self.balLoadContract("Vault")
        amounts = vault.functions.queryBatchSwap(kind,
                                                 swapsTuples,
                                                 assets,
                                                 funds).call()

        output = {}
        for asset, amount in zip(assets, amounts):
            decimals = self.erc20GetDecimals(asset)
            output[asset] = amount * 10**(-decimals)
        return(output)

    def balGetLinkToFrontend(self, poolId):
        if "balFrontend" in self.networkParams[self.network].keys():
            return("https://" + self.networkParams[self.network]["balFrontend"] + "pool/0x" + poolId)
        else:
            return("")

    def multiCallErc20BatchDecimals(self, tokens):
        self.mc.reset()
        payload = []
        for token in tokens:
            currTokenContract = self.erc20GetContract(token)
            self.mc.addCall(currTokenContract.address,
                            currTokenContract.abi, 'decimals')

        # make the actual call to MultiCall
        outputData = self.mc.execute()
        tokensToDecimals = {}

        for token, odBytes in zip(tokens, outputData):
            decimals = odBytes[0]
            tokensToDecimals[token] = decimals
            self.decimals[token] = decimals
        return(tokensToDecimals)

    def getOnchainData(self, pools):
        # reset multicaller
        self.mc.reset()

        # load the vault contract
        vault = self.balLoadContract("Vault")
        target = vault.address

        poolAbis = {}
        for poolType in pools.keys():
            poolAbis[poolType] = self.balPoolGetAbi(poolType)

        payload = []
        pidAndFns = []
        outputAbis = {}

        poolToType = {}
        for poolType in pools.keys():
            poolIds = pools[poolType]
            poolAbi = poolAbis[poolType]

            # construct all the calls in format (VaultAddress, encodedCallData)
            for poolId in poolIds:
                poolToType[poolId] = poolType

                poolAddress = self.balPooldIdToAddress(poolId)
                currPool = self.balLoadArbitraryContract(
                    poolAddress, self.mc.listToString(poolAbi))

                # === all pools have tokens and swap fee, pausedState ===
                self.mc.addCall(vault.address, vault.abi,
                                'getPoolTokens', args=[poolId])
                self.mc.addCall(currPool.address, currPool.abi,
                                'getSwapFeePercentage')
                self.mc.addCall(currPool.address,
                                currPool.abi, 'getPausedState')
                pidAndFns.append((poolId, "getPoolTokens"))
                pidAndFns.append((poolId, "getSwapFeePercentage"))
                pidAndFns.append((poolId, "getPausedState"))

                # === using weighted math ===
                if poolType in ["Weighted", "LiquidityBootstrapping", "Investment"]:
                    self.mc.addCall(currPool.address,
                                    currPool.abi, 'getNormalizedWeights')
                    pidAndFns.append((poolId, "getNormalizedWeights"))

                # === using stable math ===
                if poolType in ["Stable", "MetaStable"]:
                    self.mc.addCall(currPool.address, currPool.abi,
                                    'getAmplificationParameter')
                    pidAndFns.append((poolId, "getAmplificationParameter"))

                # === have pausable swaps by pool owner ===
                if poolType in ["LiquidityBootstrapping", "Investment"]:
                    self.mc.addCall(currPool.address,
                                    currPool.abi, 'getSwapEnabled')
                    pidAndFns.append((poolId, "getSwapEnabled"))

        data = self.mc.execute()

        chainDataOut = {}
        chainDataBookkeeping = {}

        for decodedOutputData, pidAndFn in zip(data, pidAndFns):
            poolId = pidAndFn[0]
            decoder = pidAndFn[1]

            if not poolId in chainDataOut.keys():
                chainDataOut[poolId] = {"poolType": poolToType[poolId]}
                chainDataBookkeeping[poolId] = {}

            if decoder == "getPoolTokens":
                addresses = list(decodedOutputData[0])
                balances = list(decodedOutputData[1])
                lastChangeBlock = decodedOutputData[2]

                chainDataOut[poolId]["lastChangeBlock"] = lastChangeBlock
                chainDataBookkeeping[poolId]["orderedAddresses"] = addresses

                chainDataOut[poolId]["tokens"] = {}
                for address, balance in zip(addresses, balances):
                    chainDataOut[poolId]["tokens"][address] = {
                        "rawBalance": str(balance)}

            elif decoder == "getNormalizedWeights":
                normalizedWeights = list(decodedOutputData[0])
                addresses = chainDataBookkeeping[poolId]["orderedAddresses"]
                for address, normalizedWeight in zip(addresses, normalizedWeights):
                    weight = Decimal(str(normalizedWeight)) * \
                        Decimal(str(1e-18))
                    chainDataOut[poolId]["tokens"][address]["weight"] = str(
                        weight)

            elif decoder == "getSwapFeePercentage":
                swapFee = Decimal(decodedOutputData[0]) * Decimal(str(1e-18))
                chainDataOut[poolId]["swapFee"] = str(swapFee)

            elif decoder == "getAmplificationParameter":
                rawAmp = Decimal(decodedOutputData[0])
                scaling = Decimal(decodedOutputData[2])
                chainDataOut[poolId]["amp"] = str(rawAmp/scaling)

            elif decoder == "getSwapEnabled":
                chainDataOut[poolId]["swapEnabled"] = decodedOutputData[0]

            elif decoder == "getPausedState":
                chainDataOut[poolId]["pausedState"] = decodedOutputData[0]

        # find tokens for which decimals have not been cached
        tokens = set()
        for pool in chainDataOut.keys():
            for token in chainDataOut[pool]["tokens"].keys():
                tokens.add(token)
        haveDecimalsFor = set(self.decimals.keys())

        # set subtraction (A - B) gives "What is in A that isn't in B?"
        needDecimalsFor = tokens - haveDecimalsFor

        # if there are any, query them onchain using multicall
        if len(needDecimalsFor) > 0:
            print("New tokens found. Caching decimals for",
                  len(needDecimalsFor), "tokens...")
            self.multiCallErc20BatchDecimals(needDecimalsFor)

        # update rawBalances to have decimal adjusted values
        for pool in chainDataOut.keys():
            for token in chainDataOut[pool]["tokens"].keys():
                rawBalance = chainDataOut[pool]["tokens"][token]["rawBalance"]
                decimals = self.erc20GetDecimals(token)
                chainDataOut[pool]["tokens"][token]["balance"] = str(
                    Decimal(rawBalance) * Decimal(10**(-decimals)))

        return(chainDataOut)

    def generateDeploymentsDocsTable(self):
        outputString = ""
        contracts = []
        addresses = []

        network = self.network
        blockExplorer = "https://" + \
            self.networkParams[network]["blockExplorerUrl"]
        outputString += "{% tab title=\"" + network.title() + "\" %}\n"

        for contractType in self.deploymentAddresses:
            contracts.append(contractType)

            address = self.deploymentAddresses[contractType]
            etherscanPage = blockExplorer + "/address/" + address
            addresses.append("[" + address + "](" + etherscanPage + ")")

        longestContractStringLength = getLongestStringLength(contracts)
        longestAddressStringLength = getLongestStringLength(addresses)

        contractDashLine = "".join(["-"]*longestContractStringLength)
        addressDashLine = "".join(["-"]*longestAddressStringLength)

        contracts = ["Contracts", contractDashLine] + contracts
        addresses = ["Addresses", addressDashLine] + addresses

        for (c, a) in zip(contracts, addresses):
            cPadded = padWithSpaces(c, longestContractStringLength)
            aPadded = padWithSpaces(a, longestAddressStringLength)
            line = "| " + cPadded + " | " + aPadded + " |\n"
            outputString += line
        outputString += "\n{% endtab %}"
        return(outputString)
