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

added rough support for mirror.finance liquidity pools on terra money #25

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions blockapi/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@
from blockapi.api.tzstats import *
from blockapi.api.zchain import *
from blockapi.api.zensystem import *

from blockapi.api.mirror import *
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the previous imports are sorted alphabetically, could you please do the same?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, no problem. The whole code is just a prototype.

302 changes: 302 additions & 0 deletions blockapi/api/mirror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
from decimal import Decimal

from blockapi.services import BlockchainAPI


class MirrorApi(BlockchainAPI):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are extending the functionality of TerraMoneyApi and copying some of its methods and attributes verbatim.

It is understandable you do not want to modify that class on the first try, but you could subclass it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can extend TerraMoneyApi, subclass it I guess. I wasn't sure if it wont brake the testing mechanism(need to have a look) and if to have one base_url and testnet_url in the singleton api class is generally a good idea. Just not sure here. Point is I need two api servers (lcd.terra.dev a and fcd.terra.dev) - suggestions? Adding it to the class would not work with url_build mechanism. Its just a prototype.



symbol = 'LP'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This symbol is incorrect - LUNA

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure either. LUNA makes sense for the getbalance from terraMoneyApi. the amount that fetch_pool_balances return is in LP(it is not a coin though)...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, Mirror service is not blockchain, so in this case symbol doesn't make sense
but if there have to be one, LUNA is closer because LP is not token, it's just prefix for LP tokens

btw base currency in Mirror is UST (USD stable coin)

Copy link
Author

@grosaktom grosaktom Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, lets put there LUNA it is, settled! :) On the other hand - mmm you call get_balance you get a number without ticker in reality denominated in LP an you think that if you get a number you get the ticker to it by reading the attribute in the api (I would).

base_url = 'https://tequila-lcd.terra.dev'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base_url is not correct either - this is for testnet.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, its a prototype....

rate_limit = 0.5
coef = Decimal(1e-6)
max_items_per_page = 100
page_offset_step = 1

supported_requests = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To also support the requests of TerraMoneyApi, you can do

    supported_requests = {
        **TerraMoneyApi.supported_requests,
        **{
            'get_mirror_pool_balance': '/wasm/contracts/{lp_token_contract}/store?query_msg={{ "balance" : {{  "address" : "{address}" }} }}',
            'get_mirror_pool': '/wasm/contracts/{pair_contract}/store?query_msg={{ "pool": {{ }} }}'
        }
    }

with proper formatting of course :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean. You mean to put the endpoint and the get parameter after "?" in one string? okok :)

'get_mirror_pool_balance': '/wasm/contracts/{lp_token_contract}/store'
'?query_msg={{ "balance" : {{ "address" : "{address}" }} }}',
'get_mirror_pool': '/wasm/contracts/{pair_contract}/store'
'?query_msg={{ "pool": {{ }} }}'
}

symbols = {
'uluna': 'LUNA',
'ukrw': 'KRT',
'usdr': 'SDT',
'uusd': 'UST',
'umnt': 'MNT'
}

liqudity_pool_suffix = "LP"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You use this variable only once, is it really necessary to define it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can put it just into the evaluation + "LP". I just expected this one to change, so others don't have to search it for too long.



def get_balance(self):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaviour is not correct - if you do now want to support get_balance method, you should raise an exception, not return an empty string.

def get_balance(self):
    raise NotImplementedError()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Or better return get_lp_balance();

return ""

def get_contracts(self):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contracts do not change here, so it does not make much sense to always define them anew.

You could define them as a class variable to have them static or, if a need to load them dynamically arose, it could be something like (with elements of pseudo-code):

// self.terra_contracts instance variable needs to be initialized as None in a constructor

def get_contracts(self, force: bool = False):
    if self.terra_contracts is None or force:
        self.terra_contracts = self.get_contracts_dynamically()  # implementation needed
    return self.terra_contracts

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the json copy of whitelist is there only instead of self.get_contracts_dynamically(). I think they wont probably change therir contract addresses - but new mirrored assets can get listed and if we keep a copy of the old whitelist in the class it wont be able to fetch the new ones unless we change the code...

