utils.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. import json
  2. import numpy as np
  3. import pandas as pd
  4. from copy import copy
  5. from datetime import date
  6. from datetime import datetime
  7. from typing import Dict, Any, List, Tuple, Optional, Union, Set
  8. from sqlalchemy import desc, cast, Float
  9. from app.models import Address, TornadoPool
  10. from sqlalchemy import or_
  11. # CONSTS for schema
  12. ADDRESS_COL: str = 'address'
  13. ENTITY_COL: str = 'entity'
  14. CONF_COL: str = 'conf'
  15. NAME_COL: str = 'name'
  16. HEURISTIC_COL: str = 'heuristic'
  17. # --
  18. EOA: str = 'eoa'
  19. DEPOSIT: str = 'deposit'
  20. EXCHANGE: str = 'exchange'
  21. DEX: str = 'dex'
  22. DEFI: str = 'defi'
  23. ICO_WALLET: str = 'ico wallet'
  24. MINING: str = 'mining'
  25. TORNADO: str = 'tornado'
  26. UNKNOWN: str = 'unknown'
  27. NODE: str = 'node'
  28. # --
  29. GAS_PRICE_HEUR: str = 'unique_gas_price'
  30. DEPO_REUSE_HEUR: str = 'deposit_address_reuse'
  31. DIFF2VEC_HEUR: str = 'diff2vec'
  32. SAME_NUM_TX_HEUR: str = 'multi_denomination'
  33. SAME_ADDR_HEUR: str = 'address_match'
  34. LINKED_TX_HEUR: str = 'linked_transaction'
  35. TORN_MINE_HEUR: str = 'torn_mine'
  36. def safe_int(x, default=0):
  37. try:
  38. return int(x)
  39. except:
  40. return default
  41. def safe_float(x, default=0):
  42. try:
  43. return float(x)
  44. except:
  45. return default
  46. def safe_bool(x, default=False):
  47. try:
  48. return bool(x)
  49. except:
  50. return default
  51. def get_today_date_str():
  52. today = date.today()
  53. return today.strftime('%m/%d/%Y')
  54. def get_order_command(s, descending):
  55. s = s.strip()
  56. column = None
  57. if s == CONF_COL:
  58. column = cast(Address.conf, Float)
  59. elif s == ENTITY_COL:
  60. column = Address.entity
  61. elif s == NAME_COL:
  62. column = Address.name
  63. elif s == ADDRESS_COL:
  64. column = Address.address
  65. else:
  66. column = Address.id # default
  67. if descending:
  68. return desc(column)
  69. return column # ascending
  70. def get_anonymity_score(
  71. cluster_confs: np.array,
  72. cluster_sizes: np.array,
  73. slope: float = 1,
  74. ) -> float:
  75. """
  76. Since we are still at one heuristic, let's compute the anonymity
  77. score as 1 - np.tanh(slope * cluster_conf * cluster_size) where
  78. slope is a hyperparameter controlling the slope of the TanH.
  79. """
  80. return float(1 - np.tanh(slope * np.dot(cluster_confs, cluster_sizes)))
  81. def get_display_aliases() -> Dict[str, str]:
  82. return {
  83. 'num_deposit': 'deposits',
  84. 'num_withdraw': 'withdraws',
  85. 'tcash_num_compromised': 'compromised deposits',
  86. 'tcash_num_uncompromised': 'uncompromised deposits',
  87. 'num_compromised': 'compromised transactions',
  88. 'num_uncompromised': 'uncompromised transactions',
  89. 'num_compromised_exact_match': 'address match',
  90. 'num_compromised_gas_price': 'unique gas price',
  91. 'num_compromised_multi_denom': 'multi-denom',
  92. 'num_compromised_linked_tx': 'linked address',
  93. 'num_compromised_torn_mine': 'TORN mining',
  94. 'conf': 'confidence score',
  95. 'entity': 'address type',
  96. 'balance': 'ETH balance',
  97. 'ens_name': 'ENS',
  98. '_distance': 'distance',
  99. 'exchange_address': 'exchange address',
  100. 'exchange_name': 'associated exchange',
  101. 'account_type': 'category',
  102. 'label': 'legitimacy',
  103. 'tags': 'other',
  104. 'num_deposits': 'total equal user deposits',
  105. 'compromised': 'compromised deposits',
  106. 'exact_match': 'address match reveals',
  107. 'multi_denom': 'multi-denom reveals',
  108. 'gas_price': 'unique gas price reveals',
  109. 'linked_tx': 'linked address reveals',
  110. 'linked_transaction': 'linked transaction',
  111. 'torn_mine': 'TORN mining',
  112. 'unique_gas_price': 'unique gas price',
  113. 'deposit_address_reuse': 'deposit address reuse',
  114. 'multi_denomination': 'multi-denomination',
  115. 'address_match': 'address match',
  116. 'all_reveals': 'all reveals',
  117. 'ranks': 'How has this address performed vs. other addresses?\n\nPercentile Ranking (100% = many reveals, 0% = no reveals)',
  118. 'num_transactions': 'Number of reveals',
  119. 'num_tcash': 'Tornado Cash Reveals',
  120. 'num_ethereum': 'Ethereum Reveals',
  121. 'tcash': 'tornado cash'
  122. }
  123. def get_known_attrs(known_addresses: pd.DataFrame, address: str) -> Dict[str, Any]:
  124. """
  125. Process features from the known_addresses dataframe for a single address.
  126. """
  127. result: pd.DataFrame = known_addresses[known_addresses.address == address]
  128. if len(result) == 0:
  129. return {}
  130. result: pd.DataFrame = result.fillna('')
  131. result: pd.Series = result.iloc[0]
  132. result: Dict[str, Any] = result.to_dict()
  133. del result['address']
  134. if 'label' in result and 'legitimacy' not in result:
  135. result['legitimacy'] = result['label']
  136. del result['label']
  137. if 'tags' in result:
  138. if len(result['tags']) == 0:
  139. del result['tags']
  140. return result
  141. def entity_to_str(i: int) -> str:
  142. if i == 0:
  143. return EOA
  144. elif i == 1:
  145. return DEPOSIT
  146. elif i == 2:
  147. return EXCHANGE
  148. elif i == 3:
  149. return DEX
  150. elif i == 4:
  151. return DEFI
  152. elif i == 5:
  153. return ICO_WALLET
  154. elif i == 6:
  155. return MINING
  156. elif i == 7:
  157. return TORNADO
  158. elif i == 8:
  159. return UNKNOWN
  160. else:
  161. raise Exception(f'Fatal error: {i}')
  162. def entity_to_int(s: str) -> int:
  163. if s == EOA:
  164. return 0
  165. elif s == DEPOSIT:
  166. return 1
  167. elif s == EXCHANGE:
  168. return 2
  169. elif s == DEX:
  170. return 3
  171. elif s == DEFI:
  172. return 4
  173. elif s == ICO_WALLET:
  174. return 5
  175. elif s == MINING:
  176. return 6
  177. elif s == TORNADO:
  178. return 7
  179. elif s == UNKNOWN:
  180. return 8
  181. else:
  182. raise Exception(f'Fatal error: {s}')
  183. def heuristic_to_str(s: int) -> str:
  184. if s == 0:
  185. return DEPO_REUSE_HEUR
  186. elif s == 1:
  187. return SAME_ADDR_HEUR
  188. elif s == 2:
  189. return GAS_PRICE_HEUR
  190. elif s == 3:
  191. return SAME_NUM_TX_HEUR
  192. elif s == 4:
  193. return LINKED_TX_HEUR
  194. elif s == 5:
  195. return TORN_MINE_HEUR
  196. elif s == 6:
  197. return DIFF2VEC_HEUR
  198. else:
  199. raise Exception(f'Fatal error: {s}')
  200. def heuristic_to_int(s: str) -> int:
  201. if s == DEPO_REUSE_HEUR:
  202. return 0
  203. elif s == SAME_ADDR_HEUR:
  204. return 1
  205. elif s == GAS_PRICE_HEUR:
  206. return 2
  207. elif s == SAME_NUM_TX_HEUR:
  208. return 3
  209. elif s == LINKED_TX_HEUR:
  210. return 4
  211. elif s == TORN_MINE_HEUR:
  212. return 5
  213. elif s == DIFF2VEC_HEUR:
  214. return 6
  215. else:
  216. raise Exception(f'Fatal error: {s}')
  217. def to_dict(
  218. addr: Address,
  219. table_cols: List[str],
  220. to_add: Dict = {},
  221. to_remove: List[str] = [],
  222. to_transform: List[Tuple[str, Any]] = [],
  223. ) -> Dict[str, Any]:
  224. """
  225. Convert a raw row into the form we want to send to the frontend.
  226. for `to_transform`, each element is in the tuple form:
  227. (key_to_override_value_of, function to take of the old value)
  228. """
  229. output: Dict[str, Any] = { # ignore cluster columns
  230. k: getattr(addr, k) for k in table_cols if 'cluster' not in k
  231. }
  232. output['conf'] = round(output['conf'], 3)
  233. del output['meta_data'] # Q: should we keep this?
  234. for k, v in to_add.items():
  235. if k not in output:
  236. output[k] = v
  237. for k in to_remove:
  238. if k in output:
  239. del output[k]
  240. for key, func in to_transform:
  241. output[key] = func(output[key])
  242. return output
  243. def default_address_response() -> Dict[str, Any]:
  244. output: Dict[str, Any] = {
  245. 'data': {
  246. 'query': {
  247. 'address': '',
  248. 'metadata': {},
  249. },
  250. 'tornado': {
  251. 'summary': {
  252. 'address': {
  253. 'num_deposit': 0,
  254. 'num_withdraw': 0,
  255. 'num_compromised': 0
  256. },
  257. },
  258. },
  259. 'cluster': [],
  260. 'metadata': {
  261. 'cluster_size': 0,
  262. 'num_pages': 0,
  263. 'page': 0,
  264. 'limit': 50,
  265. 'filter_by': {
  266. 'min_conf': 0,
  267. 'max_conf': 1,
  268. 'entity': '*',
  269. 'name': '*',
  270. },
  271. 'schema': {
  272. ADDRESS_COL: {
  273. 'type': 'string',
  274. },
  275. CONF_COL: {
  276. 'type': 'float',
  277. 'values': [0, 1],
  278. },
  279. ENTITY_COL: {
  280. 'type': 'category',
  281. 'values': [
  282. EOA,
  283. DEPOSIT,
  284. EXCHANGE,
  285. DEX,
  286. DEFI,
  287. ICO_WALLET,
  288. MINING,
  289. TORNADO,
  290. UNKNOWN,
  291. ],
  292. },
  293. NAME_COL: {
  294. 'type': 'string',
  295. },
  296. },
  297. 'sort_default': {
  298. 'attribute': ENTITY_COL,
  299. 'descending': False
  300. }
  301. }
  302. },
  303. 'success': 0,
  304. 'is_tornado': 0,
  305. }
  306. return output
  307. def default_tornado_response() -> Dict[str, Any]:
  308. output: Dict[str, Any] = {
  309. 'data': {
  310. 'query': {
  311. 'address': '',
  312. 'metadata': {
  313. 'amount': 0,
  314. 'currency': '',
  315. 'stats': {
  316. 'num_deposits': 0,
  317. 'num_compromised': 0,
  318. 'exact_match': 0,
  319. 'gas_price': 0,
  320. 'multi_denom': 0,
  321. 'linked_tx': 0,
  322. }
  323. },
  324. },
  325. 'deposits': [],
  326. 'compromised': {},
  327. 'metadata': {
  328. 'compromised_size': 0,
  329. 'num_pages': 0,
  330. 'page': 0,
  331. 'limit': 50,
  332. 'schema': {
  333. CONF_COL: {
  334. 'type': 'float',
  335. 'values': [0, 1]
  336. },
  337. HEURISTIC_COL: {
  338. 'type': 'category',
  339. 'values': [
  340. DEPO_REUSE_HEUR,
  341. SAME_ADDR_HEUR,
  342. GAS_PRICE_HEUR,
  343. SAME_NUM_TX_HEUR,
  344. LINKED_TX_HEUR,
  345. TORN_MINE_HEUR,
  346. DIFF2VEC_HEUR,
  347. ],
  348. },
  349. },
  350. 'sort_default': {
  351. 'attribute': CONF_COL,
  352. 'descending': True
  353. }
  354. },
  355. },
  356. 'success': 1,
  357. 'is_tornado': 1,
  358. }
  359. return output
  360. def default_transaction_response() -> Dict[str, Any]:
  361. output: Dict[str, Any] = {
  362. 'data': {
  363. 'query': {
  364. 'address': '',
  365. 'start_date': '',
  366. 'end_date': '',
  367. 'metadata': {
  368. 'stats': {
  369. 'num_transactions': 0,
  370. 'num_ethereum': {
  371. DEPO_REUSE_HEUR: 0,
  372. },
  373. 'num_tcash': {
  374. SAME_ADDR_HEUR: 0,
  375. GAS_PRICE_HEUR: 0,
  376. SAME_NUM_TX_HEUR: 0,
  377. LINKED_TX_HEUR: 0,
  378. TORN_MINE_HEUR: 0,
  379. },
  380. },
  381. 'ranks': {
  382. 'overall': 0,
  383. 'ethereum': {
  384. DEPO_REUSE_HEUR: 0,
  385. },
  386. 'tcash': {
  387. SAME_ADDR_HEUR: 0,
  388. GAS_PRICE_HEUR: 0,
  389. SAME_NUM_TX_HEUR: 0,
  390. LINKED_TX_HEUR: 0,
  391. TORN_MINE_HEUR: 0,
  392. },
  393. },
  394. },
  395. },
  396. 'transactions': [],
  397. 'plotdata': [],
  398. 'metadata': {
  399. 'num_pages': 0,
  400. 'page': 0,
  401. 'limit': 50,
  402. 'window': '1yr',
  403. 'schema': {
  404. HEURISTIC_COL: {
  405. 'type': 'category',
  406. 'values': [
  407. DEPO_REUSE_HEUR,
  408. SAME_ADDR_HEUR,
  409. GAS_PRICE_HEUR,
  410. SAME_NUM_TX_HEUR,
  411. LINKED_TX_HEUR,
  412. TORN_MINE_HEUR,
  413. DIFF2VEC_HEUR,
  414. ],
  415. },
  416. },
  417. },
  418. },
  419. 'success': 1,
  420. }
  421. return output
  422. def default_plot_response() -> Dict[str, Any]:
  423. output: Dict[str, Any] = {
  424. 'query': {
  425. 'window': '1yr',
  426. 'start_date': '',
  427. 'end_date': '',
  428. 'metadata': {
  429. 'num_points': 0,
  430. 'today': get_today_date_str(),
  431. }
  432. },
  433. 'data': [],
  434. 'success': 1,
  435. }
  436. return output
  437. def is_valid_address(address: str) -> bool:
  438. address: str = address.lower().strip()
  439. if len(address) != 42:
  440. return False
  441. if len(address) > 2:
  442. if address[:2] != '0x':
  443. return False
  444. if len(address.split()) != 1:
  445. return False
  446. return True
  447. class PlotRequestChecker:
  448. def __init__(
  449. self,
  450. request: Any,
  451. default_window: str = '1yr',
  452. ):
  453. self._request: Any = request
  454. self._default_window: str = default_window
  455. self._params: Dict[str, Any] = {}
  456. def check(self):
  457. return self._check_address() and self._check_window()
  458. def _check_address(self) -> bool:
  459. """
  460. Check that the first two chars are 0x and that the string
  461. is 42 chars long. Check that there are no spaces.
  462. """
  463. address: str = self._request.args.get('address', '')
  464. is_valid: bool = is_valid_address(address)
  465. if is_valid: # if not valid, don't save this
  466. self._params['address'] = address
  467. return is_valid
  468. def _check_window(self) -> bool:
  469. default: str = self._default_window
  470. window: Union[str, str] = self._request.args.get('window', default)
  471. if window not in ['1mth', '3mth', '6mth', '1yr', '3yr', '5yr']:
  472. window = '1yr'
  473. self._params['window'] = window
  474. return True
  475. def get(self, k: str) -> Optional[Any]:
  476. return self._params.get(k, None)
  477. def to_str(self):
  478. _repr: Dict[str, Any] = copy(self._params)
  479. return json.dumps(_repr, sort_keys=True)
  480. class TransactionRequestChecker:
  481. def __init__(
  482. self,
  483. request: Any,
  484. default_page: int = 0,
  485. default_limit: int = 50,
  486. default_start_date: str = '01/01/2013', # pick a date pre-ethereum
  487. default_end_date: str = get_today_date_str(),
  488. ):
  489. self._request: Any = request
  490. self._default_page: int = default_page
  491. self._default_limit: int = default_limit
  492. self._default_start_date: str = default_start_date
  493. self._default_end_date: str = default_end_date
  494. self._params: Dict[str, Any] = {}
  495. def check(self):
  496. return (self._check_address() and self._check_page() and self._check_limit() and
  497. self._check_start_date() and self._check_end_date())
  498. def _check_address(self) -> bool:
  499. """
  500. Check that the first two chars are 0x and that the string
  501. is 42 chars long. Check that there are no spaces.
  502. """
  503. address: str = self._request.args.get('address', '')
  504. is_valid: bool = is_valid_address(address)
  505. if is_valid: # if not valid, don't save this
  506. self._params['address'] = address
  507. return is_valid
  508. def _check_page(self) -> bool:
  509. # intentionally only returns True as we don't want to block a user
  510. # bc of typo on page
  511. default: int = self._default_page
  512. page: Union[str, int] = self._request.args.get('page', default)
  513. page: int = safe_int(page, default)
  514. page: int = max(page, 0) # at least 0
  515. self._params['page'] = page
  516. return True
  517. def _check_limit(self) -> bool:
  518. # intentionally only returns True as we don't want to block a user
  519. # bc of typo on limit
  520. default: int = self._default_limit
  521. limit: Union[str, int] = self._request.args.get('limit', default)
  522. limit: int = safe_int(limit, default)
  523. limit: int = min(max(limit, 1), default) # at least 1
  524. self._params['limit'] = limit
  525. return True
  526. def _check_start_date(self) -> bool:
  527. default: int = self._default_start_date
  528. start_date = self._request.args.get('start_date', default)
  529. try:
  530. start_date_obj = datetime.strptime(start_date, '%m/%d/%Y')
  531. self._params['start_date'] = start_date
  532. self._params['start_date_obj'] = start_date_obj
  533. return True
  534. except:
  535. return False
  536. def _check_end_date(self) -> bool:
  537. default: int = self._default_end_date
  538. end_date = self._request.args.get('end_date', default)
  539. try:
  540. end_date_obj = datetime.strptime(end_date, '%m/%d/%Y')
  541. self._params['end_date'] = end_date
  542. self._params['end_date_obj'] = end_date_obj
  543. return True
  544. except:
  545. return False
  546. def get(self, k: str) -> Optional[Any]:
  547. return self._params.get(k, None)
  548. def to_str(self):
  549. _repr: Dict[str, Any] = copy(self._params)
  550. del _repr['start_date_obj'], _repr['end_date_obj']
  551. return json.dumps(_repr, sort_keys=True)
  552. class AddressRequestChecker:
  553. """
  554. Given a request object with its args, make sure that it is
  555. providing valid arguments.
  556. """
  557. def __init__(
  558. self,
  559. request: Any,
  560. table_cols: List[str],
  561. entity_key: str = 'entity',
  562. conf_key: str = 'confidence',
  563. name_key: str = 'name',
  564. default_page: int = 0,
  565. default_limit: int = 50,
  566. ):
  567. self._request: Any = request
  568. self._table_cols: List[str] = table_cols
  569. self._entity_key: str = entity_key
  570. self._conf_key: str = conf_key
  571. self._name_key: str = name_key
  572. self._default_page: int = default_page
  573. self._default_limit: int = default_limit
  574. self._params: Dict[str, Any] = {}
  575. def check(self):
  576. return (self._check_address() and
  577. self._check_page() and
  578. self._check_limit() and
  579. self._check_sort_by() and
  580. self._check_filter_by())
  581. def _check_address(self) -> bool:
  582. """
  583. Check that the first two chars are 0x and that the string
  584. is 42 chars long. Check that there are no spaces.
  585. """
  586. address: str = self._request.args.get('address', '')
  587. is_valid: bool = is_valid_address(address)
  588. if is_valid: # if not valid, don't save this
  589. self._params['address'] = address
  590. return is_valid
  591. def _check_page(self) -> bool:
  592. # intentionally only returns True as we don't want to block a user
  593. # bc of typo on page
  594. default: int = self._default_page
  595. page: Union[str, int] = self._request.args.get('page', default)
  596. page: int = safe_int(page, default)
  597. page: int = max(page, 0) # at least 0
  598. self._params['page'] = page
  599. return True
  600. def _check_limit(self) -> bool:
  601. # intentionally only returns True as we don't want to block a user
  602. # bc of typo on limit
  603. default: int = self._default_limit
  604. limit: Union[str, int] = self._request.args.get('limit', default)
  605. limit: int = safe_int(limit, default)
  606. limit: int = min(max(limit, 1), default) # at least 1
  607. self._params['limit'] = limit
  608. return True
  609. def _check_sort_by(self) -> bool:
  610. default: str = self._entity_key
  611. sort_by: str = self._request.args.get('sort', default)
  612. # make sure column is a support column
  613. if sort_by not in self._table_cols:
  614. sort_by: str = default
  615. if not self._request.args.get('sort'):
  616. desc_sort = False # default is entity asc
  617. else:
  618. is_desc = self._request.args.get('descending', False)
  619. if type(is_desc) == str:
  620. desc_sort = is_desc.lower() != 'false'
  621. else:
  622. desc_sort = False
  623. self._params['sort_by'] = sort_by
  624. self._params['desc_sort'] = desc_sort
  625. return True
  626. def _check_filter_by(self) -> bool:
  627. filter_min_conf: float = \
  628. self._request.args.get(f'filter_min_{self._conf_key}', 0)
  629. filter_max_conf: float = \
  630. self._request.args.get(f'filter_max_{self._conf_key}', 1)
  631. filter_min_conf: float = safe_float(filter_min_conf, 0)
  632. filter_max_conf: float = safe_float(filter_max_conf, 1)
  633. filter_entity: str = \
  634. self._request.args.get(f'filter_{self._entity_key}', '*')
  635. if filter_entity not in [
  636. EOA, DEPOSIT, EXCHANGE, DEX, DEFI, ICO_WALLET, MINING]:
  637. filter_entity: str = '*'
  638. filter_name: str = \
  639. self._request.args.get(f'filter_{self._name_key}', '*')
  640. filter_by: List[Any] = []
  641. # the below will fail if address doesn't exist in table
  642. if Address.query.filter_by(address = self._params['address']).first():
  643. if ((filter_min_conf >= 0 and filter_min_conf <= 1) and
  644. (filter_max_conf >= 0 and filter_max_conf <= 1) and
  645. (filter_min_conf <= filter_max_conf)):
  646. filter_by.append(Address.conf >= filter_min_conf)
  647. filter_by.append(Address.conf <= filter_max_conf)
  648. if filter_entity != '*':
  649. filter_by.append(Address.entity == entity_to_int(filter_entity))
  650. if filter_name != '*': # search either
  651. filter_by.append(
  652. # or_(
  653. # Address.name.ilike('%'+filter_name.lower()+'%'),
  654. Address.address.ilike(filter_name.lower()),
  655. # ),
  656. )
  657. self._params['filter_by'] = filter_by
  658. self._params['filter_min_conf'] = filter_min_conf
  659. self._params['filter_max_conf'] = filter_max_conf
  660. self._params['filter_entity'] = filter_entity
  661. self._params['filter_name'] = filter_name
  662. return True
  663. def get(self, k: str) -> Optional[Any]:
  664. return self._params.get(k, None)
  665. def to_str(self):
  666. _repr: Dict[str, Any] = copy(self._params)
  667. del _repr['filter_by']
  668. return json.dumps(_repr, sort_keys=True)
  669. class TornadoPoolRequestChecker:
  670. def __init__(self, request: Any, default_page: int = 0, default_limit: int = 50):
  671. self._request: Any = request
  672. self._default_page: int = default_page
  673. self._default_limit: int = default_limit
  674. self._params: Dict[str, Any] = {}
  675. def check(self):
  676. return self._check_address() and self._check_page() and \
  677. self._check_limit() and self._check_return_tx()
  678. def _check_address(self) -> bool:
  679. """
  680. Check that the first two chars are 0x and that the string
  681. is 42 chars long. Check that there are no spaces.
  682. """
  683. address: str = self._request.args.get('address', '')
  684. is_valid: bool = is_valid_address(address)
  685. if is_valid: # if not valid, don't save this
  686. self._params['address'] = address
  687. return is_valid
  688. def _check_page(self) -> bool:
  689. # intentionally only returns True as we don't want to block a user
  690. # bc of typo on page
  691. default: int = self._default_page
  692. page: Union[str, int] = self._request.args.get('page', default)
  693. page: int = safe_int(page, default)
  694. page: int = max(page, 0) # at least 0
  695. self._params['page'] = page
  696. return True
  697. def _check_limit(self) -> bool:
  698. # intentionally only returns True as we don't want to block a user
  699. # bc of typo on limit
  700. default: int = self._default_limit
  701. limit: Union[str, int] = self._request.args.get('limit', default)
  702. limit: int = safe_int(limit, default)
  703. limit: int = min(max(limit, 1), default) # at least 1
  704. self._params['limit'] = limit
  705. return True
  706. def _check_return_tx(self) -> bool:
  707. return_tx: Union[str, bool] = self._request.args.get('return_tx', False)
  708. return_tx: bool = safe_bool(return_tx, False)
  709. self._params['return_tx'] = return_tx
  710. return True
  711. def get(self, k: str) -> Optional[Any]:
  712. return self._params.get(k, None)
  713. def to_str(self):
  714. _repr: Dict[str, Any] = copy(self._params)
  715. return json.dumps(_repr, sort_keys=True)
  716. # -- Tornado pool utilities --
  717. def is_tornado_address(address: str) -> bool:
  718. return TornadoPool.query.filter_by(pool = address).count() > 0
  719. def get_equal_user_deposit_txs(address: str) -> Set[str]:
  720. rows: List[TornadoPool] = \
  721. TornadoPool.query.filter_by(pool = address).all()
  722. txs: List[str] = [row.transaction for row in rows]
  723. return set(txs)
  724. def find_reveals(transactions: Set[str], class_: Any) -> Set[str]:
  725. transactions: List[str] = list(transactions)
  726. rows: List[class_] = \
  727. class_.query.filter(class_.transaction.in_(transactions)).all()
  728. clusters: List[int] = list(set([row.cluster for row in rows]))
  729. rows: List[class_] = \
  730. class_.query.filter(class_.cluster.in_(clusters)).all()
  731. reveals: Set[str] = set([row.transaction for row in rows])
  732. reveals: Set[str] = reveals.intersection(transactions)
  733. return reveals