from numpy.ma import log, sqrt, exp
from Function import Function
from approximation.Approximation import Approximation

class PowerApproximation(Approximation):
    function_type = "Степенная зависимость"

    def find_an_approximation(self, function_table: dict) -> Function:
        try:
            SLNX = sum(log(x) for x in function_table.keys())
            SLNXX = sum(log(x) * log(x) for x in function_table.keys())
            SLNY = sum(log(y) for y in function_table.values())
            SLNXY = sum(log(x) * log(y) for x, y in function_table.items())
            n = len(function_table)
        except ValueError:
            return None

        try:
            b, a = self.solve_matrix22([[n, SLNX], [SLNX, SLNXX]], [SLNY, SLNXY])
            if a is None:
                return None
            a = exp(a)
            fun = lambda x: a * (x ** b)
            s = sum((fun(x) - function_table[x]) ** 2 for x in function_table.keys())
            root_mean_square_deviation = sqrt(s / n)
            f = Function(fun, f'ф = {round(a, 3)}*x^({round(b, 3)})', s, root_mean_square_deviation)
            self.print_approximation_table(function_table, f, self.function_type)
            return f
        except TypeError:
            return None


def solution(id_list, report, k):
    answer = []
    report_list = set()
    reporter = {}
    reported = {}

    for id in id_list:
        reporter[id] = []
        reported[id] = 0

    for r in report:
        if r in report_list:
            continue
        report_list.add(r)
        a, b = r.split()

        reporter[a].append(b)
        reported[b] += 1

    ban_list = []

    for r in reported:
        if reported[r] >= k:
            ban_list.append(r)

    for r in reporter:
        cnt = 0
        for b in ban_list:
            if b in reporter[r]:
                cnt += 1
        answer.append(cnt)

    return answer

3주차/PGS_신고결과받기_디노.py

PGS_신고결과받기_디노.py


from utils import toCsv
from pathlib import Path

dirs = Path('outDayDataset/unbalanced/2014/MOORE')

for month_dir in dirs.glob('*'):
    print(month_dir.name)
    for file in month_dir.glob('*.arff'):
        with open(file, 'r') as _in:
            c = _in.readlines()
            data = toCsv(c)
            with open(file.parent / f'{file.name[:-5]}.csv', 'w') as out:
                out.writelines(data)


# balpy.py

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

# 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