# Make a mechanism to fetch the contracts by the initialization of the api or a mechanism to get this from DB for translation of contract address to ticker
terra_contracts = {
"contracts": {
"gov": "terra12r5ghc6ppewcdcs3hkewrz24ey6xl7mmpk478s",
"mirrorToken": "terra10llyp6v3j3her8u3ce66ragytu45kcmd9asj3u",
"factory": "terra10l9xc9eyrpxd5tqjgy6uxrw7dd9cv897cw8wdr",
"oracle": "terra1uvxhec74deupp47enh7z5pk55f3cvcz8nj4ww9",
"mint": "terra1s9ehcjv0dqj2gsl72xrpp0ga5fql7fj7y3kq3w",
"staking": "terra1a06dgl27rhujjphsn4drl242ufws267qxypptx",
"tokenFactory": "terra18qpjm4zkvqnpjpw0zn0tdr8gdzvt8au35v45xf",
"collector": "terra1v046ktavwzlyct5gh8ls767fh7hc4gxc95grxy",
"community": "terra10qm80sfht0zhh3gaeej7sd4f92tswc44fn000q",
"airdrop": "terra1p6nvyw7vz3fgpy4nyh3q3vc09e65sr97ejxn2p"
},
"whitelist": {
"terra10llyp6v3j3her8u3ce66ragytu45kcmd9asj3u": {
"symbol": "MIR",
"name": "Mirror",
"token": "terra10llyp6v3j3her8u3ce66ragytu45kcmd9asj3u",
"pair": "terra1cz6qp8lfwht83fh9xm9n94kj04qc35ulga5dl0",
"lpToken": "terra1zrryfhlrpg49quz37u90ck6f396l4xdjs5s08j",
"status": "LISTED"
},
"terra16vfxm98rxlc8erj4g0sj5932dvylgmdufnugk0": {
"symbol": "mAAPL",
"name": "Apple",
"token": "terra16vfxm98rxlc8erj4g0sj5932dvylgmdufnugk0",
"pair": "terra1yj892rl8edvk0y2ayf3h36t6uf89lzxg8jea4a",
"lpToken": "terra1vth958fsn8zawllaqcdzswksjkv3dz2sqqmcu4",
"status": "LISTED"
},
"terra1qg9ugndl25567u03jrr79xur2yk9d632fke3h2": {
"symbol": "mGOOGL",
"name": "Google",
"token": "terra1qg9ugndl25567u03jrr79xur2yk9d632fke3h2",
"pair": "terra1z2734asgwhma8ma2fq4yu7ce2l3mrvj4qnz6ws",
"lpToken": "terra1qxurxcgl30eu4ar34ltr5e9tqc2gjl4atspvy3",
"status": "LISTED"
},
"terra1nslem9lgwx53rvgqwd8hgq7pepsry6yr3wsen4": {
"symbol": "mTSLA",
"name": "Tesla",
"token": "terra1nslem9lgwx53rvgqwd8hgq7pepsry6yr3wsen4",
"pair": "terra1tsln42kfeq8edwscmw8njgter5dp8evn40znn9",
"lpToken": "terra1utf7qw0uce42vqsh255hxgd3pvuzfvp6jcayk5",
"status": "LISTED"
},
"terra1djnlav60utj06kk9dl7defsv8xql5qpryzvm3h": {
"symbol": "mNFLX",
"name": "Netflix",
"token": "terra1djnlav60utj06kk9dl7defsv8xql5qpryzvm3h",
"pair": "terra18yl0z6wntjkustt9cckc9ptp7l5qh7kr0xrmav",
"lpToken": "terra1e0njrqcsehxpt9due62x9zsxl7h9htl0xqdujv",
"status": "LISTED"
},
"terra18yx7ff8knc98p07pdkhm3u36wufaeacv47fuha": {
"symbol": "mQQQ",
"name": "Invesco QQQ Trust",
"token": "terra18yx7ff8knc98p07pdkhm3u36wufaeacv47fuha",
"pair": "terra1epxv8z6tzxezjfgw7tveytw5n3fuf6wvg6w8f5",
"lpToken": "terra1h52zc9qmndczgru9vp2cvuwfclyykl5yt3qjk8",
"status": "LISTED"
},
"terra1ax7mhqahj6vcqnnl675nqq2g9wghzuecy923vy": {
"symbol": "mTWTR",
"name": "Twitter",
"token": "terra1ax7mhqahj6vcqnnl675nqq2g9wghzuecy923vy",
"pair": "terra1jv937296dy5c5dxglrzf05h0jlaaxp55tqlyh6",
"lpToken": "terra10cugucjwn4hdtvavl0n2sh2ke64nx93luhj49k",
"status": "LISTED"
},
"terra12s2h8vlztjwu440khpc0063p34vm7nhu25w4p9": {
"symbol": "mMSFT",
"name": "Microsoft Corporation",
"token": "terra12s2h8vlztjwu440khpc0063p34vm7nhu25w4p9",
"pair": "terra1dt7ne6gwv23wg6chl89q95yj6999alagc6rqd9",
"lpToken": "terra1f7azmktepw5rq35e2m6r6smtwl8wdrxp0dsvar",
"status": "LISTED"
},
"terra12saaecsqwxj04fn0jsv4jmdyp6gylptf5tksge": {
"symbol": "mAMZN",
"name": "Amazon.com",
"token": "terra12saaecsqwxj04fn0jsv4jmdyp6gylptf5tksge",
"pair": "terra1xs3vy9zs8agmnzyn7z9s7kqk392uu2h3x3l6er",
"lpToken": "terra1kgvcrtupc8y4dgc9n08ud99ckdxp08j59zgccf",
"status": "LISTED"
},
"terra15dr4ah3kha68kam7a907pje9w6z2lpjpnrkd06": {
"symbol": "mBABA",
"name": "Alibaba Group Holdings Ltd ADR",
"token": "terra15dr4ah3kha68kam7a907pje9w6z2lpjpnrkd06",
"pair": "terra15qq59h2canrr2pf8ny7rw57nx3mcvw97tp3xj4",
"lpToken": "terra1px2ya3e07aprfgc76e57r3nuvy3czssrvcxg9t",
"status": "LISTED"
},
"terra19dl29dpykvzej8rg86mjqg8h63s9cqvkknpclr": {
"symbol": "mIAU",
"name": "iShares Gold Trust",
"token": "terra19dl29dpykvzej8rg86mjqg8h63s9cqvkknpclr",
"pair": "terra1tq6w7rl4ryrk458k57dstelx54eylph5zwnpf9",
"lpToken": "terra193c2xvuzswct8qtsg4e6qhe3hyt3l6fac9cy79",
"status": "LISTED"
},
"terra1fdkfhgk433tar72t4edh6p6y9rmjulzc83ljuw": {
"symbol": "mSLV",
"name": "iShares Silver Trust",
"token": "terra1fdkfhgk433tar72t4edh6p6y9rmjulzc83ljuw",
"pair": "terra1tyzsl0dw4pltlqey5v6g646hm22pql8vy3yh2g",
"lpToken": "terra16cn5cgwaktrzczda0c6ux0e2quudh4vn3t8jjm",
"status": "LISTED"
},
"terra1fucmfp8x4mpzsydjaxyv26hrkdg4vpdzdvf647": {
"symbol": "mUSO",
"name": "United States Oil Fund, LP",
"token": "terra1fucmfp8x4mpzsydjaxyv26hrkdg4vpdzdvf647",
"pair": "terra1llk7ycwwlj2zs2l2dvnvmsxrsrnucqwaltstcf",
"lpToken": "terra1rag9w5ch0jrdxjffr6napqz0zsrpm6uz2zezmj",
"status": "LISTED"
},
"terra1z0k7nx0vl85hwpv3e3hu2cyfkwq07fl7nqchvd": {
"symbol": "mVIXY",
"name": "ProShares VIX",
"token": "terra1z0k7nx0vl85hwpv3e3hu2cyfkwq07fl7nqchvd",
"pair": "terra1xg2393l4s7n4z2r0cnu4rr55mkpp942f4d3qzr",
"lpToken": "terra1ud750vcv39hd467sj2kk6s6nn8zf5xhgggf7uq",
"status": "LISTED"
}
}
}
pool_contracts = terra_contracts['whitelist']
return pool_contracts


