Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SOR support #53

Merged
merged 1 commit into from Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
83 changes: 83 additions & 0 deletions balpy/balpy.py
Expand Up @@ -110,6 +110,8 @@ class balpy(object):
"sepolia": {"id":11155111, "blockExplorerUrl":"sepolia.etherscan.io", "balFrontend":"app.balancer.fi/#/sepolia" }
};

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

# ABIs and Deployment Addresses
abis = {};
deploymentAddresses = {};
Expand Down Expand Up @@ -475,6 +477,11 @@ def erc20ScaleDecimalsStandard(self, tokenAddress, amount):
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;
Expand Down Expand Up @@ -2062,6 +2069,82 @@ def balGetLinkToFrontend(self, poolId):
else:
return("")

def balGetApiEndpointSor(self):
return(os.path.join(self.apiEndpoint, "sor", str(self.networkParams[self.network]["id"])));

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 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 = [];
Expand Down
24 changes: 24 additions & 0 deletions samples/batchSwaps/sampleSorSwap.json
@@ -0,0 +1,24 @@
{
"network": "polygon",
"slippageTolerancePercent":"1.0", // Direct percentages (1.0 equates to 1%)
"sor": {
"sellToken":"0x2791bca1f2de4661ed88a30c99a7a9449aa84174", // token in
"buyToken":"0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270", // token out
"orderKind":"sell", // must be "sell" or "buy" for GIVEN_IN and GIVEN_OUT respectively
"amount":"0.01", // denominated as a float; automatically scaled w/ appropriate decimals

// Pick between queried gas price and manual gas price. Manual gas price will override if it is set.
"gasSpeed":"fast" // Option 1: Query the current gas price with "slow", "average", or "fast"
// "gasPrice":1000000000 // Option 2: Directly set the gas price in **WEI**
},
"batchSwap": {
"funds": {
"sender": "0x7a73a786d16243680B3253a35392860eAE87d071", // your address
"recipient": "0x7a73a786d16243680B3253a35392860eAE87d071", // your address
"fromInternalBalance": false, // to/from internal balance
"toInternalBalance": false // set to "false" unless you know what you're doing
},
// unix timestamp after which the trade will revert if it hasn't executed yet
"deadline": "999999999999999999"
}
}
76 changes: 76 additions & 0 deletions samples/batchSwaps/sorSwapSample.py
@@ -0,0 +1,76 @@
import balpy
import sys
import os
import jstyleson
import json

def main():

if len(sys.argv) < 2:
print("Usage: python3", sys.argv[0], "/path/to/swap.json");
quit();

pathToSwap = sys.argv[1];
if not os.path.isfile(pathToSwap):
print("Path", pathToSwap, "does not exist. Please enter a valid path.")
quit();

with open(pathToSwap) as f:
data = jstyleson.load(f)

gasFactor = 1.05;
gasSpeedApproval = "fast";
gasSpeedTrade = "fast";


bal = balpy.balpy.balpy(data["network"]);

print();
print("==============================================================")
print("============== Step 1: Query Smart Order Router ==============")
print("==============================================================")
print();
sor_result = bal.balSorQuery(data)
swap = sor_result["batchSwap"];

isFlashSwap = bal.balSwapIsFlashSwap(swap);

print();
print("==============================================================")
print("================ Step 2: Check Token Balances ================")
print("==============================================================")
print();

tokens = swap["assets"];
amountsIn = swap["limits"];
if not isFlashSwap:
if not bal.erc20HasSufficientBalances(tokens, amountsIn):
print("Please fix your insufficient balance before proceeding.")
print("Quitting...")
quit();
else:
print("Executing Flash Swap, no token balances necessary.")


print();
print("==============================================================")
print("============== Step 3: Approve Token Allowance ===============")
print("==============================================================")
print();

if not isFlashSwap:
bal.erc20AsyncEnforceSufficientVaultAllowances(tokens, amountsIn, amountsIn, gasFactor, gasSpeedApproval);
else:
print("Executing Flash Swap, no token approvals necessary.")
# quit();

print();
print("==============================================================")
print("=============== Step 4: Execute Batch Swap Txn ===============")
print("==============================================================")
print();

txHash = bal.balDoBatchSwap(swap, gasFactor=gasFactor, gasPriceSpeed=gasSpeedTrade);

if __name__ == '__main__':
main();