# balpy 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 balpy(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 = 	"BALPY_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/#/ethereum"		},
					"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",		"balFrontend":"op.beets.fi/#/"					},
					"kovan":			{"id":42,		"blockExplorerUrl":"kovan.etherscan.io",															},
					"gnosis":			{"id":100,		"blockExplorerUrl":"gnosisscan.io",					"balFrontend":"app.balancer.fi/#/gnosis-chain"	},
					"polygon":			{"id":137,		"blockExplorerUrl":"polygonscan.com",				"balFrontend":"app.balancer.fi/#/polygon"		},
					"fantom":			{"id":250,		"blockExplorerUrl":"ftmscan.com",					"balFrontend":"beets.fi/#/"						},
					"zkevm":			{"id":1101,		"blockExplorerUrl":"zkevm.polygonscan.com",			"balFrontend":"app.balancer.fi/#/zkevm"			},
					"arbitrum":			{"id":42161,	"blockExplorerUrl":"arbiscan.io",					"balFrontend":"app.balancer.fi/#/arbitrum"		},
					"sepolia":			{"id":11155111,	"blockExplorerUrl":"sepolia.etherscan.io",			"balFrontend":"app.balancer.fi/#/sepolia"		}
				};

	apiEndpoint = "https://api.balancer.fi/"

	# ABIs and Deployment Addresses
	abis = {};
	deploymentAddresses = {};

	contractDirectories = {};
	deprecatedContractDirectories = {};
	deprecated = True;
	deploymentsDir = "balancer-deployments/tasks/deprecated"

	for i in range(2):
		# Get all deployment directories from the balancer-deployments repo
		if not deprecated:
			deploymentsDir = "balancer-deployments/tasks"

		spec = importlib.util.find_spec(__name__)
		headTail = os.path.split(spec.origin);
		packagePath = headTail[0];
		taskDir = os.path.join(packagePath, deploymentsDir);
		taskSubDirs = os.listdir(taskDir);
		taskSubDirs.sort();

		for t in taskSubDirs:
			# skip deprecated and scripts folders
			if len(t.split("-")) == 1:
				continue;

			# skip 00000000-tokens
			if t.startswith("00000000"):
				continue;

			currPath = os.path.join(taskDir, t, "artifact");
			artifactNames = os.listdir(currPath);
			for a in artifactNames:
				contractName = a.split(".")[0];
				if deprecated:
					deprecatedContractDirectories[contractName] = t;
				else:
					contractDirectories[contractName] = t;
		deprecated = False;

	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"
	};

	omitCalldataFlag = False;
	calldataFlag = "py".encode('utf-8').hex();

	def __init__(self, network=None, verbose=True, customConfigFile=None, manualEnv={}, omitCalldataFlag=False):
		super(balpy, 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 BALPY_CUSTOM_RPC environment variables\n");
			self.ERROR("!! If you are using L2, you must use BALPY_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:
			if network in ["polygon", "optimism", "arbitrum"]:
				endpoint = 'https://' + self.network + '-mainnet.infura.io/v3/' + self.infuraApiKey;
			else:
				endpoint = 'https://' + self.network + '.infura.io/v3/' + 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)

		self.omitCalldataFlag = omitCalldataFlag;

		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];

			# get contract abi from deployments repo artifacts
			abiPath = os.path.join(self.deploymentsDir, subdir , "artifact", contractType + '.json');
			try:
				f = pkgutil.get_data(__name__, abiPath).decode();
				currAbi = json.loads(f)["abi"];
				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(self.deploymentsDir, 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):
		if not self.omitCalldataFlag:
			tx["data"] += self.calldataFlag;
		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 erc20ScaleDecimalsStandard(self, tokenAddress, amount):
		decimals = self.erc20GetDecimals(tokenAddress);
		standardBalance = Decimal(amount) * Decimal(10**(-decimals));
		return(standardBalance);

	def erc20ScaleDecimalsWei(self, tokenAddress, amount):
		decimals = self.erc20GetDecimals(tokenAddress);
		weiBalance = Decimal(amount) * Decimal(10**(decimals));
		return(weiBalance);

	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 = "-";
		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 balConvertWeiToToken(self, token, wei):
		decimals = self.erc20GetDecimals(token)
		return float(Decimal(wei) / Decimal(10**decimals))

	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 generateSalt(self, salt_input=None):
		if salt_input is None:
			salt_input = random.randint(0, 2**256 - 1);
		salt = eth_abi.encode_abi(['uint256'],[int(salt_input)]);
		salt_hex = binascii.hexlify(salt);
		salt_str = "0x" + salt_hex.decode("ascii");
		return(salt_str);

	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,
													self.generateSalt());
		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 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"],
				assetManagers
			),
			(
				checksumTokens,
				intWithDecimalsWeights,
				swapFeePercentage,
				poolData["swapEnabledOnStart"],
				poolData["mustAllowlistLPs"],
				managementAumFeePercentage,
				int(poolData["aumFeeId"])
			),
			owner,
			self.generateSalt()
		);
		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]
		exemptFromYieldProtocolFeeFlag = poolData["exemptFromYieldProtocolFeeFlag"];

		createFunction = factory.functions.create(	poolData["name"],
													poolData["symbol"],
													checksumTokens,
													int(poolData["amplificationParameter"]),
													rateProviders,
													tokenRateCacheDurations,
													exemptFromYieldProtocolFeeFlag,
													swapFeePercentage,
													owner,
													self.generateSalt());
		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"]),
													self.generateSalt());
		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 == "ManagedPoolFactory":
			createFunction = self.balCreateFnManagedPoolFactory(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("Supported pool types are:");
			print("\tWeightedPool");
			print("\tManagedPool");
			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();
		factoryNames = [c for c in contractNames if ("Factory" in c) and ("Pool" in c)]; #can't simply use "PoolFactory" b/c of WeightedPool2TokensFactory

		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", "NoProtocolFeeLiquidityBootstrappingPoolFactory", "ManagedPoolFactory"];
		usingComposableStable = factoryName in ["ComposableStablePoolFactory"];

		if usingWeighted:
			joinKindEnum = WeightedPoolJoinKind[joinKind];
		elif usingComposableStable:
			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 balDoQueryExitPool(self, poolId, address, exitPoolRequestTuple):
		bh = self.balLoadContract("BalancerHelpers")
		return bh.functions.queryExit(poolId, address, address, exitPoolRequestTuple).call()

	def balFormatQueryExitPoolOutput(self, queryExitPoolOutput, tokens, poolAddress):
		result = {}
		bptAmountIn, tokensAmountsOut = queryExitPoolOutput
		result["bptIn"] = {
                    "address": poolAddress,
                    "amount": self.balConvertWeiToToken(poolAddress, bptAmountIn)
                }
		result["tokensOut"] = []
		for address, amount in zip(tokens, tokensAmountsOut):
			result["tokensOut"].append({
				"address": address,
				"amount": self.balConvertWeiToToken(address, amount)
			})
		return result

	def balDoExitPool(self, poolId, address, exitPoolRequestTuple, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
		vault = self.balLoadContract("Vault")
		exitPoolFunction = vault.functions.exitPool(
		    poolId, address, address, exitPoolRequestTuple)
		tx = self.buildTx(exitPoolFunction, gasFactor, gasPriceSpeed,
		                  nonceOverride, gasEstimateOverride, gasPriceGweiOverride)
		return self.sendTx(tx)

	def balFormatExitPoolRequestTupleExactBptInForOneTokenOut(self, exitKindValue, tokens, bptAmount, tokenOut, minAmountsOut, toInternalBalance=False):
		minAmountsOut = self.balConvertTokensToWei(tokens, minAmountsOut)
		userData = eth_abi.encode_abi(["uint256", "uint256", "uint256"], [
		                              exitKindValue, bptAmount, tokenOut])
		return tokens, minAmountsOut, userData, toInternalBalance

	def balFormatExitPoolRequestTupleExactBptInForTokensOut(self, exitKindValue, tokens, bptAmount, minAmountsOut, toInternalBalance=False):
		minAmountsOut = self.balConvertTokensToWei(tokens, minAmountsOut)
		userData = eth_abi.encode_abi(["uint256", "uint256"], [
		                              exitKindValue, bptAmount])
		return tokens, minAmountsOut, userData, toInternalBalance

	def balFormatExitPoolRequestTupleBptInForExactTokensOut(self, exitKindValue, tokens, maxBptAmount, amountsOut, minAmountsOut, toInternalBalance=False):
		amountsOut = self.balConvertTokensToWei(tokens, amountsOut)
		minAmountsOut = self.balConvertTokensToWei(tokens, minAmountsOut)
		userData = eth_abi.encode_abi(["uint256", "uint256[]", "uint256"], [
		                              exitKindValue, amountsOut, maxBptAmount])
		return tokens, minAmountsOut, userData, toInternalBalance

	def balSortTokensExitPool(self, tokens):
		tokenKeys = tokens.keys() # the token key is the address string
		tokenAddresses = []
		for tokenKey in tokenKeys:
			tokenAddresses.append(self.web3.toChecksumAddress(tokenKey))
		minAmountsOutSorted = []
		tokenAddressesSorted = []
		amountsOutSorted = []

		for tokenAddresses, tokenKey in sorted(zip(tokenAddresses, tokenKeys)):
			tokenAddressesSorted.append(tokenAddresses)
			minAmountsOutSorted.append(float(tokens[tokenKey].get("minAmount", 0)))
			amountsOutSorted.append(float(tokens[tokenKey].get("amount", 0)))

		tokenOutSorted = None
		if max(amountsOutSorted)==sum(amountsOutSorted):
			tokenOutSorted = amountsOutSorted.index(max(amountsOutSorted))
		return tokenAddressesSorted, amountsOutSorted, minAmountsOutSorted, tokenOutSorted

	def balExitPool(self, exitDescription, query=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
		exitKind = exitDescription["exitKind"]
		poolId = exitDescription["poolId"]
		tokens = exitDescription["tokens"]
		toInternalBalance = exitDescription.get("toInternalBalance", False)

		poolAddress = self.balPooldIdToAddress(poolId)
		exitKindValue = WeightedPoolExitKind[exitKind].value
		userAddress = self.web3.toChecksumAddress(self.web3.eth.default_account)
		tokenAddresses, amountsOut, minAmountsOut, tokenOut  = self.balSortTokensExitPool(tokens)

		if exitKind == "EXACT_BPT_IN_FOR_ONE_TOKEN_OUT":
			bptAmount = self.balConvertTokensToWei([poolAddress], [float(exitDescription["bptAmount"])])[0]
			exitPoolRequestTuple = self.balFormatExitPoolRequestTupleExactBptInForOneTokenOut(
				exitKindValue, tokenAddresses, bptAmount, tokenOut, minAmountsOut, toInternalBalance)
		elif exitKind == "EXACT_BPT_IN_FOR_TOKENS_OUT":
			bptAmount = self.balConvertTokensToWei([poolAddress], [float(exitDescription["bptAmount"])])[0]
			exitPoolRequestTuple = self.balFormatExitPoolRequestTupleExactBptInForTokensOut(
				exitKindValue, tokenAddresses, bptAmount, minAmountsOut, toInternalBalance)
		elif exitKind == "BPT_IN_FOR_EXACT_TOKENS_OUT":
			maxBptAmount = self.balConvertTokensToWei([poolAddress], [float(exitDescription["maxBptAmount"])])[0]
			if query:
				maxBptAmount = self.balConvertTokensToWei([poolAddress], [self.INFINITE])[0]
			exitPoolRequestTuple = self.balFormatExitPoolRequestTupleBptInForExactTokensOut(
				exitKindValue, tokenAddresses, maxBptAmount, amountsOut, minAmountsOut, toInternalBalance)

		if query:
			tokensSorted = exitPoolRequestTuple[0]
			queryOutput = self.balDoQueryExitPool(
			    poolId, userAddress, exitPoolRequestTuple)
			return self.balFormatQueryExitPoolOutput(queryOutput, tokensSorted, poolAddress)
		return self.balDoExitPool(poolId, userAddress, exitPoolRequestTuple, gasFactor=gasFactor, gasPriceSpeed=gasPriceSpeed, nonceOverride=nonceOverride, gasEstimateOverride=gasEstimateOverride, gasPriceGweiOverride=gasPriceGweiOverride)

	def balGetRebalanceLinearPoolsData(self, linearPoolAddresses):
		# Can realistically be any linear pool, this one just is the most generic.
		poolAbi = self.balPoolGetAbi("ERC4626LinearPool");

		# get poolIds and mainTokens to feed into getPoolTokenInfo (to get asset managers)
		for i in linearPoolAddresses:
			self.mc.addCall(i, poolAbi, 'getPoolId');
			self.mc.addCall(i, poolAbi, 'getMainToken');
		output = self.mc.execute();

		pool_id_by_pool_address = {};
		main_tokens_by_pool_id = {};
		for i in range(0, len(output[0]), 2):
			poolIdSuccess = output[1][i];
			mainTokenSuccess = output[1][i + 1];
			if poolIdSuccess and mainTokenSuccess:
				poolId = "0x" + output[0][i][0].hex();
				mainToken = self.web3.toChecksumAddress(output[0][i + 1][0]);
				main_tokens_by_pool_id[poolId] = mainToken;
				pool_id_by_pool_address[linearPoolAddresses[int(i/2)]] = poolId;

		self.multiCallErc20BatchDecimals(list(main_tokens_by_pool_id.values()));

		# determine each linear pool's asset manager (the rebalancer)
		vault = self.balLoadContract("Vault");
		assetManagers = [];
		for poolId in main_tokens_by_pool_id:
			mainToken = main_tokens_by_pool_id[poolId];
			self.mc.addCall(vault.address, vault.abi, 'getPoolTokenInfo', args=[poolId, mainToken]);
		output = self.mc.execute();
		data = output[0]
		successes = output[1]

		pools_by_rebalancer = {};
		for d, a in zip(data, linearPoolAddresses):
				pools_by_rebalancer[self.web3.toChecksumAddress(d[3])] = a;

		for rebalancer in pools_by_rebalancer:
			pool_address = pools_by_rebalancer[rebalancer];

			linearPoolRebalancer = self.balLoadContractAtAddress("ERC4626LinearPoolRebalancer", rebalancer);
			self.mc.addCall(linearPoolRebalancer.address, linearPoolRebalancer.abi, 'rebalance', args=[self.ZERO_ADDRESS]);

		mc_output = self.mc.execute();
		results = mc_output[0];
		successes = mc_output[1];
		output = {};
		for result, success, rebalancer in zip(results, successes, list(pools_by_rebalancer.keys())):
			if success and not result is None and result[0] > 0:
				amount = result[0];
				token = main_tokens_by_pool_id[pool_id_by_pool_address[pools_by_rebalancer[rebalancer]]];
				linearPoolRebalancer = self.balLoadContractAtAddress("ERC4626LinearPoolRebalancer", rebalancer);

				output[rebalancer] = {
					"token_out":token,
					"amount_out":float(self.erc20ScaleDecimalsStandard(token, amount)),
					"gas_estimate":linearPoolRebalancer.functions.rebalance(self.address).estimateGas()
				}
		return(output);

	def balDoRebalanceLinearPool(self, pool_address, recipient=None, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
		linear_pool = self.balLoadContractAtAddress("ERC4626LinearPool", self.web3.toChecksumAddress(pool_address));
		pool_id = linear_pool.functions.getPoolId().call();
		main_token = linear_pool.functions.getMainToken().call();

		vault = self.balLoadContract("Vault");
		pool_token_info = vault.functions.getPoolTokenInfo(pool_id, main_token).call();
		rebalancer = self.web3.toChecksumAddress(pool_token_info[3]);

		linear_pool_rebalancer = self.balLoadContractAtAddress("ERC4626LinearPoolRebalancer", rebalancer);
		if recipient is None:
			recipient = self.address;
		fn = linear_pool_rebalancer.functions.rebalance(recipient);
		tx = self.buildTx(fn, gasFactor, gasPriceSpeed, nonceOverride, gasEstimateOverride, gasPriceGweiOverride);
		tx_hash = self.sendTx(tx, isAsync);
		return(txHash);

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

	def balVaultGetAuthorizer(self):
		vault = self.balLoadContract("Vault")
		authorizerAddress = vault.functions.getAuthorizer().call()
		return authorizerAddress

	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 balVaultGetPool(self, poolId):
		vault = self.balLoadContract("Vault")
		address, specialization = vault.functions.getPool(poolId).call()
		return address, specialization

	def balVaultGetPoolTokenInfo(self, poolId, tokenAddress):
		vault = self.balLoadContract("Vault")
		tokenAddress = self.web3.toChecksumAddress(tokenAddress)
		tokenInfo = vault.functions.getPoolTokenInfo(poolId, tokenAddress).call()
		cash, managed, lastChangeBlock, assetManager = tokenInfo
		return cash, managed, lastChangeBlock, assetManager

	def balVaultGetProtocolFeesCollector(self):
		vault = self.balLoadContract("Vault")
		address = vault.functions.getProtocolFeesCollector().call()
		return address

	def balVaultHasApprovedRelayer(self, userAddress, relayerAddress):
		vault = self.balLoadContract("Vault")
		hasApprovedRelayer = vault.functions.hasApprovedRelayer(
		    userAddress, relayerAddress).call()
		return hasApprovedRelayer

	def balVaultSetAuthorizer(self, newAuthorizerAddress, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
		vault = self.balLoadContract("Vault")
		setAuthorizerFn = vault.functions.newAuthorizer(newAuthorizerAddress)
		tx = self.buildTx(setAuthorizerFn, gasFactor, gasPriceSpeed, nonceOverride, gasEstimateOverride, gasPriceGweiOverride);
		return self.sendTx(tx, isAsync)

	def balVaultSetPaused(self, paused, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
		vault = self.balLoadContract("Vault")
		setPausedFn = vault.functions.setPaused(paused)
		tx = self.buildTx(setPausedFn, gasFactor, gasPriceSpeed, nonceOverride, gasEstimateOverride, gasPriceGweiOverride);
		return self.sendTx(tx, isAsync)

	def balVaultSetRelayerApproval(self, senderAddress, relayerAddress, approved, isAsync=False, gasFactor=1.05, gasPriceSpeed="average", nonceOverride=-1, gasEstimateOverride=-1, gasPriceGweiOverride=-1):
		vault = self.balLoadContract("Vault")
		setRelayerApprovalFn = vault.functions.setRelayerApproval(
		    senderAddress, relayerAddress, approved)
		tx = self.buildTx(setRelayerApprovalFn, gasFactor, gasPriceSpeed, nonceOverride, gasEstimateOverride, gasPriceGweiOverride);
		return self.sendTx(tx, isAsync)

	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 balLoadContractAtAddress(self, contractName, address):
		contract = self.web3.eth.contract(address=address, 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 poolType == "HighAmpComposableStable":
			poolType = "ComposableStable";

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

		deploymentFolder = None;
		deprecatedString = ""
		try:
			deploymentFolder = self.contractDirectories[poolType + "Factory"];
		except KeyError:
			try:
				deploymentFolder = self.deprecatedContractDirectories[poolType + "Factory"];
				deprecatedString = "deprecated"
			except KeyError:
				return None;
		except Exception as e:
			print(e);
			quit();

		abiPath = os.path.join(self.deploymentsDir, deprecatedString, deploymentFolder, "artifact", poolType + ".json");
		f = pkgutil.get_data(__name__, abiPath).decode();
		poolAbi = json.loads(f)["abi"];
		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],
								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 balGetApiEndpoint(self, endpoint):
		return(os.path.join(self.apiEndpoint, endpoint, str(self.networkParams[self.network]["id"])));

	def balGetApiEndpointSor(self):
		return(self.balGetApiEndpoint("sor"));

	def balGetApiEndpointPools(self, poolId=None):
		endpoint = self.balGetApiEndpoint("pools");
		if poolId is None:
			return(endpoint);
		else:
			return(os.path.join(endpoint, poolId));

	def balSorQuery(self, data):
		query = data["sor"];

		# scale amount based on input/output
		token_for_decimals = query["orderKind"].lower() + "Token";
		amount_scaled = self.erc20ScaleDecimalsWei(query[token_for_decimals], query["amount"]);
		query["amount"] = int(amount_scaled);

		# get gas price if not provided
		if not "gasPrice" in query.keys():
			gas_price_gwei = self.getGasPrice(query["gasSpeed"]);
			gas_price_wei = int(gas_price_gwei * 1e9);
			query["gasPrice"] = gas_price_wei;
			del query["gasSpeed"];

		# API gets grumpy when you send it numbers. Send everything as a string
		for field in query:
			query[field] = str(query[field])
		response = requests.post(
			self.balGetApiEndpointSor(),
			headers={'Content-Type': 'application/json'},
			data=json.dumps(query)
		);

		batch_swap = self.balSorResponseToBatchSwapFormat(data, response.json())

		return(batch_swap)

	def balApiGetPools(self):
		url = self.balGetApiEndpointPools()
		success = False
		resp = None;
		maxRetries = 5
		retries = 0
		delay = 3
		while not success or retries > maxRetries:
			response = requests.get(
				url,
				headers={'Content-Type': 'application/json'}
			);
			if response.status_code == 200:
				success = True
				resp = response.json()
			else:
				retries += 1
				print("Query failed, trying in", delay, "seconds")
				time.sleep(delay)
		return(resp)

	def balApiGetPool(self, poolId):
		url = self.balGetApiEndpointPools(poolId)
		response = requests.get(
			url,
			headers={'Content-Type': 'application/json'}
		);
		return(response.json())

	def balSorResponseToBatchSwapFormat(self, query, response):
		sor = query["sor"];
		del query["sor"];

		kind = None;
		if sor["orderKind"] not in ["buy", "sell"]:
			bal.ERROR("orderKind must be \"buy\" or \"sell\"");
			quit();
		if sor["orderKind"] == "sell":
			kind = "0";
		if sor["orderKind"] == "buy":
			kind = "1";

		query["batchSwap"]["kind"] = kind;
		query["batchSwap"]["assets"] = response["tokenAddresses"];
		query["batchSwap"]["swaps"] = response["swaps"];
		query["batchSwap"]["limits"] = [0] * len(response["tokenAddresses"]);

		for step in query["batchSwap"]["swaps"]:
			index = step["assetInIndex"];
			if kind == "1":
				index = step["assetOutIndex"];
			asset = query["batchSwap"]["assets"][index]
			step["amount"] = float(self.erc20ScaleDecimalsStandard(asset, step["amount"]));

		query_results = self.balQueryBatchSwap(query["batchSwap"]);

		idx = 0;
		for a in query["batchSwap"]["assets"]:
			chk_asset = self.web3.toChecksumAddress(a);
			factor = 1.00; # 100%
			asset_delta = query_results[chk_asset];

			slippage_factor = float(query["slippageTolerancePercent"])/100.0;

			if asset_delta > 0:
				factor += slippage_factor;
			else:
				factor -= slippage_factor;

			query["batchSwap"]["limits"][idx] = asset_delta * factor;
			idx += 1;

		return(query)

	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();
		outputData = outputData[0];
		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];

			if poolAbi is None:
				continue;

			# 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();
		data = data[0];

		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)


from sympy import primerange, isprime
import time

N = 10**6 # our upper limit
rnge = 1,N//200 # just a heuristic, could probably come up with a programatic estimate using distribution of primes

primes = list(primerange(*rnge)) # all primes in range

# generate all primes that are sums of primes in our range
# not very pretty or efficient, but fast enough
sums = [(j-i,sum(primes[i:j])) for i in range(*rnge) for j in range(*rnge)
        if i<j and j <= len(primes) and isprime(sum(primes[i:j])) and sum(primes[i:j]) < N]

lengths, sums = zip(*sums) # unzip tuples
ind = lengths.index(max(lengths)) # get index of max sum

print(sums[ind]) # max sum


import os
import shutil
from log_picker.sending.senderbaseclass import SenderBaseClass
from log_picker.sending.senderbaseclass import SenderError

class LocalSender(SenderBaseClass):

    def __init__(self, *args, **kwargs):
        SenderBaseClass.__init__(self, args, kwargs)
        self.path = None

    def set_path(self, directory):
        self.path = directory

        if os.path.exists(self.path) and not os.path.isdir(self.path):
            raise SenderError('Cannot create "%s" directory. A file of '
                                    'the same name already exists.' % self.path)

    def sendfile(self, filename, contenttype):

        # Another check due possibility of race condition
        if os.path.exists(self.path):
            if not os.path.isdir(self.path):
                raise SenderError('Cannot create "%s" directory. A file of '
                                    'the same name already exists.' % self.path)
        else:
            try:
                os.makedirs(self.path)
            except Exception as e:
                raise SenderError('Cannot create "%s" directory: %s' % \
                                                                (self.path, e))
        try:
            shutil.copy(filename, self.path)
        except Exception as e:
            raise SenderError('Could not save "%s" to "%s": %s' % \
                                (os.path.basename(filename), self.path, e))


class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        self.phone = {
            '2': ['a', 'b', 'c'],
            '3': ['d', 'e', 'f'],
            '4': ['g', 'h', 'i'],
            '5': ['j', 'k', 'l'],
            '6': ['m', 'n', 'o'],
            '7': ['p', 'q', 'r', 's'],
            '8': ['t', 'u', 'v'],
            '9': ['w', 'x', 'y', 'z'],
        }
        ans = self.dfs(digits, [])
        return ans

    def dfs(self, digits, comb):
        if not digits:
            return comb
        if not comb:
            return self.dfs(digits[1:], self.phone[digits[0]])

        next_comb = []
        for letter in self.phone[digits[0]]:
            for precomb in comb:
                next_comb.append(precomb + letter)
        return self.dfs(digits[1:], next_comb)


# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function

from window_switcher.vendor.Qt import QtWidgets

_MAYA_MAIN_WINDOW_NAME = "MayaWindow"

def collect_switchable_windows(parent=None):
    switchable_windows = []
    active_window = None
    for w in QtWidgets.QApplication.topLevelWidgets():
        if not w.isVisible():
            continue
        if parent and w == parent:
            continue
        if not w.children():
            continue
        if w.isActiveWindow():
            active_window = w
            continue

        if not w.windowTitle() and w.height() < 20:
            # workaround for Maya2019 outliner bug.
            continue

        switchable_windows.append(w)

    if active_window:
        switchable_windows.insert(0, active_window)
    return switchable_windows


'''Faça um algoritmo para ler duas variáveis inteiras A e B e garantir que A e B
fiquem em ordem crescente, ou seja, a variável A deverá armazenar o menor valor
fornecido e a variável B o maior.'''

# entrada
a = int(input('Digite um valor para A:'));
b = int(input('Digite um valor para B:'));

# processamento
if a > b:
    aux = a
    a = b
    b = aux

# saída
print('A =',a,'B =',b);