def get_pool_lp_balance(self, lp_token_contract):
"""Gets a balance denominated in LPs based on the "lpToken" contract address from whitelist

:param lp_token_contract: Contract of the lp token from whitelist on the running network (ex. terra1zrryfhlrpg49quz37u90ck6f396l4xdjs5s08j)
:type lp_token_contract: str
:returns: A string with the LP balance
:rtype: str
"""
response = self.request('get_mirror_pool_balance', lp_token_contract=lp_token_contract, address=self.address)

# if possible check if the returned ballance is for the right contract (different query) and throw exception in case response doesn't have the right format
balance = response['result']['balance']
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not make sense to return a Decimal here? I would not, as a API consumer, expect a str from a function that returns something balance-related.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, lets return decimal...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way I have noticed decimal has a precision up to 10^28 right? So it should be more or less sufficient for majority of cryptocurrencies right? Ether has smallerst amout 10^18 so float is a bit tricky right?

return balance



def get_pool_info(self, pair_contract):
"""Gets pool information from a the paircontract from the whitelist

:param pair_contract: Contract adress of the pair (ex. terra1cz6qp8lfwht83fh9xm9n94kj04qc35ulga5dl0)
:type pair_contract: str
:returns: A json dict with details of the pool (ex. {'stablecoin_amount': '552499703522', 'stablecoin_symbol': 'UST', 'token_amount': '1323959908110', 'token_symbol': 'MIR', 'total_share': '851877273095'})
:rtype: dict
"""

response = self.request('get_mirror_pool', pair_contract=pair_contract, address=self.address)

# Rewrite this one to check if the asset No. 0 is really usd in case there will be more fiat pegged currencies in Mirror or in case the order of assets will be different
stablecoin_amount = response['result']['assets'][0]['amount'];
stablecoin_symbol = response['result']['assets'][0]['info']['native_token']['denom'];
stablecoin_symbol = self._get_symbol(stablecoin_symbol)

# Rewrite this one to be sure we really get the token amount an of the right token
token_amount = response['result']['assets'][1]['amount'];
token_contract = response['result']['assets'][1]['info']['token']['contract_addr'];
token_symbol = self.get_token_symbol(token_contract)
total_share = response['result']['total_share']

# Maybe pass this as python object with attributes instead of loose type:dict
pool_info = {
'stablecoin_amount' : stablecoin_amount,
'stablecoin_symbol' : stablecoin_symbol,
'token_amount' : token_amount,
'token_symbol' : token_symbol,
'total_share' : total_share
}

return pool_info


def get_token_symbol(self , contract_addr):
"""Gets and returns a token symbol form the speficied token contractaddress

:param contract_addr: Contract of the token from whitelist on the running network (ex. terra10llyp6v3j3her8u3ce66ragytu45kcmd9asj3u)
:type contract_addr: str
:returns: A string with the symbol (ex. UUSD)
:rtype: str
"""

contracts = self.get_contracts()
symbol = contract_addr

for contract in contracts:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be rewritten more succintly as

for contract, info in contracts.items():
    if info['token'] == contract_addr:
        symbol = info['symbol']

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely better, thx ;)

if (contracts[contract]['token'] == contract_addr):
symbol = contracts[contract]['symbol']

if (symbol == contract_addr):
ex = Exception('Could not get symbol based on token address! Check the local copy of whitelisted contracts if it is up to date!')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assignment ex = Exception(...) is not neccessary, just raise Exception(...). Also, would it make sense to make a specific exception for this case? Subclassing Exception?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. About the exceptions - I have seen this somewhere else in blockapi making instead of exception of the unknown ticker a string "unknown" in the api output. What about contract address from API instead of the ticker? Will there be ETL of that data? What is better? Inconsistency in archived data (with further possibility to reconstruct what ticker it really was from the contract address) or no data because it would crash? I would also crossvalidate the input data - that the token balance I requested is the one I get in the response from lcd.terra.dev - if that wont fit? Exception?

raise ex

return symbol



def fetch_pool_balances(self):
"""Fetches all pool balances connected to the wallet provided in intialization of the api class

:returns: A json dict with pool balances and withdrawable assets ballances (ex. of one pool balance [{'symbol': 'MIR-UST LP', 'amount': '71.152144', 'underlying_assets': [{'symbol': 'MIR', 'amount': '110.582344'}, {'symbol': 'UST', 'amount': '46.146951'}]}, { ... } ] )
:rtype: dict
"""

contracts = self.get_contracts()
response = []

# loop through the pool contracts and construct the response
# that consist of pool balances of individual pools
for contract in contracts:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this iteration can be made more readable by for contract, info in contracts.items():

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely

lp_contract = contracts[contract]['lpToken']
lp_balance = self.get_pool_lp_balance(lp_contract)


if ( Decimal(lp_balance) > 0 ):

symbol = contracts[contract]['symbol']
#print("LP ballance biiger than zero for : " + symbol)

token_contract = contracts[contract]['token']
pair_contract = contracts[contract]['pair']
pool_info = self.get_pool_info(pair_contract)

lp_balance_decimal = Decimal(lp_balance) * self.coef
withdrawable_stablecoin = Decimal(pool_info['stablecoin_amount']) / Decimal(pool_info['total_share']) * lp_balance_decimal
withdrawable_token = Decimal(pool_info['token_amount']) / Decimal(pool_info['total_share']) * lp_balance_decimal

pool_symbol = symbol + '-' + pool_info['stablecoin_symbol'] + ' ' + self.liqudity_pool_suffix

pool_fragment = {
'symbol' : pool_symbol,
'amount' : format( lp_balance_decimal , '.6f'),
'underlying_assets' : [
{
'symbol': pool_info['token_symbol'],
'amount': format( withdrawable_token , '.6f'),
},
{
'symbol': pool_info['stablecoin_symbol'],
'amount' : format( withdrawable_stablecoin , '.6f'),
}
]
}

response.append(pool_fragment)
#print(response)

return response

@classmethod
def _get_symbol(cls, denom):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed at the beginning, this is a verbatim copy of TerraMoneyApi.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It s a proto. Having code duplicities is baad :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also encapsulate in the class some of the methods i wrote that don't need to be public.

"""It seems that API returns only denom instead of correct
symbols.
"""
return cls.symbols.get(denom, 'unknown')