├── .gitignore ├── .gitmodules ├── .pylintrc ├── LICENSE ├── README.md ├── anydex ├── __init__.py ├── config.py ├── core │ ├── __init__.py │ ├── assetamount.py │ ├── assetpair.py │ ├── block.py │ ├── bloomfilter.py │ ├── cache.py │ ├── clearing_policy.py │ ├── community.py │ ├── database.py │ ├── defs.py │ ├── match_queue.py │ ├── matching_engine.py │ ├── message.py │ ├── order.py │ ├── order_manager.py │ ├── order_repository.py │ ├── orderbook.py │ ├── payload.py │ ├── payment.py │ ├── payment_id.py │ ├── price.py │ ├── pricelevel.py │ ├── pricelevel_list.py │ ├── settings.py │ ├── side.py │ ├── tick.py │ ├── tickentry.py │ ├── timeout.py │ ├── timestamp.py │ ├── trade.py │ ├── transaction.py │ ├── transaction_manager.py │ ├── transaction_repository.py │ └── wallet_address.py ├── restapi │ ├── __init__.py │ ├── asks_bids_endpoint.py │ ├── base_market_endpoint.py │ ├── matchmakers_endpoint.py │ ├── orders_endpoint.py │ ├── root_endpoint.py │ ├── state_endpoint.py │ ├── transactions_endpoint.py │ ├── wallets_endpoint.py │ └── websocket.py ├── test │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_assetamount.py │ │ ├── test_assetpair.py │ │ ├── test_block.py │ │ ├── test_community.py │ │ ├── test_database.py │ │ ├── test_match_queue.py │ │ ├── test_matching_engine.py │ │ ├── test_matching_strategy.py │ │ ├── test_message.py │ │ ├── test_order.py │ │ ├── test_order_repository.py │ │ ├── test_orderbook.py │ │ ├── test_payment.py │ │ ├── test_payment_id.py │ │ ├── test_portfolio.py │ │ ├── test_price.py │ │ ├── test_pricelevel.py │ │ ├── test_pricelevel_list.py │ │ ├── test_side.py │ │ ├── test_tick.py │ │ ├── test_tickentry.py │ │ ├── test_timeout.py │ │ ├── test_timestamp.py │ │ ├── test_trade.py │ │ ├── test_transaction.py │ │ ├── test_transaction_manager.py │ │ ├── test_transaction_repository.py │ │ └── test_wallet_address.py │ ├── restapi │ │ ├── __init__.py │ │ ├── base.py │ │ ├── test_market_endpoints.py │ │ └── test_wallets_endpoint.py │ ├── trustchain │ │ ├── __init__.py │ │ ├── test_block.py │ │ ├── test_community.py │ │ └── test_database.py │ ├── util.py │ └── wallet │ │ ├── __init__.py │ │ ├── test_btc_wallet.py │ │ ├── test_dummy_wallet.py │ │ └── test_trustchain_wallet.py ├── trustchain │ ├── __init__.py │ ├── block.py │ ├── blockcache.py │ ├── caches.py │ ├── community.py │ ├── database.py │ ├── listener.py │ ├── payload.py │ └── settings.py ├── util │ ├── __init__.py │ └── asyncio.py └── wallet │ ├── __init__.py │ ├── bandwidth_block.py │ ├── bitcoinlib_main.py │ ├── btc_wallet.py │ ├── dummy_wallet.py │ ├── tc_wallet.py │ └── wallet.py ├── main.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | **.pyc 2 | **.DS_Store 3 | .idea/** 4 | **/_trial_temp/** 5 | sqlite/** 6 | ec.pem 7 | _trial_temp.lock 8 | anydex/test/temp/** 9 | .coverage 10 | cover/** 11 | build/** 12 | dist/** 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/anydex-core/596fd172d36068fbf519f07abfcaafafc984365e/.gitmodules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Linux**: [![](http://jenkins-ci.tribler.org/job/anydex/job/test_anydex_linux/badge/icon)](http://jenkins-ci.tribler.org/job/anydex/job/test_anydex_linux/) 2 | 3 | # AnyDex: The Universal Decentralized Exchange 4 | 5 | AnyDex is a universal decentralized exchange, designed to trade *any* digital asset in a quick and secure manner. 6 | -------------------------------------------------------------------------------- /anydex/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains code for Anydex. 3 | """ 4 | -------------------------------------------------------------------------------- /anydex/config.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | default = { 4 | 'address': '0.0.0.0', 5 | 'port': 8090, 6 | 'keys': [ 7 | { 8 | 'alias': "main key", 9 | 'generation': u"curve25519", 10 | 'file': u"ec.pem" 11 | } 12 | ], 13 | 'logger': { 14 | 'level': "INFO" 15 | }, 16 | 'walker_interval': 0.5, 17 | 'overlays': [ 18 | { 19 | 'class': 'DiscoveryCommunity', 20 | 'key': "main key", 21 | 'walkers': [ 22 | { 23 | 'strategy': "RandomWalk", 24 | 'peers': 20, 25 | 'init': { 26 | 'timeout': 3.0 27 | } 28 | }, 29 | { 30 | 'strategy': "RandomChurn", 31 | 'peers': -1, 32 | 'init': { 33 | 'sample_size': 8, 34 | 'ping_interval': 10.0, 35 | 'inactive_time': 27.5, 36 | 'drop_time': 57.5 37 | } 38 | }, 39 | { 40 | 'strategy': "PeriodicSimilarity", 41 | 'peers': -1, 42 | 'init': {} 43 | } 44 | ], 45 | 'initialize': {}, 46 | 'on_start': [ 47 | ('resolve_dns_bootstrap_addresses', ) 48 | ] 49 | }, 50 | { 51 | 'class': 'TrustChainCommunity', 52 | 'key': "main key", 53 | 'walkers': [{ 54 | 'strategy': "EdgeWalk", 55 | 'peers': 20, 56 | 'init': { 57 | 'edge_length': 4, 58 | 'neighborhood_size': 6, 59 | 'edge_timeout': 3.0 60 | } 61 | }], 62 | 'initialize': {}, 63 | 'on_start': [] 64 | }, 65 | { 66 | 'class': 'DHTDiscoveryCommunity', 67 | 'key': "main key", 68 | 'walkers': [{ 69 | 'strategy': "RandomWalk", 70 | 'peers': 20, 71 | 'init': { 72 | 'timeout': 3.0 73 | } 74 | }], 75 | 'initialize': {}, 76 | 'on_start': [] 77 | } 78 | ] 79 | } 80 | 81 | 82 | def get_anydex_configuration(): 83 | return copy.deepcopy(default) 84 | 85 | 86 | __all__ = ['get_anydex_configuration'] 87 | -------------------------------------------------------------------------------- /anydex/core/__init__.py: -------------------------------------------------------------------------------- 1 | class DeclinedTradeReason(object): 2 | ORDER_COMPLETED = 0 3 | ORDER_EXPIRED = 1 4 | ORDER_RESERVED = 2 5 | ORDER_INVALID = 3 6 | ORDER_CANCELLED = 4 7 | UNACCEPTABLE_PRICE = 5 8 | ADDRESS_LOOKUP_FAIL = 6 9 | NO_AVAILABLE_QUANTITY = 7 10 | ALREADY_TRADING = 8 11 | OTHER = 9 12 | 13 | 14 | class DeclineMatchReason(object): 15 | ORDER_COMPLETED = 0 16 | OTHER_ORDER_COMPLETED = 1 17 | OTHER_ORDER_CANCELLED = 2 18 | OTHER = 3 19 | 20 | 21 | MAX_ORDER_TIMEOUT = 3600 * 24 22 | 23 | VERSION = 0.1 24 | -------------------------------------------------------------------------------- /anydex/core/assetamount.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict 4 | 5 | 6 | class AssetAmount: 7 | """ 8 | This class represents a specific number of assets. It contains various utility methods to add/substract asset 9 | amounts. 10 | """ 11 | 12 | def __init__(self, amount: int, asset_id: str) -> None: 13 | """ 14 | :param amount: Integer representation of the asset amount 15 | :param asset_id: Identifier of the asset type of this amount 16 | """ 17 | if not isinstance(amount, int): 18 | raise ValueError("Price must be an integer") 19 | 20 | if not isinstance(asset_id, str): 21 | raise ValueError("Asset id must be a string") 22 | 23 | self._amount = amount 24 | self._asset_id = asset_id 25 | 26 | @property 27 | def asset_id(self) -> str: 28 | return self._asset_id 29 | 30 | @property 31 | def amount(self) -> int: 32 | return self._amount 33 | 34 | def __str__(self) -> str: 35 | return "%d %s" % (self.amount, self.asset_id) 36 | 37 | def __add__(self, other: AssetAmount) -> AssetAmount: 38 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 39 | return self.__class__(self.amount + other.amount, self.asset_id) 40 | else: 41 | return NotImplemented 42 | 43 | def __sub__(self, other: AssetAmount) -> AssetAmount: 44 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 45 | return self.__class__(self.amount - other.amount, self.asset_id) 46 | else: 47 | return NotImplemented 48 | 49 | def __lt__(self, other: AssetAmount) -> bool: 50 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 51 | return self.amount < other.amount 52 | else: 53 | return NotImplemented 54 | 55 | def __le__(self, other: AssetAmount) -> bool: 56 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 57 | return self.amount <= other.amount 58 | else: 59 | return NotImplemented 60 | 61 | def __eq__(self, other: AssetAmount) -> bool: 62 | if not isinstance(other, AssetAmount) or self.asset_id != other.asset_id: 63 | return NotImplemented 64 | else: 65 | return self.amount == other.amount 66 | 67 | def __ne__(self, other: AssetAmount) -> bool: 68 | return not self.__eq__(other) 69 | 70 | def __gt__(self, other: AssetAmount) -> bool: 71 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 72 | return self.amount > other.amount 73 | else: 74 | return NotImplemented 75 | 76 | def __ge__(self, other: AssetAmount) -> bool: 77 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 78 | return self.amount >= other.amount 79 | else: 80 | return NotImplemented 81 | 82 | def __floordiv__(self, other: AssetAmount) -> AssetAmount: 83 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 84 | return self.__class__(self.amount // other.amount, self.asset_id) 85 | else: 86 | return NotImplemented 87 | 88 | def __truediv__(self, other: AssetAmount) -> AssetAmount: 89 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 90 | return self.__class__(self.amount // other.amount, self.asset_id) 91 | else: 92 | return NotImplemented 93 | 94 | def __mod__(self, other: AssetAmount) -> AssetAmount: 95 | if isinstance(other, AssetAmount) and self.asset_id == other.asset_id: 96 | return self.__class__(self.amount % other.amount, self.asset_id) 97 | else: 98 | return NotImplemented 99 | 100 | def __hash__(self) -> int: 101 | return hash((self.amount, self.asset_id)) 102 | 103 | def to_dictionary(self) -> Dict: 104 | return { 105 | "amount": self.amount, 106 | "type": self.asset_id 107 | } 108 | -------------------------------------------------------------------------------- /anydex/core/assetpair.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict 4 | 5 | from anydex.core.assetamount import AssetAmount 6 | from anydex.core.price import Price 7 | 8 | 9 | class AssetPair: 10 | """ 11 | An asset pair represents a pair of specific amounts of assets, i.e. 10 BTC - 20 MB. 12 | It is used when dealing with orders in the market. 13 | """ 14 | 15 | def __init__(self, first: AssetAmount, second: AssetAmount) -> None: 16 | if first.asset_id > second.asset_id: 17 | raise ValueError("Asset %s must be smaller than %s" % (first, second)) 18 | 19 | self.first = first 20 | self.second = second 21 | 22 | def __eq__(self, other: AssetPair) -> bool: 23 | if not isinstance(other, AssetPair): 24 | return NotImplemented 25 | else: 26 | return self.first == other.first and self.second == other.second 27 | 28 | def to_dictionary(self) -> Dict: 29 | return { 30 | "first": self.first.to_dictionary(), 31 | "second": self.second.to_dictionary() 32 | } 33 | 34 | @classmethod 35 | def from_dictionary(cls, dictionary: Dict) -> AssetPair: 36 | return cls(AssetAmount(dictionary["first"]["amount"], dictionary["first"]["type"]), 37 | AssetAmount(dictionary["second"]["amount"], dictionary["second"]["type"])) 38 | 39 | @property 40 | def price(self) -> Price: 41 | """ 42 | Return a Price object of this asset pair, which expresses the second asset into the first asset. 43 | """ 44 | return Price(self.second.amount, self.first.amount, self.second.asset_id, self.first.asset_id) 45 | 46 | @staticmethod 47 | def from_price(price: Price, first_amount: int) -> AssetPair: 48 | return AssetPair(AssetAmount(first_amount, price.denom_type), 49 | AssetAmount(int(price.frac * first_amount), price.num_type)) 50 | 51 | def proportional_downscale(self, first: int = None, second: int = None): 52 | """ 53 | This method constructs a new AssetPair where the ratio between the first/second asset is preserved. 54 | One should specify a new amount for the first asset. 55 | For instance, if we have an asset pair (4 BTC, 8 MB), the price is 8/4 = 2 MB/BTC. 56 | If we now change the amount of the first asset from 4 BTC to 1 BTC, the new AssetPair becomes (1 BTC, 2 MB). 57 | Likewise, if the second asset is changed to 4, the new AssetPair becomes (2 BTC, 4 MB) 58 | """ 59 | if first: 60 | return AssetPair(AssetAmount(first, self.first.asset_id), 61 | AssetAmount(int(self.price.amount * first), self.second.asset_id)) 62 | elif second: 63 | return AssetPair(AssetAmount(int(second / self.price.amount), self.first.asset_id), 64 | AssetAmount(second, self.second.asset_id)) 65 | else: 66 | raise ValueError("No first/second provided in proportional downscale!") 67 | 68 | def __str__(self) -> str: 69 | return "%s %s" % (self.first, self.second) 70 | -------------------------------------------------------------------------------- /anydex/core/clearing_policy.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import logging 3 | from binascii import hexlify, unhexlify 4 | 5 | from ipv8.peer import Peer 6 | 7 | 8 | class ClearingPolicy(metaclass=abc.ABCMeta): 9 | """ 10 | The clearing policy determines whether we should trade with a specific counterparty. 11 | """ 12 | 13 | def __init__(self, community): 14 | """ 15 | Initialize a clearing policy. 16 | :param community: The MarketCommunity, used to fetch information from. 17 | """ 18 | self.community = community 19 | self.logger = logging.getLogger(self.__class__.__name__) 20 | 21 | @abc.abstractmethod 22 | async def should_trade(self, trader_id): 23 | """ 24 | :param trader_id: The ID of the trader. 25 | :type trader_id: TraderId 26 | :return: A Deferred that fires with a boolean whether we should trade or not. 27 | """ 28 | return True 29 | 30 | 31 | class SingleTradeClearingPolicy(ClearingPolicy): 32 | """ 33 | This policy limits a trading partner to a maximum number of outstanding trades with risky counterparties at once. 34 | This is achieved by a crawl/inspection of the TrustChain records of a counterparty. 35 | """ 36 | 37 | def __init__(self, community, max_concurrent_trades): 38 | ClearingPolicy.__init__(self, community) 39 | self.max_concurrent_trades = max_concurrent_trades 40 | 41 | async def should_trade(self, trader_id): 42 | """ 43 | We first fetch the latest block of the counterparty and then determine whether we can trade with this party. 44 | """ 45 | self.logger.info("Triggering clearing policy for trade with trader %s", trader_id.as_hex()) 46 | 47 | address = await self.community.get_address_for_trader(trader_id) 48 | if not address: 49 | self.logger.info("Clearing policy is unable to determine address of trader %s", trader_id.as_hex()) 50 | return False 51 | 52 | # Get the public key of the peer 53 | peer_pk = await self.community.send_trader_pk_request(trader_id) 54 | peer = Peer(peer_pk, address=address) 55 | 56 | blocks = await self.community.trustchain.send_crawl_request(peer, peer_pk.key_to_bin(), -1, -1) 57 | if not blocks: 58 | self.logger.info("Counterparty did not send blocks, failing clearing policy") 59 | return False 60 | 61 | block = blocks[0] 62 | if block.type not in [b"ask", b"bid", b"cancel_order", b"tx_init", b"tx_payment", b"tx_done"]: 63 | self.logger.info("Unknown last block type %s, not trading with this counterparty", block.type) 64 | return False 65 | 66 | # The block must contain a responsibilities array 67 | do_trade = block.transaction["responsibilities"] < self.max_concurrent_trades 68 | if do_trade: 69 | self.logger.info("Will trade with trader %s (responsible trades: %d)", 70 | trader_id.as_hex(), block.transaction["responsibilities"]) 71 | else: 72 | self.logger.info("Will NOT trade with trader %s (responsible trades: %d)", 73 | trader_id.as_hex(), block.transaction["responsibilities"]) 74 | return do_trade 75 | -------------------------------------------------------------------------------- /anydex/core/defs.py: -------------------------------------------------------------------------------- 1 | # Message definitions 2 | MSG_MATCH = 7 3 | MSG_MATCH_DECLINE = 9 4 | MSG_PROPOSED_TRADE = 10 5 | MSG_DECLINED_TRADE = 11 6 | MSG_COUNTER_TRADE = 12 7 | MSG_ACCEPT_TRADE = 13 8 | MSG_ORDER_QUERY = 16 9 | MSG_ORDER_RESPONSE = 17 10 | MSG_BOOK_SYNC = 19 11 | MSG_PING = 20 12 | MSG_PONG = 21 13 | MSG_MATCH_DONE = 22 14 | MSG_PK_QUERY = 23 15 | MSG_PK_RESPONSE = 24 16 | -------------------------------------------------------------------------------- /anydex/core/match_queue.py: -------------------------------------------------------------------------------- 1 | from functools import cmp_to_key 2 | 3 | 4 | class MatchPriorityQueue(object): 5 | """ 6 | This priority queue keeps track of incoming match message for a specific order. 7 | """ 8 | def __init__(self, order): 9 | self.order = order 10 | self.queue = [] 11 | 12 | def __str__(self): 13 | return ' '.join([str(i) for i in self.queue]) 14 | 15 | def is_empty(self): 16 | return len(self.queue) == 0 17 | 18 | def contains_order(self, order_id): 19 | for _, _, other_order_id, _ in self.queue: 20 | if other_order_id == order_id: 21 | return True 22 | return False 23 | 24 | def insert(self, retries, price, order_id, other_quantity): 25 | self.queue.append((retries, price, order_id, other_quantity)) 26 | 27 | def cmp_items(tup1, tup2): 28 | if tup1[0] < tup2[0]: 29 | return -1 30 | elif tup1[0] > tup2[0]: 31 | return 1 32 | else: 33 | if self.order.is_ask(): 34 | if tup1[1] < tup2[1]: 35 | return 1 36 | else: 37 | return -1 38 | else: 39 | if tup1[1] < tup2[1]: 40 | return -1 41 | else: 42 | return 1 43 | 44 | self.queue = sorted(self.queue, key=cmp_to_key(cmp_items)) 45 | 46 | def delete(self): 47 | if not self.queue: 48 | return None 49 | 50 | return self.queue.pop(0) 51 | -------------------------------------------------------------------------------- /anydex/core/matching_engine.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABCMeta, abstractmethod 3 | from time import time 4 | 5 | 6 | class MatchingStrategy(object): 7 | """Matching strategy base class""" 8 | __metaclass__ = ABCMeta 9 | 10 | def __init__(self, order_book): 11 | """ 12 | :param order_book: The order book to search in 13 | :type order_book: OrderBook 14 | """ 15 | super(MatchingStrategy, self).__init__() 16 | self._logger = logging.getLogger(self.__class__.__name__) 17 | 18 | self.order_book = order_book 19 | 20 | @abstractmethod 21 | def match(self, order_id, price, quantity, is_ask): 22 | """ 23 | :param order_id: The order id of the tick to match 24 | :param price: The price to match against 25 | :param quantity: The quantity that should be matched 26 | :param is_ask: Whether the object we want to match is an ask 27 | :type order_id: OrderId 28 | :type price: Price 29 | :type quantity: Quantity 30 | :type is_ask: Bool 31 | :return: A list of tuples containing the ticks and the matched quantity 32 | :rtype: [(str, TickEntry)] 33 | """ 34 | return 35 | 36 | 37 | class PriceTimeStrategy(MatchingStrategy): 38 | """Strategy that uses the price time method for picking ticks""" 39 | 40 | def match(self, order_id, price, quantity, is_ask): 41 | """ 42 | :param order_id: The order id of the tick to match 43 | :param price: The price to match against 44 | :param quantity: The quantity that should be matched 45 | :param is_ask: Whether the object we want to match is an ask 46 | :type order_id: OrderId 47 | :type price: Price 48 | :type quantity: int 49 | :type is_ask: Bool 50 | :return: A list of tuples containing the ticks and the matched quantity 51 | :rtype: [(str, TickEntry, Quantity)] 52 | """ 53 | matched_ticks = [] 54 | quantity_to_match = quantity 55 | 56 | # First check whether we can match our order at all in the order book 57 | if is_ask: 58 | bid_price = self.order_book.get_bid_price(price.num_type, price.denom_type) 59 | if not bid_price or price > bid_price: 60 | return [] 61 | if not is_ask: 62 | ask_price = self.order_book.get_ask_price(price.num_type, price.denom_type) 63 | if not ask_price or price < ask_price: 64 | return [] 65 | 66 | # Next, check whether we have a price level we can start our match search from 67 | if is_ask: 68 | price_level = self.order_book.get_bid_price_level(price.num_type, price.denom_type) 69 | else: 70 | price_level = self.order_book.get_ask_price_level(price.num_type, price.denom_type) 71 | 72 | if not price_level: 73 | return [] 74 | 75 | cur_tick_entry = price_level.first_tick 76 | cur_price_level_price = price_level.price 77 | 78 | # We now start to iterate through price levels and tick entries and match on the fly 79 | while cur_tick_entry and quantity_to_match > 0: 80 | if cur_tick_entry.is_blocked_for_matching(order_id) or \ 81 | order_id.trader_id == cur_tick_entry.order_id.trader_id: 82 | cur_tick_entry = cur_tick_entry.next_tick 83 | continue 84 | 85 | quantity_matched = min(quantity_to_match, cur_tick_entry.available_for_matching) 86 | if quantity_matched > 0: 87 | matched_ticks.append(cur_tick_entry) 88 | quantity_to_match -= quantity_matched 89 | 90 | cur_tick_entry = cur_tick_entry.next_tick 91 | if not cur_tick_entry: 92 | # We probably reached the end of a price level, check whether we have a next price level 93 | try: 94 | # Get the next price level 95 | if is_ask: 96 | next_price_level = self.order_book.bids.\ 97 | get_price_level_list(price.num_type, price.denom_type).prev_item(cur_price_level_price) 98 | else: 99 | next_price_level = self.order_book.asks.\ 100 | get_price_level_list(price.num_type, price.denom_type).succ_item(cur_price_level_price) 101 | cur_price_level_price = next_price_level.price 102 | except IndexError: 103 | break 104 | 105 | if (is_ask and price > cur_price_level_price) or (not is_ask and price < cur_price_level_price): 106 | # The price of this price level is too high/low 107 | break 108 | 109 | cur_tick_entry = next_price_level.first_tick 110 | 111 | return matched_ticks 112 | 113 | 114 | class MatchingEngine(object): 115 | """Matches ticks and orders to the order book""" 116 | 117 | def __init__(self, matching_strategy): 118 | """ 119 | :param matching_strategy: The strategy to use 120 | :type matching_strategy: MatchingStrategy 121 | """ 122 | super(MatchingEngine, self).__init__() 123 | self._logger = logging.getLogger(self.__class__.__name__) 124 | 125 | self.matching_strategy = matching_strategy 126 | 127 | def match(self, tick_entry): 128 | """ 129 | :param tick_entry: The TickEntry that should be matched 130 | :type tick_entry: TickEntry 131 | :return: A list of tuples containing a random match id, ticks and the matched quantity 132 | :rtype: [(str, TickEntry)] 133 | """ 134 | now = time() 135 | 136 | matched_ticks = self.matching_strategy.match(tick_entry.order_id, 137 | tick_entry.price, 138 | tick_entry.available_for_matching, 139 | tick_entry.tick.is_ask()) 140 | 141 | diff = time() - now 142 | return matched_ticks 143 | -------------------------------------------------------------------------------- /anydex/core/message.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify 2 | 3 | 4 | class TraderId(object): 5 | """Immutable class for representing the id of a trader.""" 6 | 7 | def __init__(self, trader_id): 8 | """ 9 | :param trader_id: String representing the trader id 10 | :type trader_id: bytes 11 | :raises ValueError: Thrown when one of the arguments are invalid 12 | """ 13 | super(TraderId, self).__init__() 14 | 15 | trader_id = trader_id if isinstance(trader_id, bytes) else bytes(trader_id) 16 | 17 | if len(trader_id) != 20: 18 | raise ValueError("Trader ID must be 20 bytes") 19 | 20 | self.trader_id = trader_id # type: bytes 21 | 22 | def __str__(self): 23 | return "%s" % self.trader_id 24 | 25 | def __bytes__(self): # type: () -> bytes 26 | return self.trader_id 27 | 28 | def as_hex(self): 29 | return hexlify(bytes(self)).decode('utf-8') 30 | 31 | def __eq__(self, other): 32 | return self.trader_id == other.trader_id 33 | 34 | def __ne__(self, other): 35 | return not self.__eq__(other) 36 | 37 | def __hash__(self): 38 | return hash(self.trader_id) 39 | 40 | 41 | class Message(object): 42 | """Abstract class for representing a message.""" 43 | 44 | def __init__(self, trader_id, timestamp): 45 | """ 46 | Don't use this class directly, use on of its implementations 47 | 48 | :param trader_id: The trader id of the message sender 49 | :param timestamp: A timestamp when the message was created 50 | :type trader_id: TraderId 51 | :type timestamp: Timestamp 52 | """ 53 | super(Message, self).__init__() 54 | 55 | self._trader_id = trader_id 56 | self._timestamp = timestamp 57 | 58 | @property 59 | def trader_id(self): 60 | """ 61 | :rtype: TraderId 62 | """ 63 | return self._trader_id 64 | 65 | @property 66 | def timestamp(self): 67 | """ 68 | :rtype: Timestamp 69 | """ 70 | return self._timestamp 71 | -------------------------------------------------------------------------------- /anydex/core/order_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from anydex.core.order import Order 4 | from anydex.core.order_repository import OrderRepository 5 | from anydex.core.timestamp import Timestamp 6 | 7 | 8 | class OrderManager(object): 9 | """Provides an interface to the user to manage the users orders""" 10 | 11 | def __init__(self, order_repository): 12 | """ 13 | :type order_repository: OrderRepository 14 | """ 15 | super(OrderManager, self).__init__() 16 | self._logger = logging.getLogger(self.__class__.__name__) 17 | self._logger.info("Market order manager initialized") 18 | 19 | self.order_repository = order_repository 20 | 21 | def create_ask_order(self, assets, timeout): 22 | """ 23 | Create an ask order (sell order) 24 | 25 | :param assets: The assets to be exchanged 26 | :param timeout: The timeout of the order, when does the order need to be timed out 27 | :type assets: AssetPair 28 | :type timeout: Timeout 29 | :return: The order that is created 30 | :rtype: Order 31 | """ 32 | order = Order(self.order_repository.next_identity(), assets, timeout, Timestamp.now(), True) 33 | self.order_repository.add(order) 34 | 35 | self._logger.info("Ask order created with id: " + str(order.order_id)) 36 | 37 | return order 38 | 39 | def create_bid_order(self, assets, timeout): 40 | """ 41 | Create a bid order (buy order) 42 | 43 | :param assets: The assets to be exchanged 44 | :param timeout: The timeout of the order, when does the order need to be timed out 45 | :type assets: AssetPair 46 | :type timeout: Timeout 47 | :return: The order that is created 48 | :rtype: Order 49 | """ 50 | order = Order(self.order_repository.next_identity(), assets, timeout, Timestamp.now(), False) 51 | self.order_repository.add(order) 52 | 53 | self._logger.info("Bid order created with id: " + str(order.order_id)) 54 | 55 | return order 56 | 57 | def cancel_order(self, order_id): 58 | """ 59 | Cancel an order that was created by the user. 60 | :return: The order that is created 61 | :rtype: Order 62 | """ 63 | order = self.order_repository.find_by_id(order_id) 64 | 65 | if order: 66 | order.cancel() 67 | self.order_repository.update(order) 68 | 69 | self._logger.info("Order cancelled with id: " + str(order_id)) 70 | -------------------------------------------------------------------------------- /anydex/core/order_repository.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABCMeta, abstractmethod 3 | 4 | from anydex.core.message import TraderId 5 | from anydex.core.order import OrderId, OrderNumber 6 | 7 | 8 | class OrderRepository(object): 9 | """A repository interface for orders in the order manager""" 10 | __metaclass__ = ABCMeta 11 | 12 | def __init__(self): 13 | """ 14 | Do not use this class directly 15 | 16 | Make a subclass of this class with a specific implementation for a storage backend 17 | """ 18 | super(OrderRepository, self).__init__() 19 | self._logger = logging.getLogger(self.__class__.__name__) 20 | 21 | @abstractmethod 22 | def find_all(self): 23 | return 24 | 25 | @abstractmethod 26 | def find_by_id(self, order_id): 27 | return 28 | 29 | @abstractmethod 30 | def add(self, order): 31 | return 32 | 33 | @abstractmethod 34 | def update(self, order): 35 | return 36 | 37 | @abstractmethod 38 | def delete_by_id(self, order_id): 39 | return 40 | 41 | @abstractmethod 42 | def next_identity(self): 43 | return 44 | 45 | 46 | class MemoryOrderRepository(OrderRepository): 47 | """A repository for orders in the order manager stored in memory""" 48 | 49 | def __init__(self, mid): 50 | """ 51 | :param mid: Hex encoded version of the member id of this node 52 | :type mid: str 53 | """ 54 | super(MemoryOrderRepository, self).__init__() 55 | 56 | self._logger.info("Memory order repository used") 57 | 58 | self._mid = mid 59 | self._next_id = 0 # Counter to keep track of the number of messages created by this repository 60 | 61 | self._orders = {} 62 | 63 | def find_all(self): 64 | """ 65 | :rtype: [Order] 66 | """ 67 | return self._orders.values() 68 | 69 | def find_by_id(self, order_id): 70 | """ 71 | :param order_id: The order id to look for 72 | :type order_id: OrderId 73 | :return: The order or null if it cannot be found 74 | :rtype: Order 75 | """ 76 | return self._orders.get(order_id) 77 | 78 | def add(self, order): 79 | """ 80 | :type order: Order 81 | """ 82 | self._logger.debug("Order with the id: " + str(order.order_id) + " was added to the order repository") 83 | 84 | self._orders[order.order_id] = order 85 | 86 | def update(self, order): 87 | """ 88 | :type order: Order 89 | """ 90 | self._logger.debug("Order with the id: " + str(order.order_id) + " was updated to the order repository") 91 | 92 | self._orders[order.order_id] = order 93 | 94 | def delete_by_id(self, order_id): 95 | """ 96 | :type order_id: OrderId 97 | """ 98 | self._logger.debug("Order with the id: " + str(order_id) + " was deleted from the order repository") 99 | 100 | del self._orders[order_id] 101 | 102 | def next_identity(self): 103 | """ 104 | :rtype: OrderId 105 | """ 106 | self._next_id += 1 107 | return OrderId(TraderId(self._mid), OrderNumber(self._next_id)) 108 | 109 | 110 | class DatabaseOrderRepository(OrderRepository): 111 | """A repository that stores orders in the database""" 112 | 113 | def __init__(self, mid, persistence): 114 | """ 115 | :param mid: Hex encoded version of the member id of this node 116 | :type mid: str 117 | """ 118 | super(DatabaseOrderRepository, self).__init__() 119 | 120 | self._logger.info("Memory order repository used") 121 | 122 | self._mid = mid 123 | self.persistence = persistence 124 | 125 | def find_all(self): 126 | """ 127 | :rtype: [Order] 128 | """ 129 | return self.persistence.get_all_orders() 130 | 131 | def find_by_id(self, order_id): 132 | """ 133 | :param order_id: The order id to look for 134 | :type order_id: OrderId 135 | :return: The order or null if it cannot be found 136 | :rtype: Order 137 | """ 138 | return self.persistence.get_order(order_id) 139 | 140 | def add(self, order): 141 | """ 142 | :param order: The order to add to the database 143 | :type order: Order 144 | """ 145 | self.persistence.add_order(order) 146 | 147 | def update(self, order): 148 | """ 149 | :param order: The order to update 150 | :type order: Order 151 | """ 152 | self.delete_by_id(order.order_id) 153 | self.add(order) 154 | 155 | def delete_by_id(self, order_id): 156 | """ 157 | :param order_id: The id of the order to remove 158 | """ 159 | self.persistence.delete_order(order_id) 160 | 161 | def next_identity(self): 162 | """ 163 | :rtype OrderId 164 | """ 165 | return OrderId(TraderId(self._mid), OrderNumber(self.persistence.get_next_order_number())) 166 | -------------------------------------------------------------------------------- /anydex/core/payment.py: -------------------------------------------------------------------------------- 1 | from binascii import unhexlify 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.message import Message, TraderId 5 | from anydex.core.payment_id import PaymentId 6 | from anydex.core.timestamp import Timestamp 7 | from anydex.core.transaction import TransactionId 8 | from anydex.core.wallet_address import WalletAddress 9 | 10 | 11 | class Payment(Message): 12 | """Class representing a payment.""" 13 | 14 | def __init__(self, trader_id, transaction_id, transferred_assets, address_from, address_to, payment_id, 15 | timestamp): 16 | super(Payment, self).__init__(trader_id, timestamp) 17 | self._transaction_id = transaction_id 18 | self._transferred_assets = transferred_assets 19 | self._address_from = address_from 20 | self._address_to = address_to 21 | self._payment_id = payment_id 22 | 23 | @classmethod 24 | def from_database(cls, data): 25 | """ 26 | Create a Payment object based on information in the database. 27 | """ 28 | (trader_id, transaction_id, payment_id, transferred_amount, transferred_id, 29 | address_from, address_to, timestamp) = data 30 | 31 | transaction_id = TransactionId(bytes(transaction_id)) 32 | return cls(TraderId(bytes(trader_id)), transaction_id, AssetAmount(transferred_amount, transferred_id.decode()), 33 | WalletAddress(str(address_from)), WalletAddress(str(address_to)), PaymentId(str(payment_id)), 34 | Timestamp(timestamp)) 35 | 36 | def to_database(self): 37 | """ 38 | Returns a database representation of a Payment object. 39 | :rtype: tuple 40 | """ 41 | return (bytes(self.trader_id), bytes(self.transaction_id), 42 | str(self.payment_id), self.transferred_assets.amount, 43 | self.transferred_assets.asset_id, str(self.address_from), 44 | str(self.address_to), int(self.timestamp)) 45 | 46 | @classmethod 47 | def from_block(cls, block): 48 | """ 49 | Restore a payment from a TrustChain block 50 | 51 | :param block: TrustChainBlock 52 | :return: Restored payment 53 | :rtype: Payment 54 | """ 55 | tx_dict = block.transaction["payment"] 56 | return cls(TraderId(unhexlify(tx_dict["trader_id"])), 57 | TransactionId(unhexlify(tx_dict["transaction_id"])), 58 | AssetAmount(tx_dict["transferred"]["amount"], tx_dict["transferred"]["type"]), 59 | WalletAddress(tx_dict["address_from"]), 60 | WalletAddress(tx_dict["address_to"]), 61 | PaymentId(tx_dict["payment_id"]), 62 | Timestamp(tx_dict["timestamp"])) 63 | 64 | @property 65 | def transaction_id(self): 66 | return self._transaction_id 67 | 68 | @property 69 | def transferred_assets(self): 70 | return self._transferred_assets 71 | 72 | @property 73 | def address_from(self): 74 | return self._address_from 75 | 76 | @property 77 | def address_to(self): 78 | return self._address_to 79 | 80 | @property 81 | def payment_id(self): 82 | return self._payment_id 83 | 84 | def to_dictionary(self): 85 | return { 86 | "trader_id": self.trader_id.as_hex(), 87 | "transaction_id": self.transaction_id.as_hex(), 88 | "transferred": self.transferred_assets.to_dictionary(), 89 | "payment_id": str(self.payment_id), 90 | "address_from": str(self.address_from), 91 | "address_to": str(self.address_to), 92 | "timestamp": int(self.timestamp), 93 | } 94 | -------------------------------------------------------------------------------- /anydex/core/payment_id.py: -------------------------------------------------------------------------------- 1 | 2 | class PaymentId(object): 3 | """Used for having a validated instance of a payment id that we can monitor.""" 4 | 5 | def __init__(self, payment_id): 6 | """ 7 | :param payment_id: String representation of the id of the payment 8 | :type payment_id: str 9 | :raises ValueError: Thrown when one of the arguments are invalid 10 | """ 11 | super(PaymentId, self).__init__() 12 | 13 | if not isinstance(payment_id, str): 14 | raise ValueError("Payment id must be a string") 15 | 16 | self._payment_id = payment_id 17 | 18 | @property 19 | def payment_id(self): 20 | """ 21 | Return the payment id. 22 | """ 23 | return self._payment_id 24 | 25 | def __str__(self): 26 | return "%s" % self._payment_id 27 | 28 | def __eq__(self, other): 29 | if not isinstance(other, PaymentId): 30 | return NotImplemented 31 | else: 32 | return self._payment_id == other.payment_id 33 | 34 | def __ne__(self, other): 35 | return not self.__eq__(other) 36 | 37 | def __hash__(self): 38 | return hash(self._payment_id) 39 | -------------------------------------------------------------------------------- /anydex/core/price.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | 3 | 4 | class Price(object): 5 | """ 6 | This class represents a price in the market. 7 | The price is simply a fraction that expresses one asset in another asset. 8 | For instance, 0.5 MB/BTC means that one exchanges 0.5 MB for 1 BTC. 9 | """ 10 | 11 | def __init__(self, num, denom, num_type, denom_type): 12 | self.num = num 13 | self.denom = denom 14 | self.num_type = num_type 15 | self.denom_type = denom_type 16 | self.frac = Fraction(num, denom) 17 | self.amount = float(self.frac) 18 | 19 | def __str__(self): 20 | return "%g %s/%s" % (self.amount, self.num_type, self.denom_type) 21 | 22 | def __lt__(self, other): 23 | if isinstance(other, Price) and self.num_type == other.num_type and self.denom_type == other.denom_type: 24 | return self.amount < other.amount 25 | else: 26 | return NotImplemented 27 | 28 | def __le__(self, other): 29 | if isinstance(other, Price) and self.num_type == other.num_type and self.denom_type == other.denom_type: 30 | return self.amount <= other.amount 31 | else: 32 | return NotImplemented 33 | 34 | def __ne__(self, other): 35 | if not isinstance(other, Price) or self.num_type != other.num_type or self.denom_type != other.denom_type: 36 | return NotImplemented 37 | return not self.__eq__(other) 38 | 39 | def __gt__(self, other): 40 | if isinstance(other, Price) and self.num_type == other.num_type and self.denom_type == other.denom_type: 41 | return self.amount > other.amount 42 | else: 43 | return NotImplemented 44 | 45 | def __ge__(self, other): 46 | if isinstance(other, Price) and self.num_type == other.num_type and self.denom_type == other.denom_type: 47 | return self.amount >= other.amount 48 | else: 49 | return NotImplemented 50 | 51 | def __eq__(self, other): 52 | if not isinstance(other, Price) or self.num_type != other.num_type or self.denom_type != other.denom_type: 53 | return NotImplemented 54 | else: 55 | return self.frac == other.frac 56 | 57 | def __hash__(self): 58 | return hash((hash(self.frac), self.num_type, self.denom_type)) 59 | -------------------------------------------------------------------------------- /anydex/core/pricelevel.py: -------------------------------------------------------------------------------- 1 | class PriceLevel(object): 2 | """Class to represents a list of ticks at a specific price level""" 3 | 4 | def __init__(self, price): 5 | self._head_tick = None # First tick of the double linked list 6 | self._tail_tick = None # Last tick of the double linked list 7 | self._length = 0 # The number of ticks in the price level 8 | self._depth = 0 # Total amount of quantity contained in this price level 9 | self._last = None # The current tick of the iterator 10 | self._price = price # The price of this price level 11 | 12 | @property 13 | def price(self): 14 | """ 15 | :rtype: Price 16 | """ 17 | return self._price 18 | 19 | @property 20 | def first_tick(self): 21 | """ 22 | :rtype: TickEntry 23 | """ 24 | return self._head_tick 25 | 26 | @property 27 | def length(self): 28 | """ 29 | Return the length of the amount of ticks contained in the price level 30 | :rtype: int 31 | """ 32 | return self._length 33 | 34 | @property 35 | def depth(self): 36 | """ 37 | The depth is equal to the total amount of volume contained in this price level 38 | :rtype: int 39 | """ 40 | return self._depth 41 | 42 | @depth.setter 43 | def depth(self, new_depth): 44 | """ 45 | :param new_depth: The new depth 46 | :type new_depth: int 47 | """ 48 | self._depth = new_depth 49 | 50 | def __len__(self): 51 | """ 52 | Return the length of the amount of ticks contained in the price level 53 | """ 54 | return self.length 55 | 56 | def __iter__(self): 57 | self._last = self._head_tick 58 | return self 59 | 60 | def __next__(self): 61 | """ 62 | Return the next tick in the price level for the iterator 63 | """ 64 | if self._last is None: 65 | raise StopIteration 66 | else: 67 | return_value = self._last 68 | self._last = self._last.next_tick 69 | return return_value 70 | 71 | def next(self): 72 | return self.__next__() 73 | 74 | def append_tick(self, tick): 75 | """ 76 | :type tick: TickEntry 77 | """ 78 | if self._length == 0: # Add the first tick 79 | tick.prev_tick = None 80 | tick.next_tick = None 81 | self._head_tick = tick 82 | self._tail_tick = tick 83 | else: # Add to the end of the existing ticks 84 | tick.prev_tick = self._tail_tick 85 | tick.next_tick = None 86 | self._tail_tick.next_tick = tick 87 | self._tail_tick = tick 88 | 89 | # Update the counters 90 | self._length += 1 91 | self._depth += tick.assets.first.amount 92 | 93 | def remove_tick(self, tick): 94 | """ 95 | Remove a specific tick from the price level. 96 | 97 | :param tick: The tick to be removed 98 | :type tick: TickEntry 99 | """ 100 | # Update the counters 101 | self._depth -= tick.assets.first.amount 102 | self._length -= 1 103 | 104 | if self._length == 0: # Was the only tick in this price level 105 | return 106 | 107 | prev_tick = tick.prev_tick 108 | next_tick = tick.next_tick 109 | if prev_tick is not None and next_tick is not None: # TickEntry in between to other ticks 110 | prev_tick.next_tick = next_tick 111 | next_tick.prev_tick = prev_tick 112 | elif next_tick is not None: # First tick 113 | next_tick.prev_tick = None 114 | self._head_tick = next_tick 115 | elif prev_tick is not None: # Last tick 116 | prev_tick.next_tick = None 117 | self._tail_tick = prev_tick 118 | 119 | def __str__(self): 120 | res_str = '' 121 | for tick in self: 122 | res_str += "%s\n" % str(tick) 123 | return res_str 124 | -------------------------------------------------------------------------------- /anydex/core/pricelevel_list.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List # pylint: disable=unused-import 2 | 3 | from anydex.core.price import Price # pylint: disable=unused-import 4 | from anydex.core.pricelevel import PriceLevel 5 | 6 | 7 | class PriceLevelList(object): 8 | """ 9 | Sorted doubly linked dictionary implementation. 10 | """ 11 | 12 | def __init__(self): 13 | super(PriceLevelList, self).__init__() 14 | self._price_list = [] # type: List[float] 15 | self._price_level_dictionary = {} # type: Dict[float, PriceLevel] 16 | 17 | def insert(self, price_level): # type: (PriceLevel) -> None 18 | """ 19 | :type price_level: PriceLevel 20 | """ 21 | self._price_list.append(price_level.price) 22 | self._price_list.sort() 23 | self._price_level_dictionary[price_level.price] = price_level 24 | 25 | def remove(self, price): # type: (Price) -> None 26 | """ 27 | :type price: Price 28 | """ 29 | self._price_list.remove(price) 30 | del self._price_level_dictionary[price] 31 | 32 | def succ_item(self, price): # type: (Price) -> PriceLevel 33 | """ 34 | Returns the price level where price_level.price is successor to given price 35 | 36 | :type price: Price 37 | :rtype: PriceLevel 38 | """ 39 | index = self._price_list.index(price) + 1 40 | if index >= len(self._price_list): 41 | raise IndexError 42 | succ_price = self._price_list[index] 43 | return self._price_level_dictionary[succ_price] 44 | 45 | def prev_item(self, price): # type: (Price) -> PriceLevel 46 | """ 47 | Returns the price level where price_level.price is predecessor to given price 48 | 49 | :type price: Price 50 | :rtype: PriceLevel 51 | """ 52 | index = self._price_list.index(price) - 1 53 | if index < 0: 54 | raise IndexError 55 | prev_price = self._price_list[index] 56 | return self._price_level_dictionary[prev_price] 57 | 58 | def min_key(self): # type: () -> Price 59 | """ 60 | Return the lowest price in the price level list 61 | 62 | :rtype: Price 63 | """ 64 | return self._price_list[0] 65 | 66 | def max_key(self): # type: () -> Price 67 | """ 68 | Return the highest price in the price level list 69 | 70 | :rtype: Price 71 | """ 72 | return self._price_list[-1] 73 | 74 | def items(self, reverse=False): # type: (bool) -> List[(Price, PriceLevel)] 75 | """ 76 | Returns a sorted list (on price) of price_levels 77 | 78 | :param reverse: When true returns the reversed sorted list of price, price_level tuples 79 | :type reverse: bool 80 | :rtype: List[(Price, PriceLevel)] 81 | """ 82 | items = [] 83 | for price in self._price_list: 84 | if reverse: 85 | items.insert(0, self._price_level_dictionary[price]) 86 | else: 87 | items.append(self._price_level_dictionary[price]) 88 | return items 89 | 90 | def get_ticks_list(self): # type: () -> List[Any] 91 | """ 92 | Returns a list describing all ticks. 93 | :return: list 94 | """ 95 | ticks_list = [] 96 | for price_level in self.items(): 97 | for tick in price_level: 98 | ticks_list.append(tick.tick.to_dictionary()) 99 | 100 | return ticks_list 101 | -------------------------------------------------------------------------------- /anydex/core/settings.py: -------------------------------------------------------------------------------- 1 | class MarketSettings(object): 2 | """ 3 | Object that defines various settings for the market. 4 | """ 5 | def __init__(self): 6 | self.ttl = 1 7 | self.fanout = 20 8 | self.match_window = 0 # How much time we wait before accepting a specific match 9 | self.match_send_interval = 0 # How long we should wait with sending a match message (to avoid overloading a peer) 10 | self.num_order_sync = 10 # How many orders to sync at most 11 | self.max_concurrent_trades = 0 # How many concurrent trades with risky counterparties we allow, 0 = unlimited 12 | self.transfers_per_trade = 1 # How many transfers each side should do when trading, defaults to 1 13 | self.match_process_batch_size = 20 # How many match items we process in one batch 14 | -------------------------------------------------------------------------------- /anydex/core/side.py: -------------------------------------------------------------------------------- 1 | from anydex.core.pricelevel import PriceLevel 2 | from anydex.core.pricelevel_list import PriceLevelList 3 | from anydex.core.tickentry import TickEntry 4 | 5 | 6 | class Side(object): 7 | """Class for representing a side of the order book""" 8 | 9 | def __init__(self): 10 | self._price_level_list_map = {} # Dict of (price_type, asset_type) -> PriceLevelList 11 | self._price_map = {} # Map: Price -> PriceLevel 12 | self._tick_map = {} # Map: MessageId -> TickEntry 13 | self._depth = {} # Dict of (price_type, asset_type) -> Int 14 | 15 | def __len__(self): 16 | """ 17 | Return the length of the amount of ticks contained in all the price level of this side 18 | """ 19 | return len(self._tick_map) 20 | 21 | def get_price_level(self, price): 22 | """ 23 | Return the price level corresponding to the given price 24 | 25 | :param price: The price for which the price level needs to be returned 26 | :type price: Price 27 | :return: The price level 28 | :rtype: PriceLevel 29 | """ 30 | return self._price_map[price] 31 | 32 | def get_tick(self, order_id): 33 | """ 34 | :param order_id: The order id of the tick 35 | :type order_id: OrderId 36 | :return: The tick 37 | :rtype: TickEntry 38 | """ 39 | return self._tick_map.get(order_id, None) 40 | 41 | def _create_price_level(self, price): 42 | """ 43 | :param price: The price to create the level for 44 | :type price: Price 45 | """ 46 | self._depth[(price.num_type, price.denom_type)] += 1 47 | price_level = PriceLevel(price) 48 | self._price_level_list_map[(price.num_type, price.denom_type)].insert(price_level) 49 | self._price_map[price] = price_level 50 | 51 | def _remove_price_level(self, price): 52 | """ 53 | :param price: The price to remove the level for 54 | :type price: Price 55 | """ 56 | self._depth[(price.num_type, price.denom_type)] -= 1 57 | 58 | self._price_level_list_map[(price.num_type, price.denom_type)].remove(price) 59 | del self._price_map[price] 60 | 61 | def _price_level_exists(self, price): 62 | """ 63 | :param price: The price to check for 64 | :type price: Price 65 | :return: True if the price level exists, False otherwise 66 | :rtype: bool 67 | """ 68 | return price in self._price_map 69 | 70 | def tick_exists(self, order_id): 71 | """ 72 | :param order_id: The order id to search for 73 | :type order_id: OrderId 74 | :return: True if the tick exists, False otherwise 75 | :rtype: bool 76 | """ 77 | return order_id in self._tick_map 78 | 79 | def insert_tick(self, tick): 80 | """ 81 | :param tick: The tick to insert 82 | :type tick: Tick 83 | """ 84 | if (tick.assets.second.asset_id, tick.assets.first.asset_id) not in self._price_level_list_map: 85 | self._price_level_list_map[(tick.assets.second.asset_id, tick.assets.first.asset_id)] = PriceLevelList() 86 | self._depth[(tick.assets.second.asset_id, tick.assets.first.asset_id)] = 0 87 | 88 | if not self._price_level_exists(tick.price): # First tick for that price 89 | self._create_price_level(tick.price) 90 | tick_entry = TickEntry(tick, self._price_map[tick.price]) 91 | self.get_price_level(tick.price).append_tick(tick_entry) 92 | self._tick_map[tick.order_id] = tick_entry 93 | 94 | def remove_tick(self, order_id): 95 | """ 96 | :param order_id: The order id of the tick that needs to be removed 97 | :type order_id: OrderId 98 | """ 99 | tick = self.get_tick(order_id) 100 | if tick: 101 | tick.cancel_all_pending_tasks() 102 | tick.price_level().remove_tick(tick) 103 | if len(tick.price_level()) == 0: # Last tick for that price 104 | self._remove_price_level(tick.price) 105 | del self._tick_map[order_id] 106 | 107 | def get_price_level_list(self, price_wallet_id, quantity_wallet_id): 108 | """ 109 | :return: PriceLevelList 110 | """ 111 | return self._price_level_list_map[(price_wallet_id, quantity_wallet_id)] 112 | 113 | def get_price_level_list_wallets(self): 114 | """ 115 | Returns the combinations (price wallet id, quantity wallet id) available in the side. 116 | """ 117 | return list(self._price_level_list_map) 118 | 119 | def get_max_price(self, price_wallet_id, quantity_wallet_id): 120 | """ 121 | Return the maximum price that a tick is listed for on this side of the order book 122 | :rtype: float 123 | """ 124 | key = price_wallet_id, quantity_wallet_id 125 | 126 | if self._depth.get(key, 0) > 0: 127 | return self.get_price_level_list(price_wallet_id, quantity_wallet_id).max_key() 128 | return None 129 | 130 | def get_min_price(self, price_wallet_id, quantity_wallet_id): 131 | """ 132 | Return the minimum price that a tick is listed for on this side of the order book 133 | :rtype: Price 134 | """ 135 | key = price_wallet_id, quantity_wallet_id 136 | 137 | if self._depth.get(key, 0) > 0: 138 | return self.get_price_level_list(price_wallet_id, quantity_wallet_id).min_key() 139 | return None 140 | 141 | def get_max_price_list(self, price_wallet_id, quantity_wallet_id): 142 | """ 143 | Return the price level for the maximum price 144 | :rtype: PriceLevel 145 | """ 146 | key = price_wallet_id, quantity_wallet_id 147 | 148 | if self._depth.get(key, 0) > 0: 149 | return self.get_price_level(self.get_max_price(price_wallet_id, quantity_wallet_id)) 150 | return None 151 | 152 | def get_min_price_list(self, price_wallet_id, quantity_wallet_id): 153 | """ 154 | Return the price level for the minimum price 155 | :rtype: PriceLevel 156 | """ 157 | key = price_wallet_id, quantity_wallet_id 158 | 159 | if self._depth.get(key, 0) > 0: 160 | return self.get_price_level(self.get_min_price(price_wallet_id, quantity_wallet_id)) 161 | return None 162 | 163 | def get_list_representation(self): 164 | """ 165 | Return a list describing all ticks in this side. 166 | :rtype: list 167 | """ 168 | rlist = [] 169 | for asset1, asset2 in self._price_level_list_map.keys(): 170 | rlist.append({'asset1': asset2, 'asset2': asset1, 171 | 'ticks': self._price_level_list_map[(asset1, asset2)].get_ticks_list()}) 172 | return rlist 173 | -------------------------------------------------------------------------------- /anydex/core/tickentry.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ipv8.taskmanager import TaskManager 4 | 5 | 6 | class TickEntry(TaskManager): 7 | """Class for representing a tick in the order book""" 8 | 9 | def __init__(self, tick, price_level): 10 | """ 11 | :param tick: A tick to represent in the order book 12 | :param price_level: A price level to place the tick in 13 | :type tick: Tick 14 | :type price_level: PriceLevel 15 | """ 16 | super(TickEntry, self).__init__() 17 | 18 | self._logger = logging.getLogger(self.__class__.__name__) 19 | 20 | self._tick = tick 21 | self._price_level = price_level 22 | self._prev_tick = None 23 | self._next_tick = None 24 | self.available_for_matching = 0 25 | self.update_available_for_matching() 26 | self._blocked_for_matching = set() 27 | 28 | @property 29 | def tick(self): 30 | """ 31 | :rtype: Tick 32 | """ 33 | return self._tick 34 | 35 | @property 36 | def order_id(self): 37 | """ 38 | :rtype: OrderId 39 | """ 40 | return self._tick.order_id 41 | 42 | @property 43 | def assets(self): 44 | """ 45 | :rtype: AssetPair 46 | """ 47 | return self._tick.assets 48 | 49 | @property 50 | def traded(self): 51 | """ 52 | :rtype int 53 | """ 54 | return self._tick.traded 55 | 56 | @traded.setter 57 | def traded(self, new_traded): 58 | self._tick.traded = new_traded 59 | self.update_available_for_matching() 60 | 61 | @property 62 | def price(self): 63 | """ 64 | :rtype: Price 65 | """ 66 | return self.assets.price 67 | 68 | def block_for_matching(self, order_id): 69 | """ 70 | Temporarily block an order id for matching 71 | """ 72 | if order_id in self._blocked_for_matching: 73 | self._logger.debug("Not blocking %s for matching; already blocked", order_id) 74 | return 75 | 76 | def unblock_order_id(unblock_id): 77 | self._logger.debug("Unblocking order id %s", unblock_id) 78 | self._blocked_for_matching.remove(unblock_id) 79 | 80 | self._logger.debug("Blocking %s for tick %s", order_id, self.order_id) 81 | self._blocked_for_matching.add(order_id) 82 | self.register_task("unblock_%s" % order_id, unblock_order_id, order_id, delay=10) 83 | 84 | def is_blocked_for_matching(self, order_id): 85 | """ 86 | Return whether the order_id is blocked for matching 87 | """ 88 | return order_id in self._blocked_for_matching 89 | 90 | def is_valid(self): 91 | """ 92 | Return if the tick is still valid 93 | 94 | :return: True if valid, False otherwise 95 | :rtype: bool 96 | """ 97 | return self._tick.is_valid() 98 | 99 | def price_level(self): 100 | """ 101 | :return: The price level the tick was placed in 102 | :rtype: PriceLevel 103 | """ 104 | return self._price_level 105 | 106 | @property 107 | def prev_tick(self): 108 | """ 109 | :rtype: TickEntry 110 | """ 111 | return self._prev_tick 112 | 113 | @prev_tick.setter 114 | def prev_tick(self, new_prev_tick): 115 | """ 116 | :param new_prev_tick: The new previous tick 117 | :type new_prev_tick: TickEntry 118 | """ 119 | self._prev_tick = new_prev_tick 120 | 121 | @property 122 | def next_tick(self): 123 | """ 124 | :rtype: TickEntry 125 | """ 126 | return self._next_tick 127 | 128 | @next_tick.setter 129 | def next_tick(self, new_next_tick): 130 | """ 131 | :param new_next_tick: The new previous tick 132 | :type new_next_tick: TickEntry 133 | """ 134 | self._next_tick = new_next_tick 135 | 136 | def update_available_for_matching(self): 137 | self.available_for_matching = self._tick._assets.first._amount - self._tick._traded 138 | 139 | def __str__(self): 140 | """ 141 | format: \t@\t 142 | :rtype: str 143 | """ 144 | return "%s\t@\t%g %s" % (self._tick.assets.first, self._tick.price.amount, 145 | self._tick.assets.second.asset_id) 146 | -------------------------------------------------------------------------------- /anydex/core/timeout.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class Timeout(object): 5 | """Used for having a validated instance of a timeout that we can easily check if it still valid.""" 6 | 7 | def __init__(self, timeout): 8 | """ 9 | :param timeout: Integer representation of a timeout 10 | :type timeout: int 11 | :raises ValueError: Thrown when one of the arguments are invalid 12 | """ 13 | super(Timeout, self).__init__() 14 | 15 | if not isinstance(timeout, int): 16 | raise ValueError("Timeout must be an integer") 17 | 18 | if timeout < 0: 19 | raise ValueError("Timeout must be positive or zero") 20 | 21 | self._timeout = timeout 22 | 23 | def is_timed_out(self, timestamp): 24 | """ 25 | Return if a timeout has occurred 26 | 27 | :param timestamp: A timestamp 28 | :type timestamp: Timestamp 29 | :return: True if timeout has occurred, False otherwise 30 | :rtype: bool 31 | """ 32 | return int(time.time() * 1000) - int(timestamp) >= self._timeout * 1000 33 | 34 | def __int__(self): 35 | return self._timeout 36 | 37 | def __hash__(self): 38 | return hash(self._timeout) 39 | -------------------------------------------------------------------------------- /anydex/core/timestamp.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | 4 | 5 | class Timestamp(object): 6 | """Used for having a validated instance of a timestamp that we can easily compare.""" 7 | 8 | def __init__(self, timestamp): 9 | """ 10 | :param timestamp: Integer representation of a timestamp in milliseconds 11 | :type timestamp: int 12 | :raises ValueError: Thrown when one of the arguments are invalid 13 | """ 14 | super(Timestamp, self).__init__() 15 | 16 | if not isinstance(timestamp, int): 17 | raise ValueError("Timestamp must be an integer") 18 | 19 | if timestamp < 0: 20 | raise ValueError("Timestamp can not be negative") 21 | 22 | self._timestamp = timestamp 23 | 24 | @classmethod 25 | def now(cls): 26 | """ 27 | Create a timestamp with the time set to the current time 28 | 29 | :return: A timestamp 30 | :rtype: Timestamp 31 | """ 32 | return cls(int(time.time() * 1000)) 33 | 34 | def __int__(self): 35 | return self._timestamp 36 | 37 | def __str__(self): 38 | return "%s" % datetime.datetime.fromtimestamp(self._timestamp // 1000) 39 | 40 | def __lt__(self, other): 41 | if isinstance(other, Timestamp): 42 | return self._timestamp < other._timestamp 43 | if isinstance(other, int): 44 | return self._timestamp < other 45 | return NotImplemented 46 | 47 | def __le__(self, other): 48 | if isinstance(other, Timestamp): 49 | return self._timestamp <= other._timestamp 50 | if isinstance(other, int): 51 | return self._timestamp <= other 52 | return NotImplemented 53 | 54 | def __eq__(self, other): 55 | if not isinstance(other, Timestamp): 56 | return NotImplemented 57 | elif self is other: 58 | return True 59 | return self._timestamp == other._timestamp 60 | 61 | def __ne__(self, other): 62 | return not self.__eq__(other) 63 | 64 | def __gt__(self, other): 65 | if isinstance(other, Timestamp): 66 | return self._timestamp > other._timestamp 67 | if isinstance(other, int): 68 | return self._timestamp > other 69 | return NotImplemented 70 | 71 | def __ge__(self, other): 72 | if isinstance(other, Timestamp): 73 | return self._timestamp >= other._timestamp 74 | if isinstance(other, int): 75 | return self._timestamp >= other 76 | return NotImplemented 77 | 78 | def __hash__(self): 79 | return hash(self._timestamp) 80 | -------------------------------------------------------------------------------- /anydex/core/transaction_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from anydex.core.payment import Payment 4 | from anydex.core.timestamp import Timestamp 5 | from anydex.core.transaction import Transaction 6 | from anydex.core.transaction_repository import TransactionRepository 7 | 8 | 9 | class TransactionManager(object): 10 | """Manager for retrieving and creating transactions""" 11 | 12 | def __init__(self, transaction_repository): 13 | """ 14 | :type transaction_repository: TransactionRepository 15 | """ 16 | super(TransactionManager, self).__init__() 17 | 18 | self._logger = logging.getLogger(self.__class__.__name__) 19 | self._logger.info("Transaction manager initialized") 20 | 21 | self.transaction_repository = transaction_repository 22 | 23 | def find_by_id(self, transaction_id): 24 | """ 25 | :param transaction_id: The transaction id to look for 26 | :type transaction_id: TransactionId 27 | :return: The transaction or null if it cannot be found 28 | :rtype: Transaction 29 | """ 30 | return self.transaction_repository.find_by_id(transaction_id) 31 | 32 | def find_all(self): 33 | """ 34 | :rtype: [Transaction] 35 | """ 36 | return self.transaction_repository.find_all() 37 | -------------------------------------------------------------------------------- /anydex/core/transaction_repository.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABCMeta, abstractmethod 3 | 4 | from anydex.core.message import TraderId 5 | from anydex.core.transaction import TransactionId 6 | 7 | 8 | class TransactionRepository(object): 9 | """A repository interface for transactions in the transaction manager""" 10 | __metaclass__ = ABCMeta 11 | 12 | def __init__(self): 13 | """ 14 | Do not use this class directly 15 | 16 | Make a subclass of this class with a specific implementation for a storage backend 17 | """ 18 | super(TransactionRepository, self).__init__() 19 | self._logger = logging.getLogger(self.__class__.__name__) 20 | 21 | @abstractmethod 22 | def find_all(self): 23 | return 24 | 25 | @abstractmethod 26 | def find_by_id(self, transaction_id): 27 | return 28 | 29 | @abstractmethod 30 | def add(self, transaction): 31 | return 32 | 33 | @abstractmethod 34 | def update(self, transaction): 35 | return 36 | 37 | @abstractmethod 38 | def delete_by_id(self, transaction_id): 39 | return 40 | 41 | 42 | class MemoryTransactionRepository(TransactionRepository): 43 | """A repository for transactions in the transaction manager stored in memory""" 44 | 45 | def __init__(self, mid): 46 | """ 47 | :param mid: Hex encoded version of the member id of this node 48 | :type mid: str 49 | """ 50 | super(MemoryTransactionRepository, self).__init__() 51 | 52 | self._logger.info("Memory transaction repository used") 53 | 54 | self._mid = mid 55 | self._next_id = 0 # Counter to keep track of the number of messages created by this repository 56 | 57 | self._transactions = {} 58 | 59 | def find_all(self): 60 | """ 61 | :rtype: [Transaction] 62 | """ 63 | return self._transactions.values() 64 | 65 | def find_by_id(self, transaction_id): 66 | """ 67 | :param transaction_id: The transaction id to look for 68 | :type transaction_id: TransactionId 69 | :return: The transaction or null if it cannot be found 70 | :rtype: Transaction 71 | """ 72 | return self._transactions.get(transaction_id) 73 | 74 | def add(self, transaction): 75 | """ 76 | :type transaction: Transaction 77 | """ 78 | self._logger.debug("Transaction with the id: %s was added to the transaction repository", 79 | transaction.transaction_id.as_hex()) 80 | 81 | self._transactions[transaction.transaction_id] = transaction 82 | 83 | def update(self, transaction): 84 | """ 85 | :type transaction: Transaction 86 | """ 87 | self._logger.debug("Transaction with the id: %s was updated in the transaction repository", 88 | transaction.transaction_id.as_hex()) 89 | 90 | self._transactions[transaction.transaction_id] = transaction 91 | 92 | def delete_by_id(self, transaction_id): 93 | """ 94 | :type transaction_id: TransactionId 95 | """ 96 | self._logger.debug("Transaction with the id: %s was deleted from the transaction repository", 97 | transaction_id.as_hex()) 98 | 99 | del self._transactions[transaction_id] 100 | 101 | 102 | class DatabaseTransactionRepository(TransactionRepository): 103 | """A repository for transactions in the transaction manager stored in a database""" 104 | 105 | def __init__(self, mid, persistence): 106 | """ 107 | :param mid: Hex encoded version of the member id of this node 108 | :type mid: str 109 | """ 110 | super(DatabaseTransactionRepository, self).__init__() 111 | 112 | self._logger.info("Database transaction repository used") 113 | 114 | self._mid = mid 115 | self.persistence = persistence 116 | 117 | def find_all(self): 118 | """ 119 | :rtype: [Transaction] 120 | """ 121 | return self.persistence.get_all_transactions() 122 | 123 | def find_by_id(self, transaction_id): 124 | """ 125 | :param transaction_id: The transaction id to look for 126 | :type transaction_id: TransactionId 127 | :return: The transaction or null if it cannot be found 128 | :rtype: Transaction 129 | """ 130 | return self.persistence.get_transaction(transaction_id) 131 | 132 | def add(self, transaction): 133 | """ 134 | :param transaction: The transaction to add to the database 135 | :type transaction: Transaction 136 | """ 137 | self.persistence.add_transaction(transaction) 138 | 139 | def update(self, transaction): 140 | """ 141 | :param transaction: The transaction to update 142 | :type transaction: Transaction 143 | """ 144 | self.delete_by_id(transaction.transaction_id) 145 | self.add(transaction) 146 | 147 | def delete_by_id(self, transaction_id): 148 | """ 149 | :param transaction_id: The id of the transaction to remove 150 | """ 151 | self.persistence.delete_transaction(transaction_id) 152 | -------------------------------------------------------------------------------- /anydex/core/wallet_address.py: -------------------------------------------------------------------------------- 1 | 2 | class WalletAddress(object): 3 | """Used for having a validated instance of a wallet address that we can easily check if it still valid.""" 4 | 5 | def __init__(self, wallet_address): 6 | """ 7 | :param wallet_address: String representation of a wallet address 8 | :type wallet_address: str or unicode 9 | :raises ValueError: Thrown when one of the arguments are invalid 10 | """ 11 | super(WalletAddress, self).__init__() 12 | 13 | if not isinstance(wallet_address, str): 14 | raise ValueError("Wallet address must be a string, found %s instead" % type(wallet_address)) 15 | 16 | self._wallet_address = wallet_address 17 | 18 | def __eq__(self, other): 19 | return str(other) == self._wallet_address 20 | 21 | @property 22 | def address(self): 23 | return self._wallet_address 24 | 25 | def __str__(self): 26 | return "%s" % self._wallet_address 27 | -------------------------------------------------------------------------------- /anydex/restapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains the REST API code for AnyDex 3 | """ 4 | -------------------------------------------------------------------------------- /anydex/restapi/base_market_endpoint.py: -------------------------------------------------------------------------------- 1 | from ipv8.REST.base_endpoint import BaseEndpoint 2 | 3 | from anydex.core.community import MarketCommunity 4 | 5 | 6 | class BaseMarketEndpoint(BaseEndpoint): 7 | """ 8 | This class can be used as a base class for all Market community endpoints. 9 | """ 10 | 11 | def get_market_community(self): 12 | for overlay in self.session.overlays: 13 | if isinstance(overlay, MarketCommunity): 14 | return overlay 15 | 16 | raise RuntimeError("Market community not found!") 17 | -------------------------------------------------------------------------------- /anydex/restapi/matchmakers_endpoint.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | from ipv8.REST.base_endpoint import Response 4 | 5 | from anydex.restapi.base_market_endpoint import BaseMarketEndpoint 6 | 7 | 8 | class MatchmakersEndpoint(BaseMarketEndpoint): 9 | """ 10 | This class handles requests regarding your known matchmakers in the market community. 11 | """ 12 | 13 | def setup_routes(self): 14 | self.app.add_routes([web.get('', self.get_matchmakers)]) 15 | 16 | async def get_matchmakers(self, request): 17 | """ 18 | .. http:get:: /market/matchmakers 19 | 20 | A GET request to this endpoint will return all known matchmakers. 21 | 22 | **Example request**: 23 | 24 | .. sourcecode:: none 25 | 26 | curl -X GET http://localhost:8085/market/matchmakers 27 | 28 | **Example response**: 29 | 30 | .. sourcecode:: javascript 31 | 32 | { 33 | "matchmakers": [{ 34 | "ip": "131.249.48.3", 35 | "port": 7008 36 | }] 37 | } 38 | """ 39 | matchmakers = self.get_market_community().matchmakers 40 | matchmakers_json = [{"ip": mm.address[0], "port": mm.address[1]} for mm in matchmakers] 41 | return Response({"matchmakers": matchmakers_json}) 42 | -------------------------------------------------------------------------------- /anydex/restapi/orders_endpoint.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | from ipv8.REST.base_endpoint import HTTP_BAD_REQUEST, HTTP_NOT_FOUND, Response 4 | 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.restapi.base_market_endpoint import BaseMarketEndpoint 8 | 9 | 10 | class OrdersEndpoint(BaseMarketEndpoint): 11 | """ 12 | This class handles requests regarding your orders in the market community. 13 | """ 14 | 15 | def setup_routes(self): 16 | self.app.add_routes([web.get('', self.get_orders), 17 | web.post('/{order_number}/cancel', self.cancel_order)]) 18 | 19 | async def get_orders(self, request): 20 | """ 21 | .. http:get:: /market/orders 22 | 23 | A GET request to this endpoint will return all your orders in the market community. 24 | 25 | **Example request**: 26 | 27 | .. sourcecode:: none 28 | 29 | curl -X GET http://localhost:8085/market/orders 30 | 31 | **Example response**: 32 | 33 | .. sourcecode:: javascript 34 | 35 | { 36 | "orders": [{ 37 | "trader_id": "12c406358ba05e5883a75da3f009477e4ca699a9", 38 | "timestamp": 1493906434.627721, 39 | "assets" { 40 | "first": { 41 | "amount": 3, 42 | "type": "BTC", 43 | }, 44 | "second": { 45 | "amount": 3, 46 | "type": "MB", 47 | } 48 | } 49 | "reserved_quantity": 0, 50 | "is_ask": False, 51 | "timeout": 3600, 52 | "traded": 0, 53 | "order_number": 1, 54 | "completed_timestamp": null, 55 | "cancelled": False, 56 | "status": "open" 57 | }] 58 | } 59 | """ 60 | orders = self.get_market_community().order_manager.order_repository.find_all() 61 | return Response({"orders": [order.to_dictionary() for order in orders]}) 62 | 63 | async def cancel_order(self, request): 64 | """ 65 | .. http:get:: /market/orders/(string:order_number)/cancel 66 | 67 | A POST request to this endpoint will cancel a specific order. 68 | 69 | **Example request**: 70 | 71 | .. sourcecode:: none 72 | 73 | curl -X GET http://localhost:8085/market/orders/3/cancel 74 | 75 | **Example response**: 76 | 77 | .. sourcecode:: javascript 78 | 79 | { 80 | "cancelled": True 81 | } 82 | """ 83 | market_community = self.get_market_community() 84 | order_number = request.match_info['order_number'] 85 | order_id = OrderId(TraderId(market_community.mid), OrderNumber(int(order_number))) 86 | order = market_community.order_manager.order_repository.find_by_id(order_id) 87 | 88 | if not order: 89 | return Response({"error": "order not found"}, status=HTTP_NOT_FOUND) 90 | 91 | if order.status != "open" and order.status != "unverified": 92 | return Response({"error": "only open and unverified orders can be cancelled"}, status=HTTP_BAD_REQUEST) 93 | 94 | await market_community.cancel_order(order_id) 95 | return Response({"cancelled": True}) 96 | -------------------------------------------------------------------------------- /anydex/restapi/root_endpoint.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiohttp import web 4 | 5 | from ipv8.REST.base_endpoint import BaseEndpoint, HTTP_INTERNAL_SERVER_ERROR, Response 6 | from ipv8.REST.root_endpoint import RootEndpoint as IPv8RootEndpoint 7 | 8 | from anydex.restapi.asks_bids_endpoint import AsksEndpoint, BidsEndpoint 9 | from anydex.restapi.matchmakers_endpoint import MatchmakersEndpoint 10 | from anydex.restapi.orders_endpoint import OrdersEndpoint 11 | from anydex.restapi.state_endpoint import StateEndpoint 12 | from anydex.restapi.transactions_endpoint import TransactionsEndpoint 13 | from anydex.restapi.wallets_endpoint import WalletsEndpoint 14 | 15 | 16 | @web.middleware 17 | async def error_middleware(request, handler): 18 | try: 19 | response = await handler(request) 20 | except Exception as e: 21 | return Response({"error": { 22 | "handled": False, 23 | "code": e.__class__.__name__, 24 | "message": str(e) 25 | }}, status=getattr(e, 'status', HTTP_INTERNAL_SERVER_ERROR)) 26 | return response 27 | 28 | 29 | class RootEndpoint(BaseEndpoint): 30 | """ 31 | The root endpoint of the HTTP API is the root resource in the request tree. 32 | It will dispatch requests regarding torrents, channels, settings etc to the right child endpoint. 33 | """ 34 | 35 | def __init__(self, middlewares=(), enable_ipv8_endpoints=True): 36 | super(RootEndpoint, self).__init__(middlewares) 37 | 38 | self.ipv8_endpoint = None 39 | if enable_ipv8_endpoints: 40 | self.ipv8_endpoint = IPv8RootEndpoint() 41 | self.add_endpoint('/ipv8', self.ipv8_endpoint) 42 | 43 | def setup_routes(self): 44 | endpoints = {'/asks': AsksEndpoint, 45 | '/bids': BidsEndpoint, 46 | '/transactions': TransactionsEndpoint, 47 | '/orders': OrdersEndpoint, 48 | '/matchmakers': MatchmakersEndpoint, 49 | '/state': StateEndpoint, 50 | '/wallets': WalletsEndpoint} 51 | for path, ep_cls in endpoints.items(): 52 | self.add_endpoint(path, ep_cls()) 53 | -------------------------------------------------------------------------------- /anydex/restapi/state_endpoint.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | from ipv8.REST.base_endpoint import Response 4 | 5 | from anydex.core import VERSION 6 | from anydex.restapi.base_market_endpoint import BaseMarketEndpoint 7 | 8 | 9 | class StateEndpoint(BaseMarketEndpoint): 10 | """ 11 | This class handles requests regarding the state of the dex. 12 | """ 13 | 14 | def setup_routes(self): 15 | self.app.add_routes([web.get('', self.get_state)]) 16 | 17 | async def get_state(self, request): 18 | return Response({"version": VERSION}) 19 | -------------------------------------------------------------------------------- /anydex/restapi/transactions_endpoint.py: -------------------------------------------------------------------------------- 1 | from binascii import unhexlify 2 | 3 | from aiohttp import web 4 | 5 | from ipv8.REST.base_endpoint import HTTP_NOT_FOUND, Response 6 | 7 | from anydex.core.transaction import TransactionId 8 | from anydex.restapi.base_market_endpoint import BaseMarketEndpoint 9 | 10 | 11 | class TransactionsEndpoint(BaseMarketEndpoint): 12 | """ 13 | This class handles requests regarding (past) transactions in the market community. 14 | """ 15 | 16 | def setup_routes(self): 17 | self.app.add_routes([web.get('', self.get_transactions), 18 | web.get('/{transaction_id}/payments', self.get_payments)]) 19 | 20 | async def get_transactions(self, request): 21 | """ 22 | .. http:get:: /market/transactions 23 | 24 | A GET request to this endpoint will return all performed transactions in the market community. 25 | 26 | **Example request**: 27 | 28 | .. sourcecode:: none 29 | 30 | curl -X GET http://localhost:8085/market/transactions 31 | 32 | **Example response**: 33 | 34 | .. sourcecode:: javascript 35 | 36 | { 37 | "transactions": [{ 38 | "trader_id": "12c406358ba05e5883a75da3f009477e4ca699a9", 39 | "order_number": 4, 40 | "partner_trader_id": "34c406358ba05e5883a75da3f009477e4ca699a9", 41 | "partner_order_number": 1, 42 | "assets" { 43 | "first": { 44 | "amount": 3, 45 | "type": "BTC", 46 | }, 47 | "second": { 48 | "amount": 3, 49 | "type": "MB", 50 | } 51 | }, 52 | "transferred" { 53 | "first": { 54 | "amount": 3, 55 | "type": "BTC", 56 | }, 57 | "second": { 58 | "amount": 3, 59 | "type": "MB", 60 | } 61 | } 62 | "timestamp": 1493906434.627721, 63 | "payment_complete": False 64 | ] 65 | } 66 | """ 67 | transactions = self.get_market_community().transaction_manager.find_all() 68 | return Response({"transactions": [transaction.to_block_dictionary() for transaction in transactions]}) 69 | 70 | async def get_payments(self, request): 71 | """ 72 | .. http:get:: /market/transactions/(string:transaction_id)/payments 73 | 74 | A GET request to this endpoint will return all payments tied to a specific transaction. 75 | 76 | **Example request**: 77 | 78 | .. sourcecode:: none 79 | 80 | curl -X GET http://localhost:8085/market/transactions/ 81 | 12c406358ba05e5883a75da3f009477e4ca699a9/3/payments 82 | 83 | **Example response**: 84 | 85 | .. sourcecode:: javascript 86 | 87 | { 88 | "payments": [{ 89 | "trader_id": "12c406358ba05e5883a75da3f009477e4ca699a9", 90 | "transaction_number": 3, 91 | "price": 10, 92 | "price_type": "MC", 93 | "quantity": 10, 94 | "quantity_type": "BTC", 95 | "transferred_quantity": 4, 96 | "payment_id": "abcd", 97 | "address_from": "my_mc_address", 98 | "address_to": "my_btc_address", 99 | "timestamp": 1493906434.627721, 100 | ] 101 | } 102 | """ 103 | transaction_id = TransactionId(unhexlify(request.match_info['transaction_id'])) 104 | transaction = self.get_market_community().transaction_manager.find_by_id(transaction_id) 105 | 106 | if not transaction: 107 | return Response({"error": "transaction not found"}, status=HTTP_NOT_FOUND) 108 | 109 | return Response({"payments": [payment.to_dictionary() for payment in transaction.payments]}) 110 | -------------------------------------------------------------------------------- /anydex/restapi/websocket.py: -------------------------------------------------------------------------------- 1 | from aiohttp import WSMsgType, web 2 | 3 | from anydex.restapi.base_market_endpoint import BaseMarketEndpoint 4 | 5 | 6 | class AnyDexWebsocketProtocol(BaseMarketEndpoint): 7 | 8 | def setup_routes(self): 9 | self.app.add_routes([web.get('/ws', self.handle_websockets)]) 10 | 11 | async def handle_websockets(self, request): 12 | print("WebSocket connection open") 13 | 14 | ws = web.WebSocketResponse() 15 | await ws.prepare(request) 16 | 17 | async for msg in ws: 18 | if msg.type == WSMsgType.TEXT: 19 | if msg.data == 'close': 20 | await ws.close() 21 | else: 22 | await ws.send_str(msg.data + '/answer') 23 | elif msg.type == WSMsgType.ERROR: 24 | print('WebSocket connection closed with exception %s' % ws.exception()) 25 | 26 | print('WebSocket connection closed') 27 | return ws 28 | -------------------------------------------------------------------------------- /anydex/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/anydex-core/596fd172d36068fbf519f07abfcaafafc984365e/anydex/test/__init__.py -------------------------------------------------------------------------------- /anydex/test/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains tests for the files in the core module. 3 | """ 4 | -------------------------------------------------------------------------------- /anydex/test/core/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.core.matching_engine import MatchingEngine, PriceTimeStrategy 4 | from anydex.core.orderbook import OrderBook 5 | 6 | 7 | @pytest.fixture 8 | @pytest.mark.asyncio 9 | async def order_book(): 10 | order_book = OrderBook() 11 | yield order_book 12 | await order_book.shutdown_task_manager() 13 | 14 | 15 | @pytest.fixture 16 | def strategy(order_book): 17 | return PriceTimeStrategy(order_book) 18 | 19 | 20 | @pytest.fixture 21 | def matching_engine(order_book): 22 | return MatchingEngine(PriceTimeStrategy(order_book)) 23 | -------------------------------------------------------------------------------- /anydex/test/core/test_assetamount.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | 5 | 6 | @pytest.fixture 7 | def asset_amounts(): 8 | return [AssetAmount(2, 'BTC'), AssetAmount(100, 'BTC'), AssetAmount(0, 'BTC'), AssetAmount(2, 'MC')] 9 | 10 | 11 | def test_init(): 12 | """ 13 | Test the initialization of a price 14 | """ 15 | with pytest.raises(ValueError): 16 | AssetAmount('1', 'MC') 17 | with pytest.raises(ValueError): 18 | AssetAmount(1, 2) 19 | 20 | 21 | def test_addition(asset_amounts): 22 | # Test for addition 23 | assert asset_amounts[0] + asset_amounts[1] == AssetAmount(102, 'BTC') 24 | assert asset_amounts[0] is not (asset_amounts[0] + asset_amounts[1]) 25 | assert asset_amounts[0].__add__(10) == NotImplemented 26 | assert asset_amounts[0].__add__(asset_amounts[3]) == NotImplemented 27 | 28 | 29 | def test_subtraction(asset_amounts): 30 | # Test for subtraction 31 | assert AssetAmount(98, 'BTC'), asset_amounts[1] - asset_amounts[0] 32 | assert NotImplemented == asset_amounts[0].__sub__(10) 33 | assert NotImplemented == asset_amounts[0].__sub__(asset_amounts[3]) 34 | 35 | 36 | def test_comparison(asset_amounts): 37 | # Test for comparison 38 | assert asset_amounts[0] < asset_amounts[1] 39 | assert asset_amounts[1] > asset_amounts[0] 40 | assert NotImplemented == asset_amounts[0].__le__(10) 41 | assert NotImplemented == asset_amounts[0].__lt__(10) 42 | assert NotImplemented == asset_amounts[0].__ge__(10) 43 | assert NotImplemented == asset_amounts[0].__gt__(10) 44 | assert NotImplemented == asset_amounts[0].__le__(asset_amounts[3]) 45 | assert NotImplemented == asset_amounts[0].__lt__(asset_amounts[3]) 46 | assert NotImplemented == asset_amounts[0].__ge__(asset_amounts[3]) 47 | assert NotImplemented == asset_amounts[0].__gt__(asset_amounts[3]) 48 | 49 | 50 | def test_equality(asset_amounts): 51 | # Test for equality 52 | assert asset_amounts[0] == AssetAmount(2, 'BTC') 53 | assert asset_amounts[0] != asset_amounts[1] 54 | assert not (asset_amounts[0] == 2) 55 | assert not (asset_amounts[0] == asset_amounts[3]) 56 | 57 | 58 | def test_hash(asset_amounts): 59 | # Test for hashes 60 | assert asset_amounts[0].__hash__() == AssetAmount(2, 'BTC').__hash__() 61 | assert asset_amounts[0].__hash__() != asset_amounts[1].__hash__() 62 | 63 | 64 | def test_str(asset_amounts): 65 | """ 66 | Test the string representation of a Price object 67 | """ 68 | assert str(asset_amounts[0]) == "2 BTC" 69 | -------------------------------------------------------------------------------- /anydex/test/core/test_assetpair.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | 6 | 7 | @pytest.fixture 8 | def asset_pairs(): 9 | return [AssetPair(AssetAmount(2, 'BTC'), AssetAmount(2, 'MB')), 10 | AssetPair(AssetAmount(4, 'BTC'), AssetAmount(8, 'MB')), 11 | AssetPair(AssetAmount(2, 'BTC'), AssetAmount(2, 'MB')), 12 | AssetPair(AssetAmount(10, 'DUM1'), AssetAmount(13, 'DUM2'))] 13 | 14 | 15 | def test_init(asset_pairs): 16 | """ 17 | Test initializing an AssetPair object 18 | """ 19 | with pytest.raises(ValueError): 20 | AssetPair(AssetAmount(2, 'MB'), AssetAmount(2, 'BTC')) 21 | 22 | 23 | def test_equality(asset_pairs): 24 | """ 25 | Test the equality method of an AssetPair 26 | """ 27 | assert not (asset_pairs[0] == asset_pairs[1]) 28 | assert asset_pairs[0] == asset_pairs[2] 29 | 30 | 31 | def test_to_dictionary(asset_pairs): 32 | """ 33 | Test the method to convert an AssetPair object to a dictionary 34 | """ 35 | assert { 36 | "first": { 37 | "amount": 2, 38 | "type": "BTC", 39 | }, 40 | "second": { 41 | "amount": 2, 42 | "type": "MB" 43 | } 44 | } == asset_pairs[0].to_dictionary() 45 | 46 | 47 | def test_from_dictionary(asset_pairs): 48 | """ 49 | Test the method to create an AssetPair object from a given dictionary 50 | """ 51 | assert AssetPair.from_dictionary({ 52 | "first": { 53 | "amount": 2, 54 | "type": "BTC", 55 | }, 56 | "second": { 57 | "amount": 2, 58 | "type": "MB" 59 | } 60 | }) == asset_pairs[0] 61 | 62 | 63 | def test_price(asset_pairs): 64 | """ 65 | Test creating a price from an asset pair 66 | """ 67 | assert asset_pairs[0].price.amount == 1 68 | assert asset_pairs[1].price.amount == 2 69 | 70 | 71 | def test_proportional_downscale(asset_pairs): 72 | """ 73 | Test the method to proportionally scale down an asset pair 74 | """ 75 | assert asset_pairs[1].proportional_downscale(first=1).second.amount == 2 76 | assert asset_pairs[1].proportional_downscale(second=4).first.amount == 2 77 | assert asset_pairs[3].proportional_downscale(first=10).second.amount == 13 78 | assert asset_pairs[3].proportional_downscale(second=13).first.amount == 10 79 | 80 | 81 | def test_to_str(asset_pairs): 82 | """ 83 | Test string conversion from an asset pair 84 | """ 85 | assert "2 BTC 2 MB" == str(asset_pairs[0]) 86 | -------------------------------------------------------------------------------- /anydex/test/core/test_match_queue.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pytest 4 | 5 | from anydex.core.assetamount import AssetAmount 6 | from anydex.core.assetpair import AssetPair 7 | from anydex.core.match_queue import MatchPriorityQueue 8 | from anydex.core.message import TraderId 9 | from anydex.core.order import Order, OrderId, OrderNumber 10 | from anydex.core.price import Price 11 | from anydex.core.timeout import Timeout 12 | from anydex.core.timestamp import Timestamp 13 | 14 | 15 | @pytest.fixture 16 | def ask_order(): 17 | order_id = OrderId(TraderId(b'3' * 20), OrderNumber(1)) 18 | return Order(order_id, AssetPair(AssetAmount(5, 'BTC'), AssetAmount(6, 'EUR')), 19 | Timeout(3600), Timestamp.now(), True) 20 | 21 | 22 | @pytest.fixture 23 | def bid_order(): 24 | order_id = OrderId(TraderId(b'3' * 20), OrderNumber(1)) 25 | return Order(order_id, AssetPair(AssetAmount(5, 'BTC'), AssetAmount(6, 'EUR')), 26 | Timeout(3600), Timestamp.now(), False) 27 | 28 | 29 | @pytest.fixture 30 | def queue(ask_order): 31 | return MatchPriorityQueue(ask_order) 32 | 33 | 34 | def test_priority(bid_order, queue): 35 | """ 36 | Test the priority mechanism of the queue 37 | """ 38 | order_id = OrderId(TraderId(b'1' * 20), OrderNumber(1)) 39 | other_order_id = OrderId(TraderId(b'2' * 20), OrderNumber(1)) 40 | queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) 41 | queue.insert(0, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) 42 | queue.insert(2, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) 43 | 44 | item1 = queue.delete() 45 | item2 = queue.delete() 46 | item3 = queue.delete() 47 | assert item1[0] == 0 48 | assert item2[0] == 1 49 | assert item3[0] == 2 50 | 51 | # Same retries, different prices 52 | queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) 53 | queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id, other_order_id) 54 | queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id, other_order_id) 55 | 56 | item1 = queue.delete() 57 | item2 = queue.delete() 58 | item3 = queue.delete() 59 | assert item1[1] == Price(1, 1, 'DUM1', 'DUM2') 60 | assert item2[1] == Price(1, 2, 'DUM1', 'DUM2') 61 | assert item3[1] == Price(1, 3, 'DUM1', 'DUM2') 62 | 63 | # Test with bid order 64 | queue = MatchPriorityQueue(bid_order) 65 | queue.insert(1, Price(1, 1, 'DUM1', 'DUM2'), order_id, other_order_id) 66 | queue.insert(1, Price(1, 2, 'DUM1', 'DUM2'), order_id, other_order_id) 67 | queue.insert(1, Price(1, 3, 'DUM1', 'DUM2'), order_id, other_order_id) 68 | 69 | item1 = queue.delete() 70 | item2 = queue.delete() 71 | item3 = queue.delete() 72 | assert item1[1] == Price(1, 3, 'DUM1', 'DUM2') 73 | assert item2[1] == Price(1, 2, 'DUM1', 'DUM2') 74 | assert item3[1] == Price(1, 1, 'DUM1', 'DUM2') 75 | -------------------------------------------------------------------------------- /anydex/test/core/test_matching_engine.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.tick import Ask, Bid 8 | from anydex.core.timeout import Timeout 9 | from anydex.core.timestamp import Timestamp 10 | 11 | 12 | @pytest.fixture 13 | def ask(): 14 | return Ask(OrderId(TraderId(b'2' * 20), OrderNumber(1)), 15 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'MB')), Timeout(30), Timestamp.now()) 16 | 17 | 18 | @pytest.fixture 19 | def bid(): 20 | return Bid(OrderId(TraderId(b'4' * 20), OrderNumber(2)), 21 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'MB')), Timeout(30), Timestamp.now()) 22 | 23 | 24 | def insert_ask(order_book, amount1, amount2): 25 | """ 26 | Insert an ask in the order book with a specific price and quantity 27 | """ 28 | new_ask = Ask(OrderId(TraderId(b'2' * 20), OrderNumber(len(order_book.asks) + 1)), 29 | AssetPair(AssetAmount(amount1, 'BTC'), AssetAmount(amount2, 'MB')), Timeout(30), Timestamp.now()) 30 | order_book.insert_ask(new_ask) 31 | return new_ask 32 | 33 | 34 | def insert_bid(order_book, amount1, amount2): 35 | """ 36 | Insert a bid with a specific price and quantity 37 | """ 38 | new_bid = Bid(OrderId(TraderId(b'3' * 20), OrderNumber(len(order_book.bids) + 1)), 39 | AssetPair(AssetAmount(amount1, 'BTC'), AssetAmount(amount2, 'MB')), Timeout(30), Timestamp.now()) 40 | order_book.insert_bid(new_bid) 41 | return new_bid 42 | 43 | 44 | def test_empty_match_order_empty(order_book, matching_engine, ask, bid): 45 | """ 46 | Test whether matching in an empty order book returns no matches 47 | """ 48 | order_book.insert_ask(ask) 49 | assert not matching_engine.match(order_book.get_ask(ask.order_id)) 50 | order_book.remove_ask(ask.order_id) 51 | 52 | order_book.insert_bid(bid) 53 | assert not matching_engine.match(order_book.get_bid(bid.order_id)) 54 | 55 | 56 | def test_match_order_bid(order_book, matching_engine, ask, bid): 57 | """ 58 | Test matching a bid order 59 | """ 60 | order_book.insert_ask(ask) 61 | order_book.insert_bid(bid) 62 | matching_ticks = matching_engine.match(order_book.get_bid(bid.order_id)) 63 | assert len(matching_ticks) == 1 64 | 65 | 66 | def test_match_order_ask(order_book, matching_engine, ask, bid): 67 | """ 68 | Test matching an ask order 69 | """ 70 | order_book.insert_bid(bid) 71 | order_book.insert_ask(ask) 72 | matching_ticks = matching_engine.match(order_book.get_ask(ask.order_id)) 73 | assert len(matching_ticks) == 1 74 | 75 | 76 | def test_multiple_price_levels_asks(order_book, matching_engine): 77 | """ 78 | Test matching when there are asks in multiple price levels 79 | """ 80 | insert_ask(order_book, 50, 350) 81 | insert_ask(order_book, 18, 72) 82 | insert_ask(order_book, 100, 700) 83 | 84 | my_bid = insert_bid(order_book, 200, 2000) 85 | matching_ticks = matching_engine.match(order_book.get_bid(my_bid.order_id)) 86 | 87 | assert len(matching_ticks) == 3 88 | 89 | 90 | def test_multiple_price_levels_bids(order_book, matching_engine): 91 | """ 92 | Test matching when there are bids in multiple price levels 93 | """ 94 | insert_bid(order_book, 50, 200) 95 | insert_bid(order_book, 18, 72) 96 | insert_bid(order_book, 100, 400) 97 | 98 | my_ask = insert_ask(order_book, 200, 200) 99 | matching_ticks = matching_engine.match(order_book.get_ask(my_ask.order_id)) 100 | 101 | assert len(matching_ticks) == 3 102 | 103 | 104 | def test_price_time_priority_asks(order_book, matching_engine): 105 | """ 106 | Test whether the price-time priority works correctly 107 | """ 108 | insert_ask(order_book, 20, 100) 109 | insert_ask(order_book, 25, 125) 110 | insert_ask(order_book, 10, 50) 111 | 112 | my_bid = insert_bid(order_book, 50, 250) 113 | matching_ticks = matching_engine.match(order_book.get_bid(my_bid.order_id)) 114 | 115 | assert len(matching_ticks) == 3 116 | assert matching_ticks[-1].assets.first.amount == 10 117 | 118 | 119 | def test_price_time_priority_bids(order_book, matching_engine): 120 | """ 121 | Test whether the price-time priority works correctly 122 | """ 123 | insert_bid(order_book, 20, 100) 124 | insert_bid(order_book, 25, 125) 125 | insert_bid(order_book, 10, 50) 126 | 127 | my_ask = insert_ask(order_book, 50, 250) 128 | matching_ticks = matching_engine.match(order_book.get_ask(my_ask.order_id)) 129 | 130 | assert len(matching_ticks) == 3 131 | assert matching_ticks[-1].assets.first.amount == 10 132 | 133 | 134 | def test_matching_multiple_levels(order_book, matching_engine): 135 | """ 136 | Test a matching with multiple price levels 137 | """ 138 | insert_bid(order_book, 10, 60) 139 | insert_bid(order_book, 10, 50) 140 | my_ask = insert_ask(order_book, 30, 180) 141 | matching_ticks = matching_engine.match(order_book.get_ask(my_ask.order_id)) 142 | assert len(matching_ticks) == 1 143 | -------------------------------------------------------------------------------- /anydex/test/core/test_matching_strategy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import Order, OrderId, OrderNumber 7 | from anydex.core.tick import Ask, Bid 8 | from anydex.core.timeout import Timeout 9 | from anydex.core.timestamp import Timestamp 10 | 11 | 12 | @pytest.fixture 13 | def bid(): 14 | return Bid(OrderId(TraderId(b'0' * 20), OrderNumber(5)), 15 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now()) 16 | 17 | 18 | @pytest.fixture 19 | def ask(): 20 | return Ask(OrderId(TraderId(b'0' * 20), OrderNumber(1)), 21 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now()) 22 | 23 | 24 | @pytest.fixture 25 | def ask_order(): 26 | return Order(OrderId(TraderId(b'9' * 20), OrderNumber(11)), 27 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now(), True) 28 | 29 | 30 | @pytest.fixture 31 | def bid_order(): 32 | return Order(OrderId(TraderId(b'9' * 20), OrderNumber(13)), 33 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now(), False) 34 | 35 | 36 | def test_empty_match_order(strategy, ask_order, bid_order): 37 | """ 38 | Test for match order with an empty order book 39 | """ 40 | assert not strategy.match(bid_order.order_id, bid_order.price, bid_order.available_quantity, False) 41 | assert not strategy.match(ask_order.order_id, ask_order.price, ask_order.available_quantity, True) 42 | 43 | 44 | def test_match_order_other_price(order_book, strategy, bid_order): 45 | """ 46 | Test whether two ticks with different price types are not matched 47 | """ 48 | ask = Ask(OrderId(TraderId(b'1' * 20), OrderNumber(4)), 49 | AssetPair(AssetAmount(3000, 'A'), AssetAmount(3000, 'MB')), Timeout(100), Timestamp.now()) 50 | order_book.insert_ask(ask) 51 | assert not strategy.match(bid_order.order_id, bid_order.price, bid_order.available_quantity, False) 52 | 53 | 54 | def test_match_order_other_quantity(order_book, strategy, bid_order): 55 | """ 56 | Test whether two ticks with different quantity types are not matched 57 | """ 58 | ask = Ask(OrderId(TraderId(b'1' * 20), OrderNumber(4)), 59 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'C')), Timeout(100), Timestamp.now()) 60 | 61 | order_book.insert_ask(ask) 62 | assert not strategy.match(bid_order.order_id, bid_order.price, bid_order.available_quantity, False) 63 | 64 | 65 | def test_match_order_ask(order_book, strategy, ask_order, bid): 66 | """ 67 | Test matching an ask order 68 | """ 69 | order_book.insert_bid(bid) 70 | matching_ticks = strategy.match(ask_order.order_id, ask_order.price, ask_order.available_quantity, True) 71 | assert matching_ticks 72 | assert order_book.get_tick(bid.order_id) == matching_ticks[0] 73 | 74 | 75 | def test_match_order_bid(order_book, strategy, bid_order, ask): 76 | """ 77 | Test matching a bid order 78 | """ 79 | order_book.insert_ask(ask) 80 | matching_ticks = strategy.match(bid_order.order_id, bid_order.price, bid_order.available_quantity, False) 81 | assert matching_ticks 82 | assert order_book.get_tick(ask.order_id) == matching_ticks[0] 83 | 84 | 85 | def test_match_order_divided(order_book, strategy, ask): 86 | """ 87 | Test for match order divided over two ticks 88 | """ 89 | order_book.insert_ask(ask) 90 | 91 | ask2 = Ask(OrderId(TraderId(b'1' * 20), OrderNumber(2)), 92 | AssetPair(AssetAmount(3000, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now()) 93 | bid_order2 = Order(OrderId(TraderId(b'9' * 20), OrderNumber(14)), 94 | AssetPair(AssetAmount(6000, 'BTC'), AssetAmount(60, 'MB')), Timeout(100), Timestamp.now(), False) 95 | order_book.insert_ask(ask2) 96 | matching_ticks = strategy.match(bid_order2.order_id, bid_order2.price, bid_order2.available_quantity, False) 97 | assert len(matching_ticks) == 2 98 | 99 | 100 | def test_match_order_partial_ask(order_book, strategy, bid_order, ask): 101 | """ 102 | Test partial matching of a bid order with the matching engine 103 | """ 104 | ask.traded = 1000 105 | order_book.insert_ask(ask) 106 | matching_ticks = strategy.match(bid_order.order_id, bid_order.price, bid_order.available_quantity, False) 107 | assert len(matching_ticks) == 1 108 | 109 | 110 | def test_match_order_partial_bid(order_book, strategy, ask_order, bid): 111 | """ 112 | Test partial matching of an ask order with the matching engine 113 | """ 114 | bid.traded = 1000 115 | order_book.insert_bid(bid) 116 | matching_ticks = strategy.match(ask_order.order_id, ask_order.price, ask_order.available_quantity, True) 117 | assert len(matching_ticks) == 1 118 | 119 | 120 | def test_bid_blocked_for_matching(order_book, strategy, ask_order, bid): 121 | """ 122 | Test whether a bid tick is not matched when blocked for matching 123 | """ 124 | order_book.insert_bid(bid) 125 | order_book.get_tick(bid.order_id).block_for_matching(ask_order.order_id) 126 | matching_ticks = strategy.match(ask_order.order_id, ask_order.price, ask_order.available_quantity, True) 127 | assert not matching_ticks 128 | 129 | 130 | def test_ask_blocked_for_matching(order_book, strategy, bid_order, ask): 131 | """ 132 | Test whether an ask tick is not matched when blocked for matching 133 | """ 134 | order_book.insert_ask(ask) 135 | order_book.get_tick(ask.order_id).block_for_matching(bid_order.order_id) 136 | matching_ticks = strategy.match(bid_order.order_id, bid_order.price, bid_order.available_quantity, False) 137 | assert not matching_ticks 138 | -------------------------------------------------------------------------------- /anydex/test/core/test_message.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.message import TraderId 4 | 5 | 6 | class TraderIdTestSuite(unittest.TestCase): 7 | """Trader ID test cases.""" 8 | 9 | def setUp(self): 10 | # Object creation 11 | self.trader_id = TraderId(b'0' * 20) 12 | self.trader_id2 = TraderId(b'0' * 20) 13 | self.trader_id3 = TraderId(b'1' * 20) 14 | 15 | def test_init(self): 16 | # Test for init validation 17 | with self.assertRaises((TypeError, ValueError)): 18 | TraderId(1.0) 19 | 20 | def test_conversion(self): 21 | # Test for conversions 22 | self.assertEqual(b'0' * 20, bytes(self.trader_id)) 23 | 24 | def test_equality(self): 25 | # Test for equality 26 | self.assertTrue(self.trader_id == self.trader_id2) 27 | self.assertTrue(self.trader_id != self.trader_id3) 28 | 29 | def test_hash(self): 30 | # Test for hashes 31 | self.assertEqual(self.trader_id.__hash__(), self.trader_id2.__hash__()) 32 | self.assertNotEqual(self.trader_id.__hash__(), self.trader_id3.__hash__()) 33 | -------------------------------------------------------------------------------- /anydex/test/core/test_order_repository.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import Order, OrderId, OrderNumber 7 | from anydex.core.order_repository import MemoryOrderRepository 8 | from anydex.core.timeout import Timeout 9 | from anydex.core.timestamp import Timestamp 10 | 11 | 12 | class MemoryOrderRepositoryTestSuite(unittest.TestCase): 13 | """Memory order repository test cases.""" 14 | 15 | def setUp(self): 16 | # Object creation 17 | self.memory_order_repository = MemoryOrderRepository(b'0' * 20) 18 | self.order_id = OrderId(TraderId(b'0' * 20), OrderNumber(1)) 19 | self.order = Order(self.order_id, AssetPair(AssetAmount(100, 'BTC'), AssetAmount(30, 'MC')), 20 | Timeout(0), Timestamp(10), False) 21 | self.order2 = Order(self.order_id, AssetPair(AssetAmount(1000, 'BTC'), AssetAmount(30, 'MC')), 22 | Timeout(0), Timestamp(10), False) 23 | 24 | def test_add(self): 25 | # Test for add 26 | self.assertEqual([], list(self.memory_order_repository.find_all())) 27 | self.memory_order_repository.add(self.order) 28 | self.assertEqual([self.order], list(self.memory_order_repository.find_all())) 29 | 30 | def test_delete_by_id(self): 31 | # Test for delete by id 32 | self.memory_order_repository.add(self.order) 33 | self.assertEqual([self.order], list(self.memory_order_repository.find_all())) 34 | self.memory_order_repository.delete_by_id(self.order_id) 35 | self.assertEqual([], list(self.memory_order_repository.find_all())) 36 | 37 | def test_find_by_id(self): 38 | # Test for find by id 39 | self.assertEqual(None, self.memory_order_repository.find_by_id(self.order_id)) 40 | self.memory_order_repository.add(self.order) 41 | self.assertEqual(self.order, self.memory_order_repository.find_by_id(self.order_id)) 42 | 43 | def test_find_all(self): 44 | # Test for find all 45 | self.assertEqual([], list(self.memory_order_repository.find_all())) 46 | self.memory_order_repository.add(self.order) 47 | self.assertEqual([self.order], list(self.memory_order_repository.find_all())) 48 | 49 | def test_next_identity(self): 50 | # Test for next identity 51 | self.assertEqual(OrderId(TraderId(b'0' * 20), OrderNumber(1)), 52 | self.memory_order_repository.next_identity()) 53 | 54 | def test_update(self): 55 | # Test for update 56 | self.memory_order_repository.add(self.order) 57 | self.memory_order_repository.update(self.order2) 58 | self.assertNotEqual(self.order, self.memory_order_repository.find_by_id(self.order_id)) 59 | self.assertEqual(self.order2, self.memory_order_repository.find_by_id(self.order_id)) 60 | -------------------------------------------------------------------------------- /anydex/test/core/test_orderbook.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.orderbook import OrderBook 8 | from anydex.core.price import Price 9 | from anydex.core.tick import Ask, Bid 10 | from anydex.core.timeout import Timeout 11 | from anydex.core.timestamp import Timestamp 12 | 13 | 14 | @pytest.fixture 15 | def ask(): 16 | return Ask(OrderId(TraderId(b'0' * 20), OrderNumber(1)), 17 | AssetPair(AssetAmount(100, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now()) 18 | 19 | 20 | @pytest.fixture 21 | def ask2(): 22 | return Ask(OrderId(TraderId(b'1' * 20), OrderNumber(1)), 23 | AssetPair(AssetAmount(400, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now()) 24 | 25 | 26 | @pytest.fixture 27 | def bid(): 28 | return Bid(OrderId(TraderId(b'2' * 20), OrderNumber(1)), 29 | AssetPair(AssetAmount(200, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now()) 30 | 31 | 32 | @pytest.fixture 33 | def bid2(): 34 | return Bid(OrderId(TraderId(b'3' * 20), OrderNumber(1)), 35 | AssetPair(AssetAmount(300, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now()) 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def order_book(): 40 | order_book = OrderBook() 41 | yield order_book 42 | await order_book.shutdown_task_manager() 43 | 44 | 45 | def test_timeouts(ask, bid, order_book): 46 | """ 47 | Test the timeout functions of asks/bids 48 | """ 49 | order_book.insert_ask(ask) 50 | assert order_book.timeout_ask(ask.order_id) == ask 51 | 52 | order_book.insert_bid(bid) 53 | assert order_book.timeout_bid(bid.order_id) == bid 54 | 55 | order_book.on_invalid_tick_insert() 56 | 57 | 58 | def test_ask_insertion(ask2, order_book): 59 | # Test for ask insertion 60 | order_book.insert_ask(ask2) 61 | assert order_book.tick_exists(ask2.order_id) 62 | assert order_book.ask_exists(ask2.order_id) 63 | assert not order_book.bid_exists(ask2.order_id) 64 | assert order_book.get_ask(ask2.order_id).tick == ask2 65 | 66 | 67 | def test_get_tick(ask, bid, order_book): 68 | """ 69 | Test the retrieval of a tick from the order book 70 | """ 71 | order_book.insert_ask(ask) 72 | order_book.insert_bid(bid) 73 | assert order_book.get_tick(ask.order_id) 74 | assert order_book.get_tick(bid.order_id) 75 | 76 | 77 | def test_ask_removal(ask2, order_book): 78 | # Test for ask removal 79 | order_book.insert_ask(ask2) 80 | assert order_book.tick_exists(ask2.order_id) 81 | order_book.remove_ask(ask2.order_id) 82 | assert not order_book.tick_exists(ask2.order_id) 83 | 84 | 85 | def test_bid_insertion(bid2, order_book): 86 | # Test for bid insertion 87 | order_book.insert_bid(bid2) 88 | assert order_book.tick_exists(bid2.order_id) 89 | assert order_book.bid_exists(bid2.order_id) 90 | assert not order_book.ask_exists(bid2.order_id) 91 | assert order_book.get_bid(bid2.order_id).tick == bid2 92 | 93 | 94 | def test_bid_removal(bid2, order_book): 95 | # Test for bid removal 96 | order_book.insert_bid(bid2) 97 | assert order_book.tick_exists(bid2.order_id) 98 | order_book.remove_bid(bid2.order_id) 99 | assert not order_book.tick_exists(bid2.order_id) 100 | 101 | 102 | def test_properties(ask2, bid2, order_book): 103 | # Test for properties 104 | order_book.insert_ask(ask2) 105 | order_book.insert_bid(bid2) 106 | assert order_book.get_bid_ask_spread('MB', 'BTC') == Price(-25, 1000, 'MB', 'BTC') 107 | 108 | 109 | def test_ask_price_level(ask, order_book): 110 | order_book.insert_ask(ask) 111 | price_level = order_book.get_ask_price_level('MB', 'BTC') 112 | assert price_level.depth == 100 113 | 114 | 115 | def test_bid_price_level(bid2, order_book): 116 | # Test for tick price 117 | order_book.insert_bid(bid2) 118 | price_level = order_book.get_bid_price_level('MB', 'BTC') 119 | assert price_level.depth == 300 120 | 121 | 122 | def test_ask_side_depth(ask, ask2, order_book): 123 | # Test for ask side depth 124 | order_book.insert_ask(ask) 125 | order_book.insert_ask(ask2) 126 | assert order_book.ask_side_depth(Price(3, 10, 'MB', 'BTC')) == 100 127 | assert order_book.get_ask_side_depth_profile('MB', 'BTC') == \ 128 | [(Price(75, 1000, 'MB', 'BTC'), 400), (Price(3, 10, 'MB', 'BTC'), 100)] 129 | 130 | 131 | def test_bid_side_depth(bid, bid2, order_book): 132 | # Test for bid side depth 133 | order_book.insert_bid(bid) 134 | order_book.insert_bid(bid2) 135 | assert order_book.bid_side_depth(Price(1, 10, 'MB', 'BTC')) == 300 136 | assert order_book.get_bid_side_depth_profile('MB', 'BTC') == \ 137 | [(Price(1, 10, 'MB', 'BTC'), 300), (Price(15, 100, 'MB', 'BTC'), 200)] 138 | 139 | 140 | def test_remove_tick(ask2, bid2, order_book): 141 | # Test for tick removal 142 | order_book.insert_ask(ask2) 143 | order_book.insert_bid(bid2) 144 | order_book.remove_tick(ask2.order_id) 145 | assert not order_book.tick_exists(ask2.order_id) 146 | order_book.remove_tick(bid2.order_id) 147 | assert not order_book.tick_exists(bid2.order_id) 148 | 149 | 150 | def test_get_order_ids(ask, bid, order_book): 151 | """ 152 | Test the get order IDs function in order book 153 | """ 154 | assert not order_book.get_order_ids() 155 | order_book.insert_ask(ask) 156 | order_book.insert_bid(bid) 157 | assert len(order_book.get_order_ids()) == 2 158 | 159 | 160 | def test_update_ticks(ask, bid, order_book): 161 | """ 162 | Test updating ticks in an order book 163 | """ 164 | order_book.insert_ask(ask) 165 | order_book.insert_bid(bid) 166 | 167 | ask_dict = { 168 | "trader_id": ask.order_id.trader_id.as_hex(), 169 | "order_number": int(ask.order_id.order_number), 170 | "assets": ask.assets.to_dictionary(), 171 | "traded": 100, 172 | "timeout": 3600, 173 | "timestamp": int(Timestamp.now()) 174 | } 175 | bid_dict = { 176 | "trader_id": bid.order_id.trader_id.as_hex(), 177 | "order_number": int(bid.order_id.order_number), 178 | "assets": bid.assets.to_dictionary(), 179 | "traded": 100, 180 | "timeout": 3600, 181 | "timestamp": int(Timestamp.now()) 182 | } 183 | 184 | ask_dict["traded"] = 50 185 | bid_dict["traded"] = 50 186 | order_book.completed_orders = [] 187 | order_book.update_ticks(ask_dict, bid_dict, 100) 188 | assert len(order_book.asks) == 1 189 | assert len(order_book.bids) == 1 190 | 191 | 192 | def test_str(ask, bid, order_book): 193 | # Test for order book string representation 194 | order_book.insert_ask(ask) 195 | order_book.insert_bid(bid) 196 | 197 | assert str(order_book) == \ 198 | '------ Bids -------\n'\ 199 | '200 BTC\t@\t0.15 MB\n\n'\ 200 | '------ Asks -------\n'\ 201 | '100 BTC\t@\t0.3 MB\n\n' 202 | -------------------------------------------------------------------------------- /anydex/test/core/test_payment.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.message import TraderId 5 | from anydex.core.payment import Payment 6 | from anydex.core.payment_id import PaymentId 7 | from anydex.core.timestamp import Timestamp 8 | from anydex.core.transaction import TransactionId 9 | from anydex.core.wallet_address import WalletAddress 10 | 11 | 12 | class PaymentTestSuite(unittest.TestCase): 13 | """Payment test cases.""" 14 | 15 | def setUp(self): 16 | # Object creation 17 | self.payment = Payment(TraderId(b'0' * 20), 18 | TransactionId(b'a' * 32), 19 | AssetAmount(3, 'BTC'), 20 | WalletAddress('a'), WalletAddress('b'), 21 | PaymentId('aaa'), Timestamp(4000)) 22 | 23 | def test_to_dictionary(self): 24 | """ 25 | Test the dictionary representation of a payment 26 | """ 27 | self.assertDictEqual(self.payment.to_dictionary(), { 28 | "trader_id": "30" * 20, 29 | "transaction_id": "61" * 32, 30 | "transferred": { 31 | "amount": 3, 32 | "type": "BTC" 33 | }, 34 | "payment_id": 'aaa', 35 | "address_from": 'a', 36 | "address_to": 'b', 37 | "timestamp": 4000, 38 | }) 39 | -------------------------------------------------------------------------------- /anydex/test/core/test_payment_id.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.payment_id import PaymentId 4 | 5 | 6 | class PaymentIdTestSuite(unittest.TestCase): 7 | """Payment Id test cases.""" 8 | 9 | def setUp(self): 10 | self.payment_id1 = PaymentId("3") 11 | self.payment_id2 = PaymentId("4") 12 | 13 | def test_init(self): 14 | """ 15 | Test the initialization of a quantity 16 | """ 17 | with self.assertRaises(ValueError): 18 | PaymentId(1) 19 | 20 | def test_str(self): 21 | """ 22 | Test the string representation of a payment id 23 | """ 24 | self.assertEqual(str(self.payment_id1), "3") 25 | 26 | def test_equality(self): 27 | """ 28 | Test equality between payment ids 29 | """ 30 | self.assertEqual(self.payment_id1, PaymentId("3")) 31 | self.assertNotEqual(self.payment_id1, self.payment_id2) 32 | self.assertEqual(NotImplemented, self.payment_id1.__eq__("3")) 33 | 34 | def test_hash(self): 35 | """ 36 | Test the hash creation of a payment id 37 | """ 38 | self.assertEqual(self.payment_id1.__hash__(), "3".__hash__()) 39 | self.assertNotEqual(self.payment_id1.__hash__(), self.payment_id2.__hash__()) 40 | -------------------------------------------------------------------------------- /anydex/test/core/test_portfolio.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.order_manager import OrderManager 8 | from anydex.core.order_repository import MemoryOrderRepository 9 | from anydex.core.timeout import Timeout 10 | 11 | 12 | class PortfolioTestSuite(unittest.TestCase): 13 | """OrderManager test cases.""" 14 | 15 | def setUp(self): 16 | # Object creation 17 | self.order_manager = OrderManager(MemoryOrderRepository(b'0' * 20)) 18 | 19 | def test_create_ask_order(self): 20 | # Test for create ask order 21 | ask_order = self.order_manager.create_ask_order( 22 | AssetPair(AssetAmount(100, 'BTC'), AssetAmount(10, 'MC')), Timeout(0)) 23 | self.assertTrue(ask_order.is_ask()) 24 | self.assertEqual(OrderId(TraderId(b'0' * 20), OrderNumber(1)), ask_order.order_id) 25 | self.assertEqual(AssetPair(AssetAmount(100, 'BTC'), AssetAmount(10, 'MC')), ask_order.assets) 26 | self.assertEqual(100, ask_order.total_quantity) 27 | self.assertEqual(0, int(ask_order.timeout)) 28 | 29 | def test_create_bid_order(self): 30 | # Test for create bid order 31 | bid_order = self.order_manager.create_bid_order( 32 | AssetPair(AssetAmount(100, 'BTC'), AssetAmount(10, 'MC')), Timeout(0)) 33 | self.assertFalse(bid_order.is_ask()) 34 | self.assertEqual(OrderId(TraderId(b'0' * 20), OrderNumber(1)), bid_order.order_id) 35 | self.assertEqual(AssetPair(AssetAmount(100, 'BTC'), AssetAmount(10, 'MC')), bid_order.assets) 36 | self.assertEqual(100, bid_order.total_quantity) 37 | self.assertEqual(0, int(bid_order.timeout)) 38 | 39 | def test_cancel_order(self): 40 | # test for cancel order 41 | order = self.order_manager.create_ask_order( 42 | AssetPair(AssetAmount(100, 'BTC'), AssetAmount(10, 'MC')), Timeout(0)) 43 | self.order_manager.cancel_order(order.order_id) 44 | -------------------------------------------------------------------------------- /anydex/test/core/test_price.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.price import Price 4 | 5 | 6 | class TestPrice(unittest.TestCase): 7 | """ 8 | This class contains tests for the Price object. 9 | """ 10 | 11 | def setUp(self): 12 | self.price1 = Price(4, 2, 'MB', 'BTC') 13 | self.price2 = Price(3, 1, 'MB', 'BTC') 14 | self.price3 = Price(8, 4, 'MB', 'BTC') 15 | 16 | def test_str(self): 17 | """ 18 | Test the str method of a Price object 19 | """ 20 | self.assertEqual(str(self.price1), "2 MB/BTC") 21 | 22 | def test_equality(self): 23 | """ 24 | Test the equality method of a Price object 25 | """ 26 | self.assertEqual(self.price1, self.price3) 27 | self.assertNotEqual(self.price1, self.price2) 28 | self.assertNotEqual(self.price1, 2) 29 | self.assertFalse(self.price1 == 2) 30 | 31 | def test_cmp(self): 32 | """ 33 | Test comparison of a Price object 34 | """ 35 | self.assertTrue(self.price1 < self.price2) 36 | self.assertFalse(self.price1 > self.price2) 37 | -------------------------------------------------------------------------------- /anydex/test/core/test_pricelevel.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.price import Price 8 | from anydex.core.pricelevel import PriceLevel 9 | from anydex.core.tick import Tick 10 | from anydex.core.tickentry import TickEntry 11 | from anydex.core.timeout import Timeout 12 | from anydex.core.timestamp import Timestamp 13 | 14 | 15 | class PriceLevelTestSuite(unittest.TestCase): 16 | """PriceLevel test cases.""" 17 | 18 | def setUp(self): 19 | # Object creation 20 | tick = Tick(OrderId(TraderId(b'0' * 20), OrderNumber(1)), AssetPair(AssetAmount(60, 'BTC'), 21 | AssetAmount(30, 'MC')), 22 | Timeout(100), Timestamp.now(), True) 23 | tick2 = Tick(OrderId(TraderId(b'0' * 20), OrderNumber(2)), AssetPair(AssetAmount(30, 'BTC'), 24 | AssetAmount(30, 'MC')), 25 | Timeout(100), Timestamp.now(), True) 26 | 27 | self.price_level = PriceLevel(Price(50, 5, 'MC', 'BTC')) 28 | self.tick_entry1 = TickEntry(tick, self.price_level) 29 | self.tick_entry2 = TickEntry(tick, self.price_level) 30 | self.tick_entry3 = TickEntry(tick, self.price_level) 31 | self.tick_entry4 = TickEntry(tick, self.price_level) 32 | self.tick_entry5 = TickEntry(tick2, self.price_level) 33 | 34 | def test_appending_length(self): 35 | # Test for tick appending and length 36 | self.assertEqual(0, self.price_level.length) 37 | self.assertEqual(0, len(self.price_level)) 38 | 39 | self.price_level.append_tick(self.tick_entry1) 40 | self.price_level.append_tick(self.tick_entry2) 41 | self.price_level.append_tick(self.tick_entry3) 42 | self.price_level.append_tick(self.tick_entry4) 43 | 44 | self.assertEqual(4, self.price_level.length) 45 | self.assertEqual(4, len(self.price_level)) 46 | 47 | def test_tick_removal(self): 48 | # Test for tick removal 49 | self.price_level.append_tick(self.tick_entry1) 50 | self.price_level.append_tick(self.tick_entry2) 51 | self.price_level.append_tick(self.tick_entry3) 52 | self.price_level.append_tick(self.tick_entry4) 53 | 54 | self.price_level.remove_tick(self.tick_entry2) 55 | self.price_level.remove_tick(self.tick_entry1) 56 | self.price_level.remove_tick(self.tick_entry4) 57 | self.price_level.remove_tick(self.tick_entry3) 58 | self.assertEqual(0, self.price_level.length) 59 | 60 | def test_str(self): 61 | # Test for price level string representation 62 | self.price_level.append_tick(self.tick_entry1) 63 | self.price_level.append_tick(self.tick_entry2) 64 | self.assertEqual('60 BTC\t@\t0.5 MC\n' 65 | '60 BTC\t@\t0.5 MC\n', str(self.price_level)) 66 | -------------------------------------------------------------------------------- /anydex/test/core/test_pricelevel_list.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.price import Price 4 | from anydex.core.pricelevel import PriceLevel 5 | from anydex.core.pricelevel_list import PriceLevelList 6 | 7 | 8 | class PriceLevelListTestSuite(unittest.TestCase): 9 | """PriceLevelList test cases.""" 10 | 11 | def setUp(self): 12 | # Object creation 13 | self.price_level_list = PriceLevelList() 14 | self.price_level_list2 = PriceLevelList() 15 | self.price = Price(1, 1, 'BTC', 'MB') 16 | self.price2 = Price(2, 1, 'BTC', 'MB') 17 | self.price3 = Price(3, 1, 'BTC', 'MB') 18 | self.price4 = Price(4, 1, 'BTC', 'MB') 19 | self.price_level = PriceLevel(self.price) 20 | self.price_level2 = PriceLevel(self.price2) 21 | self.price_level3 = PriceLevel(self.price3) 22 | self.price_level4 = PriceLevel(self.price4) 23 | 24 | # Fill price level list 25 | self.price_level_list.insert(self.price_level) 26 | self.price_level_list.insert(self.price_level2) 27 | self.price_level_list.insert(self.price_level3) 28 | self.price_level_list.insert(self.price_level4) 29 | 30 | def test_min_key(self): 31 | # Test for min key 32 | self.assertEqual(self.price, self.price_level_list.min_key()) 33 | 34 | def test_min_key_empty(self): 35 | # Test for min key when empty 36 | with self.assertRaises(IndexError): 37 | self.price_level_list2.min_key() 38 | 39 | def test_max_key(self): 40 | # Test for max key 41 | self.assertEqual(self.price4, self.price_level_list.max_key()) 42 | 43 | def test_max_key_empty(self): 44 | # Test for max key when empty 45 | with self.assertRaises(IndexError): 46 | self.price_level_list2.max_key() 47 | 48 | def test_succ_item(self): 49 | # Test for succ item 50 | self.assertEqual(self.price_level2, self.price_level_list.succ_item(self.price)) 51 | self.assertEqual(self.price_level4, self.price_level_list.succ_item(self.price3)) 52 | 53 | def test_succ_item_tail(self): 54 | # Test for succ item when at tail 55 | with self.assertRaises(IndexError): 56 | self.price_level_list.succ_item(self.price4) 57 | 58 | def test_prev_item(self): 59 | # Test for prev item 60 | self.assertEqual(self.price_level3, self.price_level_list.prev_item(self.price4)) 61 | self.assertEqual(self.price_level2, self.price_level_list.prev_item(self.price3)) 62 | 63 | def test_prev_item_head(self): 64 | # Test for prev item when at head 65 | with self.assertRaises(IndexError): 66 | self.price_level_list.prev_item(self.price) 67 | 68 | def test_remove(self): 69 | # Test for remove 70 | self.price_level_list.remove(self.price4) 71 | self.assertEqual(self.price3, self.price_level_list.max_key()) 72 | 73 | def test_remove_empty(self): 74 | # Test for remove when element not exists 75 | self.price_level_list.remove(self.price4) 76 | with self.assertRaises(ValueError): 77 | self.price_level_list.remove(self.price4) 78 | 79 | def test_items(self): 80 | # Test for items 81 | self.assertEqual( 82 | [self.price_level, self.price_level2, self.price_level3, self.price_level4], self.price_level_list.items()) 83 | self.price_level_list.remove(self.price2) 84 | self.assertEqual( 85 | [self.price_level, self.price_level3, self.price_level4], self.price_level_list.items()) 86 | 87 | def test_items_empty(self): 88 | # Test for items when empty 89 | self.assertEqual([], self.price_level_list2.items()) 90 | 91 | def test_items_reverse(self): 92 | # Test for items with reverse attribute 93 | self.assertEqual( 94 | [self.price_level4, self.price_level3, self.price_level2, self.price_level], 95 | self.price_level_list.items(reverse=True)) 96 | self.price_level_list.remove(self.price2) 97 | self.assertEqual( 98 | [self.price_level4, self.price_level3, self.price_level], self.price_level_list.items(reverse=True)) 99 | 100 | def test_items_reverse_empty(self): 101 | # Test for items when empty with reverse attribute 102 | self.assertEqual([], self.price_level_list2.items(reverse=True)) 103 | -------------------------------------------------------------------------------- /anydex/test/core/test_side.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.price import Price 8 | from anydex.core.side import Side 9 | from anydex.core.tick import Tick 10 | from anydex.core.timeout import Timeout 11 | from anydex.core.timestamp import Timestamp 12 | 13 | 14 | class SideTestSuite(unittest.TestCase): 15 | """Side test cases.""" 16 | 17 | def setUp(self): 18 | # Object creation 19 | 20 | self.tick = Tick(OrderId(TraderId(b'0' * 20), OrderNumber(1)), 21 | AssetPair(AssetAmount(60, 'BTC'), AssetAmount(30, 'MB')), 22 | Timeout(100), Timestamp.now(), True) 23 | self.tick2 = Tick(OrderId(TraderId(b'1' * 20), OrderNumber(2)), 24 | AssetPair(AssetAmount(120, 'BTC'), AssetAmount(30, 'MB')), 25 | Timeout(100), Timestamp.now(), True) 26 | self.side = Side() 27 | 28 | def test_max_price(self): 29 | # Test max price (list) 30 | self.assertEqual(None, self.side.get_max_price('MB', 'BTC')) 31 | self.assertEqual(None, self.side.get_max_price_list('MB', 'BTC')) 32 | 33 | self.side.insert_tick(self.tick) 34 | self.side.insert_tick(self.tick2) 35 | 36 | self.assertEqual(Price(1, 2, 'MB', 'BTC'), self.side.get_max_price('MB', 'BTC')) 37 | 38 | def test_min_price(self): 39 | # Test min price (list) 40 | self.assertEqual(None, self.side.get_min_price_list('MB', 'BTC')) 41 | self.assertEqual(None, self.side.get_min_price('MB', 'BTC')) 42 | 43 | self.side.insert_tick(self.tick) 44 | self.side.insert_tick(self.tick2) 45 | 46 | self.assertEqual(Price(1, 4, 'MB', 'BTC'), self.side.get_min_price('MB', 'BTC')) 47 | 48 | def test_insert_tick(self): 49 | # Test insert tick 50 | self.assertEqual(0, len(self.side)) 51 | self.assertFalse(self.side.tick_exists(OrderId(TraderId(b'0' * 20), OrderNumber(1)))) 52 | 53 | self.side.insert_tick(self.tick) 54 | self.side.insert_tick(self.tick2) 55 | 56 | self.assertEqual(2, len(self.side)) 57 | self.assertTrue(self.side.tick_exists(OrderId(TraderId(b'0' * 20), OrderNumber(1)))) 58 | 59 | def test_remove_tick(self): 60 | # Test remove tick 61 | self.side.insert_tick(self.tick) 62 | self.side.insert_tick(self.tick2) 63 | 64 | self.side.remove_tick(OrderId(TraderId(b'0' * 20), OrderNumber(1))) 65 | self.assertEqual(1, len(self.side)) 66 | self.side.remove_tick(OrderId(TraderId(b'1' * 20), OrderNumber(2))) 67 | self.assertEqual(0, len(self.side)) 68 | 69 | def test_get_price_level_list_wallets(self): 70 | """ 71 | Test the price level lists of wallets of a side 72 | """ 73 | self.assertFalse(self.side.get_price_level_list_wallets()) 74 | self.side.insert_tick(self.tick) 75 | self.assertTrue(self.side.get_price_level_list_wallets()) 76 | 77 | def test_get_list_representation(self): 78 | """ 79 | Testing the list representation of a side 80 | """ 81 | self.assertFalse(self.side.get_list_representation()) 82 | self.side.insert_tick(self.tick) 83 | 84 | list_rep = self.side.get_list_representation() 85 | self.assertTrue(list_rep) 86 | -------------------------------------------------------------------------------- /anydex/test/core/test_tick.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from binascii import hexlify 3 | 4 | from anydex.core.assetamount import AssetAmount 5 | from anydex.core.assetpair import AssetPair 6 | from anydex.core.message import TraderId 7 | from anydex.core.order import Order, OrderId, OrderNumber 8 | from anydex.core.tick import Ask, Bid, Tick 9 | from anydex.core.timeout import Timeout 10 | from anydex.core.timestamp import Timestamp 11 | 12 | 13 | class TickTestSuite(unittest.TestCase): 14 | """ 15 | This class contains tests for the Tick object. 16 | """ 17 | 18 | def setUp(self): 19 | # Object creation 20 | self.timestamp_now = Timestamp.now() 21 | self.tick = Tick(OrderId(TraderId(b'0' * 20), OrderNumber(1)), 22 | AssetPair(AssetAmount(30, 'BTC'), AssetAmount(30, 'MB')), Timeout(30), Timestamp(0), True) 23 | self.tick2 = Tick(OrderId(TraderId(b'0' * 20), OrderNumber(2)), 24 | AssetPair(AssetAmount(30, 'BTC'), AssetAmount(30, 'MB')), Timeout(0), Timestamp(0), False) 25 | self.order_ask = Order(OrderId(TraderId(b'0' * 20), OrderNumber(2)), 26 | AssetPair(AssetAmount(30, 'BTC'), AssetAmount(30, 'MB')), 27 | Timeout(0), Timestamp(0), True) 28 | self.order_bid = Order(OrderId(TraderId(b'0' * 20), OrderNumber(2)), 29 | AssetPair(AssetAmount(30, 'BTC'), AssetAmount(30, 'MB')), 30 | Timeout(0), Timestamp(0), False) 31 | 32 | def test_is_ask(self): 33 | # Test 'is ask' function 34 | self.assertTrue(self.tick.is_ask()) 35 | self.assertFalse(self.tick2.is_ask()) 36 | 37 | def test_to_network(self): 38 | # Test for to network 39 | self.assertEqual((TraderId(b'0' * 20), self.tick.timestamp, OrderNumber(1), 40 | AssetPair(AssetAmount(30, 'BTC'), AssetAmount(30, 'MB')), self.tick.timeout, 0), 41 | self.tick.to_network()) 42 | 43 | def test_traded_setter(self): 44 | # Test for traded setter 45 | self.tick.traded = 10 46 | self.assertEqual(10, self.tick.traded) 47 | 48 | def test_from_order_ask(self): 49 | # Test for from order 50 | ask = Tick.from_order(self.order_ask) 51 | self.assertIsInstance(ask, Ask) 52 | self.assertEqual(self.tick2.price, ask.price) 53 | self.assertEqual(self.tick2.assets, ask.assets) 54 | self.assertEqual(self.tick2.timestamp, ask.timestamp) 55 | self.assertEqual(self.tick2.order_id, ask.order_id) 56 | self.assertEqual(self.tick2.traded, 0) 57 | 58 | def test_from_order_bid(self): 59 | # Test for from order 60 | bid = Tick.from_order(self.order_bid) 61 | self.assertIsInstance(bid, Bid) 62 | self.assertEqual(self.tick2.price, bid.price) 63 | self.assertEqual(self.tick2.assets, bid.assets) 64 | self.assertEqual(self.tick2.timestamp, bid.timestamp) 65 | self.assertEqual(self.tick2.order_id, bid.order_id) 66 | self.assertEqual(self.tick2.traded, 0) 67 | 68 | def test_to_dictionary(self): 69 | """ 70 | Test the to dictionary method of a tick 71 | """ 72 | self.assertDictEqual(self.tick.to_dictionary(), { 73 | "trader_id": '30' * 20, 74 | "order_number": 1, 75 | "assets": self.tick.assets.to_dictionary(), 76 | "timeout": 30, 77 | "timestamp": 0.0, 78 | "traded": 0, 79 | "block_hash": hexlify(b'0' * 32).decode('utf-8') 80 | }) 81 | -------------------------------------------------------------------------------- /anydex/test/core/test_tickentry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.price import Price 8 | from anydex.core.pricelevel import PriceLevel 9 | from anydex.core.tick import Tick 10 | from anydex.core.tickentry import TickEntry 11 | from anydex.core.timeout import Timeout 12 | from anydex.core.timestamp import Timestamp 13 | 14 | 15 | @pytest.fixture 16 | def price_level(): 17 | return PriceLevel(Price(100, 1, 'MB', 'BTC')) 18 | 19 | 20 | @pytest.fixture 21 | async def tick_entry(price_level): 22 | tick = Tick(OrderId(TraderId(b'0' * 20), OrderNumber(1)), 23 | AssetPair(AssetAmount(60, 'BTC'), AssetAmount(30, 'MB')), Timeout(0), Timestamp(0), True) 24 | tick_entry = TickEntry(tick, price_level) 25 | yield tick_entry 26 | await tick_entry.shutdown_task_manager() 27 | 28 | 29 | @pytest.fixture 30 | async def tick_entry2(price_level): 31 | tick = Tick(OrderId(TraderId(b'0' * 20), OrderNumber(2)), 32 | AssetPair(AssetAmount(63400, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now(), True) 33 | tick_entry = TickEntry(tick, price_level) 34 | yield tick_entry 35 | await tick_entry.shutdown_task_manager() 36 | 37 | 38 | def test_price_level(price_level, tick_entry): 39 | assert price_level == tick_entry.price_level() 40 | 41 | 42 | def test_next_tick(price_level, tick_entry, tick_entry2): 43 | # Test for next tick 44 | assert not tick_entry.next_tick 45 | price_level.append_tick(tick_entry) 46 | price_level.append_tick(tick_entry2) 47 | assert tick_entry.next_tick == tick_entry2 48 | 49 | 50 | def test_prev_tick(price_level, tick_entry, tick_entry2): 51 | # Test for previous tick 52 | assert not tick_entry.prev_tick 53 | price_level.append_tick(tick_entry) 54 | price_level.append_tick(tick_entry2) 55 | assert tick_entry2.prev_tick == tick_entry 56 | 57 | 58 | def test_str(tick_entry): 59 | # Test for tick string representation 60 | assert str(tick_entry) == '60 BTC\t@\t0.5 MB' 61 | 62 | 63 | def test_is_valid(tick_entry, tick_entry2): 64 | # Test for is valid 65 | assert not tick_entry.is_valid() 66 | assert tick_entry2.is_valid() 67 | 68 | 69 | def test_block_for_matching(tick_entry): 70 | """ 71 | Test blocking of a match 72 | """ 73 | tick_entry.block_for_matching(OrderId(TraderId(b'a' * 20), OrderNumber(3))) 74 | assert len(tick_entry._blocked_for_matching) == 1 75 | 76 | # Try to add it again - should be ignored 77 | tick_entry.block_for_matching(OrderId(TraderId(b'a' * 20), OrderNumber(3))) 78 | assert len(tick_entry._blocked_for_matching) == 1 79 | -------------------------------------------------------------------------------- /anydex/test/core/test_timeout.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | from anydex.core.timeout import Timeout 5 | from anydex.core.timestamp import Timestamp 6 | 7 | 8 | class TimeoutTestSuite(unittest.TestCase): 9 | """Timeout test cases.""" 10 | 11 | def setUp(self): 12 | # Object creation 13 | self.timeout1 = Timeout(3600) 14 | self.timeout2 = Timeout(120) 15 | 16 | def test_init(self): 17 | # Test for init validation 18 | with self.assertRaises(ValueError): 19 | Timeout(-1.0) 20 | with self.assertRaises(ValueError): 21 | Timeout("1") 22 | 23 | def test_timed_out(self): 24 | # Test for timed out 25 | self.assertTrue(self.timeout1.is_timed_out(Timestamp(int(time.time() * 1000) - 3700 * 1000))) 26 | self.assertFalse(self.timeout2.is_timed_out(Timestamp(int(time.time() * 1000)))) 27 | 28 | def test_hash(self): 29 | # Test for hashes 30 | self.assertEqual(self.timeout1.__hash__(), Timeout(3600).__hash__()) 31 | self.assertNotEqual(self.timeout1.__hash__(), self.timeout2.__hash__()) 32 | -------------------------------------------------------------------------------- /anydex/test/core/test_timestamp.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | from anydex.core.timestamp import Timestamp 5 | 6 | 7 | class TimestampTestSuite(unittest.TestCase): 8 | """Timestamp test cases.""" 9 | 10 | def setUp(self): 11 | # Object creation 12 | self.timestamp = Timestamp(1462224447000) 13 | self.timestamp2 = Timestamp(1462224447000) 14 | self.timestamp3 = Timestamp(1305743832000) 15 | 16 | def test_init(self): 17 | # Test for init validation 18 | with self.assertRaises(ValueError): 19 | Timestamp(-1.0) 20 | with self.assertRaises(ValueError): 21 | Timestamp("1") 22 | 23 | def test_now(self): 24 | # Test for Timestamp.now 25 | self.assertAlmostEqual(int(time.time() * 1000), int(Timestamp.now()), delta=1000) 26 | 27 | def test_conversion(self): 28 | # Test for conversions 29 | self.assertEqual(1462224447000, int(self.timestamp)) 30 | 31 | # We cannot check the exact timestamp since this is specific to the configured time zone 32 | self.assertTrue(str(self.timestamp)) 33 | 34 | def test_comparison(self): 35 | # Test for comparison 36 | self.assertTrue(self.timestamp3 < self.timestamp) 37 | self.assertTrue(self.timestamp > self.timestamp3) 38 | self.assertTrue(self.timestamp3 < 1405743832000) 39 | self.assertTrue(self.timestamp <= 1462224447000) 40 | self.assertTrue(self.timestamp > 1362224447000) 41 | self.assertTrue(self.timestamp3 >= 1305743832000) 42 | self.assertEqual(NotImplemented, self.timestamp.__lt__("10")) 43 | self.assertEqual(NotImplemented, self.timestamp.__le__("10")) 44 | self.assertEqual(NotImplemented, self.timestamp.__gt__("10")) 45 | self.assertEqual(NotImplemented, self.timestamp.__ge__("10")) 46 | 47 | def test_equality(self): 48 | # Test for equality 49 | self.assertTrue(self.timestamp == self.timestamp2) 50 | self.assertTrue(self.timestamp != self.timestamp3) 51 | self.assertFalse(self.timestamp == 6) 52 | 53 | def test_hash(self): 54 | # Test for hashes 55 | self.assertEqual(self.timestamp.__hash__(), self.timestamp2.__hash__()) 56 | self.assertNotEqual(self.timestamp.__hash__(), self.timestamp3.__hash__()) 57 | -------------------------------------------------------------------------------- /anydex/test/core/test_transaction.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.payment import Payment 8 | from anydex.core.payment_id import PaymentId 9 | from anydex.core.timestamp import Timestamp 10 | from anydex.core.trade import Trade 11 | from anydex.core.transaction import Transaction, TransactionId 12 | from anydex.core.wallet_address import WalletAddress 13 | 14 | 15 | class TransactionTestSuite(unittest.TestCase): 16 | """Transaction test cases.""" 17 | 18 | def setUp(self): 19 | # Object creation 20 | self.maxDiff = None 21 | self.transaction_id = TransactionId(b'a' * 32) 22 | self.transaction = Transaction(self.transaction_id, AssetPair(AssetAmount(100, 'BTC'), AssetAmount(100, 'MB')), 23 | OrderId(TraderId(b'3' * 20), OrderNumber(2)), 24 | OrderId(TraderId(b'2' * 20), OrderNumber(1)), Timestamp(0)) 25 | self.proposed_trade = Trade.propose(TraderId(b'0' * 20), 26 | OrderId(TraderId(b'0' * 20), OrderNumber(2)), 27 | OrderId(TraderId(b'1' * 20), OrderNumber(3)), 28 | AssetPair(AssetAmount(100, 'BTC'), AssetAmount(100, 'MB')), Timestamp(0)) 29 | self.payment = Payment(TraderId(b'0' * 20), TransactionId(b'a' * 32), 30 | AssetAmount(100, 'MB'), WalletAddress('a'), WalletAddress('b'), 31 | PaymentId('aaa'), Timestamp(4)) 32 | self.payment2 = Payment(TraderId(b'0' * 20), TransactionId(b'a' * 32), 33 | AssetAmount(100, 'BTC'), WalletAddress('a'), WalletAddress('b'), 34 | PaymentId('aaa'), Timestamp(4)) 35 | 36 | def test_add_payment(self): 37 | """ 38 | Test the addition of a payment to a transaction 39 | """ 40 | self.transaction.add_payment(self.payment) 41 | self.assertEqual(self.transaction.transferred_assets.first.amount, 0) 42 | self.assertEqual(self.transaction.transferred_assets.second.amount, 100) 43 | self.assertTrue(self.transaction.payments) 44 | 45 | def test_next_payment(self): 46 | """ 47 | Test the process of determining the next payment details during a transaction 48 | """ 49 | self.assertEqual(self.transaction.next_payment(True, transfers_per_trade=1), AssetAmount(100, 'BTC')) 50 | self.assertEqual(self.transaction.next_payment(False, transfers_per_trade=1), AssetAmount(100, 'MB')) 51 | 52 | def test_next_payment_incremental(self): 53 | """ 54 | Test the process of determining the next payment details during a transaction with incremental settlement 55 | """ 56 | self.assertEqual(self.transaction.next_payment(True, transfers_per_trade=2), AssetAmount(50, 'BTC')) 57 | self.transaction._current_payment = 0 58 | self.assertEqual(self.transaction.next_payment(False, transfers_per_trade=2), AssetAmount(50, 'MB')) 59 | self.transaction._current_payment = 0 60 | 61 | # Test the edge case 62 | next_payment = self.transaction.next_payment(True, transfers_per_trade=3) 63 | self.assertEqual(next_payment, AssetAmount(33, 'BTC')) 64 | self.transaction.transferred_assets.first += next_payment 65 | next_payment = self.transaction.next_payment(True, transfers_per_trade=3) 66 | self.assertEqual(next_payment, AssetAmount(33, 'BTC')) 67 | self.transaction.transferred_assets.first += next_payment 68 | self.assertEqual(self.transaction.next_payment(True, transfers_per_trade=3), AssetAmount(34, 'BTC')) 69 | 70 | def test_is_payment_complete(self): 71 | """ 72 | Test whether a payment is correctly marked as complete 73 | """ 74 | self.assertFalse(self.transaction.is_payment_complete()) 75 | self.transaction.add_payment(self.payment) 76 | self.assertFalse(self.transaction.is_payment_complete()) 77 | self.transaction._transferred_assets = AssetPair(AssetAmount(100, 'BTC'), AssetAmount(100, 'MB')) 78 | self.assertTrue(self.transaction.is_payment_complete()) 79 | 80 | def test_to_dictionary(self): 81 | """ 82 | Test the to dictionary method of a transaction 83 | """ 84 | self.assertDictEqual(self.transaction.to_block_dictionary(), { 85 | 'trader_id': "33" * 20, 86 | 'transaction_id': "61" * 32, 87 | 'order_number': 2, 88 | 'partner_trader_id': "32" * 20, 89 | 'partner_order_number': 1, 90 | 'assets': { 91 | 'first': { 92 | 'amount': 100, 93 | 'type': 'BTC', 94 | }, 95 | 'second': { 96 | 'amount': 100, 97 | 'type': 'MB' 98 | } 99 | }, 100 | 'transferred': { 101 | 'first': { 102 | 'amount': 0, 103 | 'type': 'BTC', 104 | }, 105 | 'second': { 106 | 'amount': 0, 107 | 'type': 'MB' 108 | } 109 | }, 110 | 'timestamp': 0, 111 | }) 112 | 113 | def test_status(self): 114 | """ 115 | Test the status of a transaction 116 | """ 117 | self.assertEqual(self.transaction.status, 'pending') 118 | 119 | self.transaction.add_payment(self.payment) 120 | self.transaction.add_payment(self.payment2) 121 | self.assertEqual(self.transaction.status, 'completed') 122 | -------------------------------------------------------------------------------- /anydex/test/core/test_transaction_manager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.timestamp import Timestamp 8 | from anydex.core.transaction import Transaction, TransactionId 9 | from anydex.core.transaction_manager import TransactionManager 10 | from anydex.core.transaction_repository import MemoryTransactionRepository 11 | 12 | 13 | class TransactionManagerTestSuite(unittest.TestCase): 14 | """Transaction manager test cases.""" 15 | 16 | def setUp(self): 17 | # Object creation 18 | self.memory_transaction_repository = MemoryTransactionRepository(b'0' * 20) 19 | self.transaction_manager = TransactionManager(self.memory_transaction_repository) 20 | 21 | self.transaction_id = TransactionId(b'a' * 32) 22 | self.transaction = Transaction(self.transaction_id, AssetPair(AssetAmount(100, 'BTC'), AssetAmount(30, 'MB')), 23 | OrderId(TraderId(b'3' * 20), OrderNumber(2)), 24 | OrderId(TraderId(b'2' * 20), OrderNumber(1)), Timestamp(0)) 25 | 26 | def test_find_by_id(self): 27 | # Test for find by id 28 | self.assertEqual(None, self.transaction_manager.find_by_id(self.transaction_id)) 29 | self.memory_transaction_repository.add(self.transaction) 30 | self.assertEqual(self.transaction, self.transaction_manager.find_by_id(self.transaction_id)) 31 | 32 | def test_find_all(self): 33 | # Test for find all 34 | self.assertEqual([], list(self.transaction_manager.find_all())) 35 | self.memory_transaction_repository.add(self.transaction) 36 | self.assertEqual([self.transaction], list(self.transaction_manager.find_all())) 37 | -------------------------------------------------------------------------------- /anydex/test/core/test_transaction_repository.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.assetamount import AssetAmount 4 | from anydex.core.assetpair import AssetPair 5 | from anydex.core.message import TraderId 6 | from anydex.core.order import OrderId, OrderNumber 7 | from anydex.core.timestamp import Timestamp 8 | from anydex.core.transaction import Transaction, TransactionId 9 | from anydex.core.transaction_repository import MemoryTransactionRepository 10 | 11 | 12 | class MemoryTransactionRepositoryTestSuite(unittest.TestCase): 13 | """Memory transaction repository test cases.""" 14 | 15 | def setUp(self): 16 | # Object creation 17 | self.memory_transaction_repository = MemoryTransactionRepository(b'0' * 20) 18 | self.transaction_id = TransactionId(b'a' * 32) 19 | self.transaction = Transaction(self.transaction_id, AssetPair(AssetAmount(10, 'BTC'), AssetAmount(10, 'MB')), 20 | OrderId(TraderId(b'0' * 20), OrderNumber(1)), 21 | OrderId(TraderId(b'2' * 20), OrderNumber(2)), Timestamp(0)) 22 | 23 | def test_find_by_id(self): 24 | # Test for find by id 25 | self.assertEqual(None, self.memory_transaction_repository.find_by_id(self.transaction_id)) 26 | self.memory_transaction_repository.add(self.transaction) 27 | self.assertEqual(self.transaction, self.memory_transaction_repository.find_by_id(self.transaction_id)) 28 | 29 | def test_delete_by_id(self): 30 | # Test for delete by id 31 | self.memory_transaction_repository.add(self.transaction) 32 | self.assertEqual(self.transaction, self.memory_transaction_repository.find_by_id(self.transaction_id)) 33 | self.memory_transaction_repository.delete_by_id(self.transaction_id) 34 | self.assertEqual(None, self.memory_transaction_repository.find_by_id(self.transaction_id)) 35 | 36 | def test_find_all(self): 37 | # Test for find all 38 | self.assertEqual([], list(self.memory_transaction_repository.find_all())) 39 | self.memory_transaction_repository.add(self.transaction) 40 | self.assertEqual([self.transaction], list(self.memory_transaction_repository.find_all())) 41 | 42 | def test_update(self): 43 | # Test for update 44 | self.memory_transaction_repository.add(self.transaction) 45 | self.memory_transaction_repository.update(self.transaction) 46 | self.assertEqual(self.transaction, self.memory_transaction_repository.find_by_id(self.transaction_id)) 47 | -------------------------------------------------------------------------------- /anydex/test/core/test_wallet_address.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from anydex.core.wallet_address import WalletAddress 4 | 5 | 6 | class WalletAddressTestSuite(unittest.TestCase): 7 | """Bitcoin address test cases.""" 8 | 9 | def setUp(self): 10 | # Object creation 11 | self.wallet_address = WalletAddress("0") 12 | self.wallet_address2 = WalletAddress("1") 13 | 14 | def test_init(self): 15 | # Test for init validation 16 | with self.assertRaises(ValueError): 17 | WalletAddress(1) 18 | 19 | def test_conversion(self): 20 | # Test for conversions 21 | self.assertEqual("0", str(self.wallet_address)) 22 | self.assertEqual("1", str(self.wallet_address2)) 23 | -------------------------------------------------------------------------------- /anydex/test/restapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains REST API tests. 3 | """ 4 | -------------------------------------------------------------------------------- /anydex/test/restapi/base.py: -------------------------------------------------------------------------------- 1 | from ipv8.REST.rest_manager import RESTManager 2 | from ipv8.test.REST.rest_base import MockRestIPv8, RESTTestBase, partial_cls 3 | 4 | from anydex.core.community import MarketCommunity 5 | from anydex.restapi.root_endpoint import RootEndpoint 6 | from anydex.trustchain.community import TrustChainCommunity 7 | from anydex.wallet.dummy_wallet import DummyWallet1, DummyWallet2 8 | 9 | 10 | class MarketRESTTestBase(RESTTestBase): 11 | NUM_NODES = 1 12 | 13 | async def setUp(self): 14 | super(MarketRESTTestBase, self).setUp() 15 | await self.initialize([partial_cls(TrustChainCommunity, working_directory=':memory:'), 16 | partial_cls(MarketCommunity, working_directory=':memory:')], 1) 17 | 18 | self.market_community = self.nodes[0].get_overlay(MarketCommunity) 19 | self.market_community.trustchain = self.nodes[0].get_overlay(TrustChainCommunity) 20 | 21 | async def create_node(self, *args, **kwargs): 22 | ipv8 = MockRestIPv8(u"curve25519", overlay_classes=self.overlay_classes, *args, **kwargs) 23 | self.rest_manager = RESTManager(ipv8, root_endpoint_class=RootEndpoint) 24 | ipv8.rest_manager = self.rest_manager 25 | await self.rest_manager.start(0) 26 | ipv8.rest_port = self.rest_manager.site._server.sockets[0].getsockname()[1] 27 | 28 | dum1_wallet = DummyWallet1() 29 | dum2_wallet = DummyWallet2() 30 | dum1_wallet.MONITOR_DELAY = 0 31 | dum2_wallet.MONITOR_DELAY = 0 32 | 33 | wallets = {'DUM1': dum1_wallet, 'DUM2': dum2_wallet} 34 | 35 | market_community = ipv8.get_overlay(MarketCommunity) 36 | market_community.wallets = wallets 37 | market_community.settings.single_trade = False 38 | market_community.clearing_policies = [] 39 | 40 | return ipv8 41 | -------------------------------------------------------------------------------- /anydex/test/restapi/test_wallets_endpoint.py: -------------------------------------------------------------------------------- 1 | from ipv8.util import fail, succeed 2 | 3 | from sqlalchemy.orm import session as db_session 4 | 5 | from anydex.test.restapi.base import MarketRESTTestBase 6 | from anydex.test.util import timeout 7 | 8 | 9 | class TestWalletsEndpoint(MarketRESTTestBase): 10 | 11 | async def setUp(self): 12 | await super(TestWalletsEndpoint, self).setUp() 13 | 14 | from anydex.wallet.btc_wallet import BitcoinWallet, BitcoinTestnetWallet 15 | wallet_path = self.temporary_directory() 16 | btc_wallet = BitcoinWallet(wallet_path) 17 | btc_testnet_wallet = BitcoinTestnetWallet(wallet_path) 18 | 19 | self.market_community.wallets[btc_wallet.get_identifier()] = btc_wallet 20 | self.market_community.wallets[btc_testnet_wallet.get_identifier()] = btc_testnet_wallet 21 | 22 | async def tearDown(self): 23 | # Close all bitcoinlib Wallet DB sessions if exists 24 | db_session.close_all_sessions() 25 | 26 | await super(TestWalletsEndpoint, self).tearDown() 27 | 28 | @timeout(20) 29 | async def test_get_wallets(self): 30 | """ 31 | Testing whether the API returns wallets when we query for them 32 | """ 33 | json_response = await self.make_request(self.nodes[0], 'wallets', 'GET') 34 | self.assertIn('wallets', json_response) 35 | self.assertGreaterEqual(len(json_response['wallets']), 4) 36 | 37 | @timeout(20) 38 | async def test_create_wallet_exists(self): 39 | """ 40 | Testing whether creating a wallet that already exists throws an error 41 | """ 42 | await self.make_request(self.nodes[0], 'wallets/DUM1', 'PUT', expected_status=400) 43 | 44 | @timeout(20) 45 | async def test_create_wallet_btc(self): 46 | """ 47 | Test creating a BTC wallet 48 | """ 49 | self.market_community.wallets['BTC'].create_wallet = lambda: succeed(None) 50 | await self.make_request(self.nodes[0], 'wallets/BTC', 'PUT') 51 | 52 | @timeout(20) 53 | async def test_create_wallet(self): 54 | """ 55 | Testing whether we can create a wallet 56 | """ 57 | self.market_community.wallets['DUM1'].created = False 58 | await self.make_request(self.nodes[0], 'wallets/DUM1', 'PUT') 59 | 60 | @timeout(20) 61 | async def test_create_wallet_btc_error(self): 62 | """ 63 | Testing whether an error during the creation of a BTC wallet is correctly handled 64 | """ 65 | await self.make_request(self.nodes[0], 'wallets/BTC', 'PUT') 66 | self.market_community.wallets['BTC'].created = False 67 | await self.make_request(self.nodes[0], 'wallets/BTC', 'PUT', expected_status=500) 68 | 69 | @timeout(20) 70 | async def test_get_wallet_balance(self): 71 | """ 72 | Testing whether we can retrieve the balance of a wallet 73 | """ 74 | json_response = await self.make_request(self.nodes[0], 'wallets/DUM1/balance', 'GET') 75 | self.assertIn('balance', json_response) 76 | self.assertGreater(json_response['balance']['available'], 0) 77 | 78 | @timeout(20) 79 | async def test_get_wallet_transaction(self): 80 | """ 81 | Testing whether we can receive the transactions of a wallet 82 | """ 83 | json_response = await self.make_request(self.nodes[0], 'wallets/DUM1/transactions', 'GET') 84 | self.assertIn('transactions', json_response) 85 | 86 | @timeout(20) 87 | async def test_transfer_no_btc(self): 88 | """ 89 | Test transferring assets from a non-BTC wallet 90 | """ 91 | await self.make_request(self.nodes[0], 'wallets/DUM1/transfer', 'POST', expected_status=400, json={}) 92 | 93 | @timeout(20) 94 | async def test_transfer_not_created(self): 95 | """ 96 | Test transferring assets from a non-created BTC wallet 97 | """ 98 | await self.make_request(self.nodes[0], 'wallets/BTC/transfer', 'POST', expected_status=400, json={}) 99 | 100 | @timeout(20) 101 | async def test_transfer_bad_params(self): 102 | """ 103 | Test transferring assets when providing wrong parameters 104 | """ 105 | self.market_community.wallets['BTC'].created = True 106 | await self.make_request(self.nodes[0], 'wallets/BTC/transfer', 'POST', expected_status=400, json={}) 107 | 108 | @timeout(20) 109 | async def test_transfer_error(self): 110 | """ 111 | Test whether we receive the right response when we try a transfer that errors 112 | """ 113 | self.market_community.wallets['BTC'].transfer = lambda *_: fail(RuntimeError("error")) 114 | self.market_community.wallets['BTC'].created = True 115 | post_data = {'amount': 3, 'destination': 'abc'} 116 | await self.make_request(self.nodes[0], 'wallets/BTC/transfer', 'POST', expected_status=500, json=post_data) 117 | 118 | @timeout(20) 119 | async def test_transfer(self): 120 | """ 121 | Test transferring assets 122 | """ 123 | self.market_community.wallets['BTC'].created = True 124 | self.market_community.wallets['BTC'].transfer = lambda *_: succeed('abcd') 125 | post_data = {'amount': 3, 'destination': 'abc'} 126 | await self.make_request(self.nodes[0], 'wallets/BTC/transfer', 'POST', json=post_data) 127 | -------------------------------------------------------------------------------- /anydex/test/trustchain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/anydex-core/596fd172d36068fbf519f07abfcaafafc984365e/anydex/test/trustchain/__init__.py -------------------------------------------------------------------------------- /anydex/test/trustchain/test_database.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ipv8.keyvault.crypto import default_eccrypto 4 | from ipv8.test.base import TestBase 5 | 6 | from anydex.test.trustchain.test_block import TestBlock 7 | from anydex.trustchain.block import TrustChainBlock 8 | from anydex.trustchain.database import TrustChainDB 9 | 10 | 11 | class TestTrustChainMemoryDB(TestBase): 12 | 13 | def setUp(self): 14 | super(TestTrustChainMemoryDB, self).setUp() 15 | self.key = default_eccrypto.generate_key(u"curve25519") 16 | self.public_key = self.key.pub().key_to_bin() 17 | self.db = TrustChainDB(u":memory:", my_pk=self.public_key) 18 | 19 | def test_connected_users(self): 20 | """ 21 | Test returning connected users for a given public key works. 22 | """ 23 | self.assertEqual(len(self.db.get_users()), 0) 24 | self.assertEqual(len(self.db.get_connected_users(self.public_key)), 0) 25 | 26 | # Add 10 random blocks, implying 10 unique peers 27 | random_blocks = [] 28 | for i in range(0, 10): 29 | block = TestBlock() 30 | random_blocks.append(block) 31 | self.db.add_block(block) 32 | 33 | self.assertEqual(len(self.db.get_users()), 10) 34 | self.assertEqual(len(self.db.get_connected_users(self.public_key)), 0) 35 | 36 | # Create 5 link block implying 5 connected peers to the current user 37 | for i in range(0, 5): 38 | block = TrustChainBlock.create(b'test', {b'id': i}, self.db, self.public_key, link=random_blocks[i]) 39 | self.db.add_block(block) 40 | 41 | self.assertEqual(len(self.db.get_users()), 11) 42 | self.assertEqual(len(self.db.get_connected_users(self.public_key)), 5) 43 | 44 | def test_crawl(self): 45 | """ 46 | Test whether the crawl method returns the right blocks 47 | """ 48 | block1 = TestBlock(key=self.key) 49 | block2 = TestBlock(previous=block1, key=self.key) 50 | self.db.add_block(block1) 51 | self.db.add_block(block2) 52 | 53 | # Clear the block cache 54 | self.db.my_blocks_cache.blocks = {} 55 | self.db.my_blocks_cache.linked_blocks = {} 56 | 57 | self.assertEqual(len(self.db.crawl(self.public_key, 0, 10)), 2) 58 | 59 | # Add some linked blocks 60 | key2 = default_eccrypto.generate_key(u"curve25519") 61 | linked_block1 = TestBlock(key=key2, linked=block1) 62 | self.db.add_block(linked_block1) 63 | 64 | # We should get this newly added block now 65 | self.assertEqual(len(self.db.crawl(self.public_key, 0, 10)), 3) 66 | 67 | 68 | class TestTrustChainDB(TestBase): 69 | """ 70 | This class contains tests with a database persisted to disk. The database is written to a temporary directory and 71 | is cleaned up after the tests finished. 72 | """ 73 | 74 | def setUp(self): 75 | super(TestTrustChainDB, self).setUp() 76 | self.key = default_eccrypto.generate_key(u"curve25519") 77 | self.db_path = os.path.join(self.temporary_directory(), "trustchain.db") 78 | self.db = TrustChainDB(self.db_path) 79 | 80 | def test_upgrade_wipe_db(self): 81 | """ 82 | Test whether upgrading the database from version 1 to 6 removes all blocks 83 | """ 84 | self.db.execute("UPDATE OPTION set value='1' WHERE key='database_version'") 85 | block1 = TestBlock(key=self.key) 86 | self.db.add_block(block1) 87 | self.assertTrue(self.db.get_all_blocks()) 88 | self.db.close() 89 | 90 | # Load the database again 91 | db = TrustChainDB(self.db_path) 92 | self.assertFalse(db.get_all_blocks()) 93 | db.close() 94 | 95 | def test_upgrade_blob_text(self): 96 | """ 97 | Test whether upgrading the database from version 7 to 8 does not result in an error. 98 | We specifically test for the situation where a public key got inserted as TEXT type (in Python 2) and another 99 | public key as BLOB type (in Python 3). This would bypass the IntegrityViolation error. 100 | """ 101 | self.db.execute("UPDATE OPTION set value='7' WHERE key='database_version'") 102 | 103 | # Insert two blocks with the same public key and sequence number, bypassing the integrity violation. 104 | self.db.execute("INSERT INTO blocks VALUES('test', '{}', '1', 3, 'test', 3, '', '', 12345, 0, '');") 105 | self.db.execute("INSERT INTO blocks VALUES('test', '{}', X'31', 3, 'test', 3, '', '', 12345, 0, '');") 106 | 107 | self.assertTrue(self.db.get_all_blocks()) 108 | self.db.close() 109 | 110 | # Load the database again 111 | db = TrustChainDB(self.db_path, 'temp_trustchain') 112 | db.close() 113 | -------------------------------------------------------------------------------- /anydex/test/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test utilities. 3 | 4 | Partially based on the code from http://code.activestate.com/recipes/52215/ 5 | 6 | Author(s): Elric Milon 7 | """ 8 | import logging 9 | from asyncio import coroutine, iscoroutinefunction, wait_for 10 | from functools import wraps 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class MockObject: 17 | """ 18 | This class is used to create as base class for fake (mocked) objects. 19 | """ 20 | pass 21 | 22 | 23 | def timeout(timeout): 24 | def decorator(func): 25 | @wraps(func) 26 | async def wrapper(*args, **kwargs): 27 | coro = func if iscoroutinefunction(func) else coroutine(func) 28 | await wait_for(coro(*args, **kwargs), timeout) 29 | return wrapper 30 | return decorator 31 | -------------------------------------------------------------------------------- /anydex/test/wallet/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains tests for the files in the wallet module. 3 | """ 4 | -------------------------------------------------------------------------------- /anydex/test/wallet/test_btc_wallet.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from ipv8.util import succeed 4 | 5 | import pytest 6 | 7 | from sqlalchemy.orm import session as db_session 8 | 9 | from anydex.test.util import MockObject 10 | from anydex.wallet.btc_wallet import BitcoinTestnetWallet, BitcoinWallet 11 | 12 | 13 | @pytest.fixture 14 | async def wallet(tmpdir): 15 | wallet = BitcoinTestnetWallet(tmpdir) 16 | yield wallet 17 | await wallet.shutdown_task_manager() 18 | db_session.close_all_sessions() 19 | 20 | 21 | @pytest.mark.timeout(10) 22 | async def test_btc_wallet(wallet, tmpdir): 23 | """ 24 | Test the creating, opening, transactions and balance query of a Bitcoin (testnet) wallet 25 | """ 26 | await wallet.create_wallet() 27 | assert wallet.wallet 28 | assert wallet.get_address() 29 | 30 | wallet.wallet.utxos_update = lambda **_: None # We don't want to do an actual HTTP request here 31 | wallet.wallet.balance = lambda **_: 3 32 | balance = await wallet.get_balance() 33 | 34 | assert balance == {'available': 3, 'pending': 0, 'currency': 'BTC', 'precision': 8} 35 | wallet.wallet.transactions_update = lambda **_: None # We don't want to do an actual HTTP request here 36 | transactions = await wallet.get_transactions() 37 | assert not transactions 38 | 39 | wallet.get_transactions = lambda: succeed([{"id": "abc"}]) 40 | await wallet.monitor_transaction("abc") 41 | 42 | 43 | def test_btc_wallet_name(wallet): 44 | """ 45 | Test the name of a Bitcoin wallet 46 | """ 47 | assert wallet.get_name() == 'Testnet BTC' 48 | 49 | 50 | def test_btc_wallet_identfier(wallet): 51 | """ 52 | Test the identifier of a Bitcoin wallet 53 | """ 54 | assert wallet.get_identifier() == 'TBTC' 55 | 56 | 57 | def test_btc_wallet_address(wallet): 58 | """ 59 | Test the address of a Bitcoin wallet 60 | """ 61 | assert wallet.get_address() == '' 62 | 63 | 64 | def test_btc_wallet_unit(wallet): 65 | """ 66 | Test the mininum unit of a Bitcoin wallet 67 | """ 68 | assert wallet.min_unit() == 100000 69 | 70 | 71 | @pytest.mark.asyncio 72 | async def test_btc_balance_no_wallet(wallet): 73 | """ 74 | Test the retrieval of the balance of a BTC wallet that is not created yet 75 | """ 76 | balance = await wallet.get_balance() 77 | assert balance == {'available': 0, 'pending': 0, 'currency': 'BTC', 'precision': 8} 78 | 79 | 80 | @pytest.mark.timeout(10) 81 | @pytest.mark.asyncio 82 | async def test_btc_wallet_transfer(wallet): 83 | """ 84 | Test that the transfer method of a BTC wallet works 85 | """ 86 | await wallet.create_wallet() 87 | wallet.get_balance = lambda: succeed({'available': 100000, 'pending': 0, 'currency': 'BTC', 'precision': 8}) 88 | mock_tx = MockObject() 89 | mock_tx.hash = 'a' * 20 90 | wallet.wallet.send_to = lambda *_: mock_tx 91 | await wallet.transfer(3000, '2N8hwP1WmJrFF5QWABn38y63uYLhnJYJYTF') 92 | 93 | 94 | @pytest.mark.timeout(10) 95 | @pytest.mark.asyncio 96 | async def test_btc_wallet_create_error(wallet): 97 | """ 98 | Test whether an error during wallet creation is handled 99 | """ 100 | await wallet.create_wallet() # This should work 101 | with pytest.raises(Exception): 102 | await wallet.create_wallet() 103 | 104 | 105 | @pytest.mark.timeout(10) 106 | @pytest.mark.asyncio 107 | async def test_btc_wallet_transfer_no_funds(wallet): 108 | """ 109 | Test that the transfer method of a BTC wallet raises an error when we don't have enough funds 110 | """ 111 | await wallet.create_wallet() 112 | wallet.wallet.utxos_update = lambda **_: None # We don't want to do an actual HTTP request here 113 | with pytest.raises(Exception): 114 | await wallet.transfer(3000, '2N8hwP1WmJrFF5QWABn38y63uYLhnJYJYTF') 115 | 116 | 117 | @pytest.mark.timeout(10) 118 | @pytest.mark.asyncio 119 | async def test_get_transactions(wallet): 120 | """ 121 | Test whether transactions in bitcoinlib are correctly returned 122 | """ 123 | raw_tx = '02000000014bca66ebc0e3ab0c5c3aec6d0b3895b968497397752977dfd4a2f0bc67db6810000000006b483045022100fc' \ 124 | '93a034db310fbfead113283da95e980ac7d867c7aa4e6ef0aba80ef321639e02202bc7bd7b821413d814d9f7d6fc76ff46' \ 125 | 'b9cd3493173ef8d5fac40bce77a7016d01210309702ce2d5258eacc958e5a925b14de912a23c6478b8e2fb82af43d20212' \ 126 | '14f3feffffff029c4e7020000000001976a914d0115029aa5b2d2db7afb54a6c773ad536d0916c88ac90f4f70000000000' \ 127 | '1976a914f0eabff37e597b930647a3ec5e9df2e0fed0ae9888ac108b1500' 128 | 129 | mock_wallet = MockObject() 130 | mock_wallet.transactions_update = lambda **_: None 131 | mock_wallet._session = MockObject() 132 | 133 | mock_all = MockObject() 134 | mock_all.all = lambda *_: [(raw_tx, 3, datetime.datetime(2012, 9, 16, 0, 0), 12345)] 135 | mock_filter = MockObject() 136 | mock_filter.filter = lambda *_: mock_all 137 | mock_wallet._session.query = lambda *_: mock_filter 138 | wallet.wallet = mock_wallet 139 | wallet.wallet.wallet_id = 3 140 | 141 | mock_key = MockObject() 142 | mock_key.address = 'n3Uogo82Tyy76ZNuxmFfhJiFqAUbJ5BPho' 143 | wallet.wallet.keys = lambda **_: [mock_key] 144 | wallet.created = True 145 | 146 | transactions = await wallet.get_transactions() 147 | assert transactions 148 | assert transactions[0]["fee_amount"] == 12345 149 | assert transactions[0]["amount"] == 16250000 150 | 151 | 152 | @pytest.mark.asyncio 153 | async def test_real_btc_wallet_name(tmpdir): 154 | """ 155 | Test the name of a Bitcoin wallet 156 | """ 157 | wallet = BitcoinWallet(tmpdir) 158 | assert wallet.get_name() == 'Bitcoin' 159 | await wallet.shutdown_task_manager() 160 | db_session.close_all_sessions() 161 | 162 | 163 | @pytest.mark.asyncio 164 | async def test_real_btc_wallet_identfier(tmpdir): 165 | """ 166 | Test the identifier of a Bitcoin wallet 167 | """ 168 | wallet = BitcoinWallet(tmpdir) 169 | assert wallet.get_identifier() == 'BTC' 170 | await wallet.shutdown_task_manager() 171 | db_session.close_all_sessions() 172 | -------------------------------------------------------------------------------- /anydex/test/wallet/test_dummy_wallet.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from anydex.wallet.dummy_wallet import BaseDummyWallet, DummyWallet1, DummyWallet2 4 | from anydex.wallet.wallet import InsufficientFunds 5 | 6 | 7 | @pytest.fixture 8 | async def dummy_wallet(): 9 | dummy_wallet = BaseDummyWallet() 10 | yield dummy_wallet 11 | await dummy_wallet.shutdown_task_manager() 12 | 13 | 14 | def test_wallet_id(dummy_wallet): 15 | """ 16 | Test the identifier of a dummy wallet 17 | """ 18 | assert dummy_wallet.get_identifier() == 'DUM' 19 | assert DummyWallet1().get_identifier() == 'DUM1' 20 | assert DummyWallet2().get_identifier() == 'DUM2' 21 | 22 | 23 | def test_wallet_name(dummy_wallet): 24 | """ 25 | Test the name of a dummy wallet 26 | """ 27 | assert dummy_wallet.get_name() == 'Dummy' 28 | assert DummyWallet1().get_name() == 'Dummy 1' 29 | assert DummyWallet2().get_name() == 'Dummy 2' 30 | 31 | 32 | @pytest.mark.timeout(10) 33 | def test_create_wallet(dummy_wallet): 34 | """ 35 | Test the creation of a dummy wallet 36 | """ 37 | dummy_wallet.create_wallet() 38 | 39 | 40 | @pytest.mark.timeout(10) 41 | @pytest.mark.asyncio 42 | async def test_get_balance(dummy_wallet): 43 | """ 44 | Test fetching the balance of a dummy wallet 45 | """ 46 | balance = await dummy_wallet.get_balance() 47 | assert isinstance(balance, dict) 48 | 49 | 50 | @pytest.mark.timeout(10) 51 | @pytest.mark.asyncio 52 | async def test_transfer(dummy_wallet): 53 | """ 54 | Test the transfer of money from a dummy wallet 55 | """ 56 | await dummy_wallet.transfer(dummy_wallet.balance - 1, None) 57 | transactions = await dummy_wallet.get_transactions() 58 | assert len(transactions) == 1 59 | 60 | 61 | @pytest.mark.timeout(10) 62 | @pytest.mark.asyncio 63 | async def test_transfer_invalid(dummy_wallet): 64 | """ 65 | Test whether transferring a too large amount of money from a dummy wallet raises an error 66 | """ 67 | with pytest.raises(InsufficientFunds): 68 | await dummy_wallet.transfer(dummy_wallet.balance + 1, None) 69 | 70 | 71 | @pytest.mark.timeout(10) 72 | @pytest.mark.asyncio 73 | async def test_monitor(dummy_wallet): 74 | """ 75 | Test the monitor loop of a transaction wallet 76 | """ 77 | dummy_wallet.MONITOR_DELAY = 1 78 | await dummy_wallet.monitor_transaction("3.0") 79 | 80 | 81 | @pytest.mark.timeout(10) 82 | @pytest.mark.asyncio 83 | async def test_monitor_instant(dummy_wallet): 84 | """ 85 | Test an instant the monitor loop of a transaction wallet 86 | """ 87 | dummy_wallet.MONITOR_DELAY = 0 88 | await dummy_wallet.monitor_transaction("3.0") 89 | 90 | 91 | def test_address(dummy_wallet): 92 | """ 93 | Test the address of a dummy wallet 94 | """ 95 | assert isinstance(dummy_wallet.get_address(), str) 96 | 97 | 98 | @pytest.mark.timeout(10) 99 | @pytest.mark.asyncio 100 | async def test_get_transaction(dummy_wallet): 101 | """ 102 | Test the retrieval of transactions of a dummy wallet 103 | """ 104 | transactions = await dummy_wallet.get_transactions() 105 | assert isinstance(transactions, list) 106 | 107 | 108 | def test_min_unit(dummy_wallet): 109 | """ 110 | Test the minimum unit of a dummy wallet 111 | """ 112 | assert dummy_wallet.min_unit() == 1 113 | 114 | 115 | def test_generate_txid(dummy_wallet): 116 | """ 117 | Test the generation of a random transaction id 118 | """ 119 | assert dummy_wallet.generate_txid(10) 120 | assert len(dummy_wallet.generate_txid(20)) == 20 121 | -------------------------------------------------------------------------------- /anydex/test/wallet/test_trustchain_wallet.py: -------------------------------------------------------------------------------- 1 | from binascii import hexlify 2 | 3 | from ipv8.test.base import TestBase 4 | from ipv8.test.mocking.ipv8 import MockIPv8 5 | 6 | from anydex.test.util import timeout 7 | from anydex.trustchain.community import TrustChainCommunity 8 | from anydex.wallet.tc_wallet import TrustchainWallet 9 | from anydex.wallet.wallet import InsufficientFunds 10 | 11 | 12 | class TestTrustchainWallet(TestBase): 13 | 14 | def setUp(self): 15 | super(TestTrustchainWallet, self).setUp() 16 | self.initialize(TrustChainCommunity, 2) 17 | self.tc_wallet = TrustchainWallet(self.nodes[0].overlay) 18 | self.tc_wallet.MONITOR_DELAY = 0.01 19 | self.tc_wallet.check_negative_balance = True 20 | 21 | async def tearDown(self): 22 | await self.tc_wallet.shutdown_task_manager() 23 | await super(TestTrustchainWallet, self).tearDown() 24 | 25 | def create_node(self): 26 | return MockIPv8(u"curve25519", TrustChainCommunity, working_directory=u":memory:") 27 | 28 | def test_get_mc_wallet_name(self): 29 | """ 30 | Test the identifier of the Trustchain wallet 31 | """ 32 | self.assertEqual(self.tc_wallet.get_name(), 'Tokens (MB)') 33 | 34 | def test_get_mc_wallet_id(self): 35 | """ 36 | Test the identifier of a Trustchain wallet 37 | """ 38 | self.assertEqual(self.tc_wallet.get_identifier(), 'MB') 39 | 40 | @timeout(2) 41 | async def test_get_balance(self): 42 | """ 43 | Test the balance retrieval of a Trustchain wallet 44 | """ 45 | await self.introduce_nodes() 46 | 47 | balance = await self.tc_wallet.get_balance() 48 | self.assertEqual(balance['available'], 0) 49 | 50 | his_pubkey = list(self.nodes[0].network.verified_peers)[0].public_key.key_to_bin() 51 | tx = { 52 | 'up': 20 * 1024 * 1024, 53 | 'down': 5 * 1024 * 1024, 54 | 'total_up': 20 * 1024 * 1024, 55 | 'total_down': 5 * 1024 * 1024 56 | } 57 | self.nodes[0].overlay.sign_block(list(self.nodes[0].network.verified_peers)[0], public_key=his_pubkey, 58 | block_type=b'tribler_bandwidth', transaction=tx) 59 | 60 | await self.deliver_messages() 61 | 62 | balance = await self.tc_wallet.get_balance() 63 | self.assertEqual(balance['available'], 15) 64 | 65 | def test_create_wallet(self): 66 | """ 67 | Test whether creating a Trustchain wallet raises an error 68 | """ 69 | self.assertRaises(RuntimeError, self.tc_wallet.create_wallet) 70 | 71 | async def test_transfer_invalid(self): 72 | """ 73 | Test the transfer method of a Trustchain wallet 74 | """ 75 | with self.assertRaises(InsufficientFunds): 76 | await self.tc_wallet.transfer(200, None) 77 | 78 | async def test_monitor_transaction(self): 79 | """ 80 | Test the monitoring of a transaction in a Trustchain wallet 81 | """ 82 | his_pubkey = self.nodes[0].overlay.my_peer.public_key.key_to_bin() 83 | 84 | tx_future = self.tc_wallet.monitor_transaction('%s.1' % hexlify(his_pubkey).decode('utf-8')) 85 | 86 | # Now create the transaction 87 | transaction = { 88 | 'up': 20 * 1024 * 1024, 89 | 'down': 5 * 1024 * 1024, 90 | 'total_up': 20 * 1024 * 1024, 91 | 'total_down': 5 * 1024 * 1024 92 | } 93 | await self.nodes[1].overlay.sign_block(list(self.nodes[1].network.verified_peers)[0], public_key=his_pubkey, 94 | block_type=b'tribler_bandwidth', transaction=transaction) 95 | 96 | await tx_future 97 | 98 | @timeout(2) 99 | async def test_monitor_tx_existing(self): 100 | """ 101 | Test monitoring a transaction that already exists 102 | """ 103 | transaction = { 104 | 'up': 20 * 1024 * 1024, 105 | 'down': 5 * 1024 * 1024, 106 | 'total_up': 20 * 1024 * 1024, 107 | 'total_down': 5 * 1024 * 1024 108 | } 109 | his_pubkey = self.nodes[0].overlay.my_peer.public_key.key_to_bin() 110 | await self.nodes[1].overlay.sign_block(list(self.nodes[1].network.verified_peers)[0], public_key=his_pubkey, 111 | block_type=b'tribler_bandwidth', transaction=transaction) 112 | await self.tc_wallet.monitor_transaction('%s.1' % hexlify(his_pubkey).decode('utf-8')) 113 | 114 | def test_address(self): 115 | """ 116 | Test the address of a Trustchain wallet 117 | """ 118 | self.assertTrue(self.tc_wallet.get_address()) 119 | 120 | async def test_get_transaction(self): 121 | """ 122 | Test the retrieval of transactions of a Trustchain wallet 123 | """ 124 | transactions = await self.tc_wallet.get_transactions() 125 | self.assertIsInstance(transactions, list) 126 | 127 | def test_min_unit(self): 128 | """ 129 | Test the minimum unit of a Trustchain wallet 130 | """ 131 | self.assertEqual(self.tc_wallet.min_unit(), 1) 132 | 133 | async def test_get_statistics(self): 134 | """ 135 | Test fetching statistics from a Trustchain wallet 136 | """ 137 | self.tc_wallet.check_negative_balance = False 138 | res = self.tc_wallet.get_statistics() 139 | self.assertEqual(res["total_blocks"], 0) 140 | await self.tc_wallet.transfer(5, self.nodes[1].overlay.my_peer) 141 | res = self.tc_wallet.get_statistics() 142 | self.assertEqual(0, res["total_up"]) 143 | self.assertEqual(5 * 1024 * 1024, res["total_down"]) 144 | -------------------------------------------------------------------------------- /anydex/trustchain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/anydex-core/596fd172d36068fbf519f07abfcaafafc984365e/anydex/trustchain/__init__.py -------------------------------------------------------------------------------- /anydex/trustchain/blockcache.py: -------------------------------------------------------------------------------- 1 | class BlockCache(object): 2 | """ 3 | This class will cache (originating and linked) blocks in the chain of this user. 4 | """ 5 | 6 | def __init__(self, database, public_key): 7 | self.database = database 8 | self.public_key = public_key 9 | self.blocks = {} # Dictionary to hold known blocks (keys: sequence number, value: block) 10 | self.linked_blocks = {} # Dictionary to hold linked blocks (keys: sequence number, value: block) 11 | 12 | def add(self, block): 13 | """ 14 | Add a TrustChain block to the cache. 15 | :param block: The block to add. 16 | """ 17 | if block.public_key == self.public_key: 18 | self.blocks[block.sequence_number] = block 19 | else: 20 | self.linked_blocks[block.sequence_number] = block 21 | 22 | def get_range(self, start_seq_num, end_seq_num): 23 | missing = self.get_missing_in_range(start_seq_num, end_seq_num) 24 | if missing: 25 | # We are missing some blocks in our own chain, fetch them from the database 26 | missing_str = ','.join(["%d" % seq_num for seq_num in missing]) 27 | query = u"SELECT * FROM (%s WHERE sequence_number IN (%s) AND public_key = ?) " \ 28 | u"UNION SELECT * FROM (%s WHERE link_sequence_number IN (%s) AND " \ 29 | u"link_sequence_number != 0 AND link_public_key = ?)" % \ 30 | (self.database.get_sql_header(), missing_str, self.database.get_sql_header(), missing_str) 31 | db_result = list(self.database.execute(query, 32 | (self.public_key, self.public_key), 33 | fetch_all=True)) 34 | blocks = [self.database.get_block_class(db_item[0] if isinstance(db_item[0], bytes) 35 | else str(db_item[0]).encode('utf-8'))(db_item) 36 | for db_item in db_result] 37 | for block in blocks: # Add them to the cache 38 | self.add(block) 39 | 40 | # Check whether we need to fetch blocks linked to linked blocks. These blocks are not fetched by our 41 | # SQL query unfortunately. 42 | for seq_num in range(start_seq_num, end_seq_num + 1): 43 | my_block = self.blocks.get(seq_num, None) 44 | if my_block and seq_num not in self.linked_blocks: 45 | linked_block = self.database.get_linked(my_block) 46 | if linked_block: 47 | self.add(linked_block) 48 | else: 49 | # Even if the linked block does not exist, we add a None value anyway. 50 | # We do so to indicate that we have actually checked whether it exists. 51 | self.linked_blocks[seq_num] = None 52 | 53 | # We now get all the blocks in the requested range 54 | blocks = [] 55 | for seq_num in range(start_seq_num, end_seq_num + 1): 56 | my_block = self.blocks.get(seq_num, None) 57 | if my_block: 58 | blocks.append(my_block) 59 | linked_block = self.linked_blocks.get(seq_num, None) 60 | if linked_block: 61 | blocks.append(linked_block) 62 | 63 | return blocks 64 | 65 | def get_missing_in_range(self, start_seq_num, end_seq_num): 66 | """ 67 | Get all the sequence numbers that are missing within a given range. 68 | """ 69 | missing = [] 70 | for seq_num in range(start_seq_num, end_seq_num + 1): 71 | if seq_num not in self.blocks: 72 | missing.append(seq_num) 73 | 74 | return missing 75 | -------------------------------------------------------------------------------- /anydex/trustchain/caches.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from asyncio import get_event_loop 3 | from binascii import hexlify 4 | from functools import reduce 5 | 6 | from ipv8.requestcache import NumberCache 7 | from ipv8.util import maximum_integer 8 | 9 | 10 | class IntroCrawlTimeout(NumberCache): 11 | """ 12 | A crawl request is sent with every introduction response. This can happen quite a lot of times per second. 13 | We wish to slow down the amount of crawls we do to not overload any node with database IO. 14 | """ 15 | 16 | def __init__(self, community, peer, identifier=u"introcrawltimeout"): 17 | super(IntroCrawlTimeout, self).__init__(community.request_cache, identifier, 18 | self.get_number_for(peer)) 19 | 20 | @classmethod 21 | def get_number_for(cls, peer): 22 | """ 23 | Convert a Peer into an int. To do this we shift every byte of the mid into an integer. 24 | """ 25 | charlist = [] 26 | for i in range(len(peer.mid)): 27 | charlist.append(peer.mid[i]) 28 | return reduce(lambda a, b: ((a << 8) | b), charlist, 0) 29 | 30 | @property 31 | def timeout_delay(self): 32 | """ 33 | We crawl the same peer, at most once every 60 seconds. 34 | :return: 35 | """ 36 | return 60.0 37 | 38 | def on_timeout(self): 39 | """ 40 | This is expected, the super class will now remove itself from the request cache. 41 | The node is then allowed to be crawled again. 42 | """ 43 | pass 44 | 45 | 46 | class ChainCrawlCache(IntroCrawlTimeout): 47 | """ 48 | This cache keeps track of the crawl of a whole chain. 49 | """ 50 | def __init__(self, community, peer, crawl_future, known_chain_length=-1): 51 | super(ChainCrawlCache, self).__init__(community, peer, identifier=u"chaincrawl") 52 | self.community = community 53 | self.current_crawl_future = None 54 | self.crawl_future = crawl_future 55 | self.peer = peer 56 | self.known_chain_length = known_chain_length 57 | 58 | self.current_request_range = (0, 0) 59 | self.current_request_attempts = 0 60 | 61 | @property 62 | def timeout_delay(self): 63 | return 120.0 64 | 65 | 66 | class HalfBlockSignCache(NumberCache): 67 | """ 68 | This request cache keeps track of outstanding half block signature requests. 69 | """ 70 | 71 | def __init__(self, community, half_block, sign_future, socket_address, timeouts=0): 72 | """ 73 | A cache to keep track of the signing of one of our blocks by a counterparty. 74 | 75 | :param community: the TrustChainCommunity 76 | :param half_block: the half_block requiring a counterparty 77 | :param sign_future: the Deferred to fire once this block has been double signed 78 | :param socket_address: the peer we sent the block to 79 | :param timeouts: the number of timeouts we have already had while waiting 80 | """ 81 | block_id_int = int(hexlify(half_block.block_id), 16) % 100000000 82 | super(HalfBlockSignCache, self).__init__(community.request_cache, u"sign", block_id_int) 83 | self.logger = logging.getLogger(self.__class__.__name__) 84 | self.community = community 85 | self.half_block = half_block 86 | self.sign_future = sign_future 87 | self.socket_address = socket_address 88 | self.timeouts = timeouts 89 | 90 | @property 91 | def timeout_delay(self): 92 | """ 93 | Note that we use a very high timeout for a half block signature. Ideally, we would like to have a request 94 | cache without any timeouts and just keep track of outstanding signature requests but this isn't possible (yet). 95 | """ 96 | return self.community.settings.sign_attempt_delay 97 | 98 | def on_timeout(self): 99 | if self.sign_future.done(): 100 | self._logger.debug("Race condition encountered with timeout/removal of HalfBlockSignCache, recovering.") 101 | return 102 | self._logger.info("Timeout for sign request for half block %s, note that it can still arrive!", self.half_block) 103 | if self.timeouts < self.community.settings.sign_timeout: 104 | self.community.send_block(self.half_block, address=self.socket_address) 105 | 106 | async def add_later(): 107 | self.community.request_cache.add(HalfBlockSignCache(self.community, self.half_block, self.sign_future, 108 | self.socket_address, self.timeouts + 1)) 109 | self.community.request_cache.register_anonymous_task("add-later", add_later, delay=0.0) 110 | else: 111 | self.sign_future.set_exception(RuntimeError("Signature request timeout")) 112 | 113 | 114 | class CrawlRequestCache(NumberCache): 115 | """ 116 | This request cache keeps track of outstanding crawl requests. 117 | """ 118 | CRAWL_TIMEOUT = 20.0 119 | 120 | def __init__(self, community, crawl_id, crawl_future): 121 | super(CrawlRequestCache, self).__init__(community.request_cache, u"crawl", crawl_id) 122 | self.logger = logging.getLogger(self.__class__.__name__) 123 | self.community = community 124 | self.crawl_future = crawl_future 125 | self.received_half_blocks = [] 126 | self.total_half_blocks_expected = maximum_integer 127 | 128 | @property 129 | def timeout_delay(self): 130 | return CrawlRequestCache.CRAWL_TIMEOUT 131 | 132 | def received_block(self, block, total_count): 133 | self.received_half_blocks.append(block) 134 | self.total_half_blocks_expected = total_count 135 | 136 | if self.total_half_blocks_expected == 0: 137 | self.community.request_cache.pop(u"crawl", self.number) 138 | self.crawl_future.set_result([]) 139 | elif len(self.received_half_blocks) >= self.total_half_blocks_expected: 140 | self.community.request_cache.pop(u"crawl", self.number) 141 | self.crawl_future.set_result(self.received_half_blocks) 142 | 143 | def received_empty_response(self): 144 | self.community.request_cache.pop(u"crawl", self.number) 145 | self.crawl_future.set_result(self.received_half_blocks) 146 | 147 | def on_timeout(self): 148 | self._logger.info("Timeout for crawl with id %d", self.number) 149 | self.crawl_future.set_result(self.received_half_blocks) 150 | -------------------------------------------------------------------------------- /anydex/trustchain/listener.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from .block import TrustChainBlock 4 | 5 | 6 | class BlockListener(metaclass=abc.ABCMeta): 7 | """ 8 | This class defines a listener for TrustChain blocks with a specific type. 9 | """ 10 | 11 | BLOCK_CLASS = TrustChainBlock 12 | 13 | @abc.abstractmethod 14 | def should_sign(self, block): 15 | """ 16 | Method to indicate whether this listener wants a specific block signed or not. 17 | """ 18 | pass 19 | 20 | @abc.abstractmethod 21 | def received_block(self, block): 22 | """ 23 | This method is called when a listener receives a block that matches the BLOCK_CLASS. 24 | """ 25 | pass 26 | 27 | @abc.abstractmethod 28 | def on_counter_signed_block(self, block): 29 | """ 30 | This method is called when we counter signed a block. 31 | """ 32 | pass 33 | -------------------------------------------------------------------------------- /anydex/trustchain/settings.py: -------------------------------------------------------------------------------- 1 | class TrustChainSettings(object): 2 | """ 3 | This class holds various settings regarding TrustChain. 4 | """ 5 | 6 | def __init__(self): 7 | # The set with block types that should not be broadcast 8 | self.block_types_bc_disabled = set() 9 | 10 | # The fan-out of the broadcast when a new block is created 11 | self.broadcast_fanout = 25 12 | 13 | # The amount of history to keep for broadcasts 14 | self.broadcast_history_size = 100000 15 | 16 | # How many prior blocks we require before signing a new incoming block 17 | self.validation_range = 0 18 | 19 | # The maximum number of blocks we want to store in the database 20 | self.max_db_blocks = 1000000 21 | 22 | # Whether we are a crawler (and fetching whole chains) 23 | self.crawler = False 24 | 25 | # How many blocks at most we allow others to crawl in one batch 26 | self.max_crawl_batch = 10 27 | 28 | # The delay in seconds after which we send a half block to the counterparty again 29 | self.sign_attempt_delay = 10 30 | 31 | # The timeout after which we stop trying to get the half block signed by the counterparty 32 | self.sign_timeout = 360 33 | -------------------------------------------------------------------------------- /anydex/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tribler/anydex-core/596fd172d36068fbf519f07abfcaafafc984365e/anydex/util/__init__.py -------------------------------------------------------------------------------- /anydex/util/asyncio.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from asyncio import CancelledError, coroutine, ensure_future, iscoroutine 3 | 4 | from ipv8.taskmanager import delay_runner 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def call_later(delay, func, *args, ignore_errors=False): 10 | if not iscoroutine(func): 11 | func = coroutine(func) 12 | 13 | task = ensure_future(delay_runner(delay, func, *args)) 14 | if ignore_errors: 15 | add_default_callback(task) 16 | return task 17 | 18 | 19 | def add_default_callback(task): 20 | def done_cb(future): 21 | try: 22 | future.result() 23 | except CancelledError: 24 | pass 25 | except Exception as e: 26 | logging.error('Task raised exception: %s', e) 27 | 28 | return task.add_done_callback(done_cb) 29 | -------------------------------------------------------------------------------- /anydex/wallet/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains code for the various wallets implemented in Tribler. 3 | """ 4 | -------------------------------------------------------------------------------- /anydex/wallet/bandwidth_block.py: -------------------------------------------------------------------------------- 1 | import orjson as json 2 | 3 | from anydex.trustchain.block import EMPTY_SIG, GENESIS_HASH, GENESIS_SEQ, TrustChainBlock, ValidationResult 4 | 5 | 6 | class TriblerBandwidthBlock(TrustChainBlock): 7 | """ 8 | Container for bandwidth block information 9 | """ 10 | 11 | @classmethod 12 | def create(cls, block_type, transaction, database, public_key, link=None, link_pk=None, additional_info=None): 13 | """ 14 | Create an empty next block. 15 | :param block_type: the type of the block to be created 16 | :param transaction: the transaction to use in this block 17 | :param database: the database to use as information source 18 | :param public_key: the public key to use for this block 19 | :param link: optionally create the block as a linked block to this block 20 | :param link_pk: the public key of the counterparty in this transaction 21 | :param additional_info: additional information, which has a higher priority than the 22 | transaction when link exists 23 | :return: A newly created block 24 | """ 25 | latest_bw_block = database.get_latest(public_key, block_type=b'tribler_bandwidth') 26 | latest_block = database.get_latest(public_key) 27 | ret = cls() 28 | if link: 29 | ret.type = link.type 30 | ret.transaction["up"] = link.transaction["down"] if "down" in link.transaction else 0 31 | ret.transaction["down"] = link.transaction["up"] if "up" in link.transaction else 0 32 | ret.link_public_key = link.public_key 33 | ret.link_sequence_number = link.sequence_number 34 | else: 35 | ret.type = block_type 36 | ret.transaction["up"] = transaction["up"] if "up" in transaction else 0 37 | ret.transaction["down"] = transaction["down"] if "down" in transaction else 0 38 | ret.link_public_key = link_pk 39 | 40 | if latest_bw_block: 41 | ret.transaction["total_up"] = latest_bw_block.transaction["total_up"] + ret.transaction["up"] 42 | ret.transaction["total_down"] = latest_bw_block.transaction["total_down"] + ret.transaction["down"] 43 | else: 44 | ret.transaction["total_up"] = ret.transaction["up"] 45 | ret.transaction["total_down"] = ret.transaction["down"] 46 | 47 | if latest_block: 48 | ret.sequence_number = latest_block.sequence_number + 1 49 | ret.previous_hash = latest_block.hash 50 | 51 | ret._transaction = json.dumps(ret.transaction) 52 | ret.public_key = public_key 53 | ret.signature = EMPTY_SIG 54 | ret.hash = ret.calculate_hash() 55 | 56 | return ret 57 | 58 | def validate_transaction(self, database): 59 | """ 60 | Validates this transaction 61 | :param database: the database to check against 62 | :return: A tuple consisting of a ValidationResult and a list of user string errors 63 | """ 64 | result = [ValidationResult.valid] 65 | errors = [] 66 | 67 | def err(reason): 68 | result[0] = ValidationResult.invalid 69 | errors.append(reason) 70 | 71 | if self.transaction["up"] < 0: 72 | err("Up field is negative") 73 | if self.transaction["down"] < 0: 74 | err("Down field is negative") 75 | if self.transaction["down"] == 0 and self.transaction["up"] == 0: 76 | # In this case the block doesn't modify any counters, these block are without purpose and are thus invalid. 77 | err("Up and down are zero") 78 | if self.transaction["total_up"] < 0: 79 | err("Total up field is negative") 80 | if self.transaction["total_down"] < 0: 81 | err("Total down field is negative") 82 | 83 | blk = database.get(self.public_key, self.sequence_number) 84 | link = database.get_linked(self) 85 | prev_blk = database.get_block_before(self, block_type=b'tribler_bandwidth') 86 | next_blk = database.get_block_after(self, block_type=b'tribler_bandwidth') 87 | 88 | is_genesis = self.sequence_number == GENESIS_SEQ or self.previous_hash == GENESIS_HASH 89 | if is_genesis: 90 | if self.transaction["total_up"] != self.transaction["up"]: 91 | err("Genesis block invalid total_up and/or up") 92 | if self.transaction["total_down"] != self.transaction["down"]: 93 | err("Genesis block invalid total_down and/or down") 94 | 95 | if blk: 96 | if blk.transaction["up"] != self.transaction["up"]: 97 | err("Up does not match known block") 98 | if blk.transaction["down"] != self.transaction["down"]: 99 | err("Down does not match known block") 100 | if blk.transaction["total_up"] != self.transaction["total_up"]: 101 | err("Total up does not match known block") 102 | if blk.transaction["total_down"] != self.transaction["total_down"]: 103 | err("Total down does not match known block") 104 | 105 | if link: 106 | if self.transaction["up"] != link.transaction["down"]: 107 | err("Up/down mismatch on linked block") 108 | if self.transaction["down"] != link.transaction["up"]: 109 | err("Down/up mismatch on linked block") 110 | 111 | if prev_blk: 112 | if prev_blk.transaction["total_up"] + self.transaction["up"] > self.transaction["total_up"]: 113 | err("Total up is lower than expected compared to the preceding block") 114 | if prev_blk.transaction["total_down"] + self.transaction["down"] > self.transaction["total_down"]: 115 | err("Total down is lower than expected compared to the preceding block") 116 | 117 | if next_blk: 118 | if self.transaction["total_up"] + next_blk.transaction["up"] > next_blk.transaction["total_up"]: 119 | err("Total up is higher than expected compared to the next block") 120 | # In this case we could say there is fraud too, since the counters are too high. Also anyone that 121 | # counter signed any such counters should be suspected since they apparently failed to validate or put 122 | # their signature on it regardless of validation status. But it is not immediately clear where this 123 | # error occurred, it might be lower on the chain than self. So it is hard to create a fraud proof here 124 | if self.transaction["total_down"] + next_blk.transaction["down"] > next_blk.transaction["total_down"]: 125 | err("Total down is higher than expected compared to the next block") 126 | # See previous comment 127 | 128 | return result[0], errors 129 | -------------------------------------------------------------------------------- /anydex/wallet/bitcoinlib_main.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the 'patched' main method for bitcoinlib. 3 | The original main file has many side effects and creates all kinds of directories across the system. 4 | This file makes sure that all these directories are created inside a designated (wallet) directory. 5 | It should be imported before any bitcoinlib imports. 6 | """ 7 | import ast 8 | import functools 9 | import imp 10 | # Important import, do not remove! Files importing stuff from this file, rely on availability of the logger module. 11 | import logging 12 | import os 13 | import struct # DO NOT REMOVE AS UNUSED; It is magically used by bitcoinlib 14 | import sys 15 | from logging.handlers import RotatingFileHandler 16 | 17 | sys.modules["bitcoinlib.main"] = sys.modules[__name__] 18 | 19 | 20 | DEFAULT_DOCDIR = None 21 | DEFAULT_DATABASEDIR = None 22 | DEFAULT_LOGDIR = None 23 | DEFAULT_SETTINGSDIR = None 24 | CURRENT_INSTALLDIR = None 25 | CURRENT_INSTALLDIR_DATA = None 26 | DEFAULT_DATABASEFILE = 'bitcoinlib.sqlite' 27 | DEFAULT_DATABASE = None 28 | TIMEOUT_REQUESTS = 5 29 | 30 | 31 | def initialize_lib(wallet_dir): 32 | global DEFAULT_DOCDIR, DEFAULT_DATABASEDIR, DEFAULT_LOGDIR, DEFAULT_SETTINGSDIR, DEFAULT_DATABASE,\ 33 | CURRENT_INSTALLDIR, CURRENT_INSTALLDIR_DATA 34 | try: 35 | bitcoinlib_path = imp.find_module('bitcoinlib')[1] 36 | CURRENT_INSTALLDIR = bitcoinlib_path 37 | CURRENT_INSTALLDIR_DATA = os.path.join(bitcoinlib_path, 'data') 38 | DEFAULT_DOCDIR = wallet_dir 39 | DEFAULT_DATABASEDIR = os.path.join(DEFAULT_DOCDIR, 'database/') 40 | DEFAULT_LOGDIR = os.path.join(DEFAULT_DOCDIR, 'log/') 41 | DEFAULT_SETTINGSDIR = os.path.join(DEFAULT_DOCDIR, 'config/') 42 | DEFAULT_DATABASE = DEFAULT_DATABASEDIR + DEFAULT_DATABASEFILE 43 | 44 | if not os.path.exists(DEFAULT_DOCDIR): 45 | os.makedirs(DEFAULT_DOCDIR) 46 | if not os.path.exists(DEFAULT_LOGDIR): 47 | os.makedirs(DEFAULT_LOGDIR) 48 | if not os.path.exists(DEFAULT_SETTINGSDIR): 49 | os.makedirs(DEFAULT_SETTINGSDIR) 50 | 51 | # Copy data and settings file 52 | from shutil import copyfile 53 | 54 | src_files = os.listdir(CURRENT_INSTALLDIR_DATA) 55 | for file_name in src_files: 56 | full_file_name = os.path.join(CURRENT_INSTALLDIR_DATA, file_name) 57 | if os.path.isfile(full_file_name): 58 | copyfile(full_file_name, os.path.join(DEFAULT_SETTINGSDIR, file_name)) 59 | 60 | # Extract all variable assignments from the original file and make sure these variables are initialized. 61 | excluded_assignments = ['logfile', 'handler', 'logger', 'formatter'] 62 | with open(os.path.join(CURRENT_INSTALLDIR, 'main.py'), 'rb') as source_file: 63 | file_contents = source_file.read() 64 | ast_module_node = ast.parse(file_contents) 65 | for node in ast.iter_child_nodes(ast_module_node): 66 | if isinstance(node, ast.Assign): 67 | node_id, value = node.targets[0].id, node.value 68 | if not hasattr(sys.modules[__name__], node_id) and node_id not in excluded_assignments: 69 | output = eval(compile(ast.Expression(value), '', 'eval')) 70 | setattr(sys.modules[__name__], node_id, output) 71 | 72 | # Clear everything related to bitcoinlib from sys.modules 73 | for module_name in list(sys.modules): 74 | if module_name.startswith('bitcoinlib') and module_name != 'bitcoinlib.main': 75 | del sys.modules[module_name] 76 | 77 | # Make sure the OPCODES are known to the transaction files 78 | import bitcoinlib 79 | from bitcoinlib.config.opcodes import opcodes, opcodenames, OP_N_CODES 80 | bitcoinlib.transactions.opcodes = opcodes 81 | bitcoinlib.transactions.opcodenames = opcodenames 82 | bitcoinlib.transactions.OP_N_CODES = OP_N_CODES 83 | except ImportError: 84 | pass 85 | 86 | 87 | def script_type_default(witness_type=None, multisig=False, locking_script=False): 88 | """ 89 | Determine default script type for provided witness type and key type combination used in this library. 90 | 91 | :param witness_type: Type of wallet: standard or segwit 92 | :type witness_type: str 93 | :param multisig: Multisig key or not, default is False 94 | :type multisig: bool 95 | :param locking_script: Limit search to locking_script. Specify False for locking scripts and True 96 | for unlocking scripts 97 | :type locking_script: bool 98 | :return str: Default script type 99 | """ 100 | 101 | if not witness_type: 102 | return None 103 | if witness_type == 'legacy' and not multisig: 104 | return 'p2pkh' if locking_script else 'sig_pubkey' 105 | elif witness_type == 'legacy' and multisig: 106 | return 'p2sh' if locking_script else 'p2sh_multisig' 107 | elif witness_type == 'segwit' and not multisig: 108 | return 'p2wpkh' if locking_script else 'sig_pubkey' 109 | elif witness_type == 'segwit' and multisig: 110 | return 'p2wsh' if locking_script else 'p2sh_multisig' 111 | elif witness_type == 'p2sh-segwit' and not multisig: 112 | return 'p2sh' if locking_script else 'p2sh_p2wpkh' 113 | elif witness_type == 'p2sh-segwit' and multisig: 114 | return 'p2sh' if locking_script else 'p2sh_p2wsh' 115 | else: 116 | raise ValueError("Wallet and key type combination not supported: %s / %s" % (witness_type, multisig)) 117 | 118 | 119 | def get_encoding_from_witness(witness_type=None): 120 | """ 121 | Derive address encoding (base58 or bech32) from transaction witness type 122 | 123 | :param witness_type: Witness type: legacy, p2sh-segwit or segwit 124 | :type witness_type: str 125 | 126 | :return str: 127 | """ 128 | 129 | if witness_type == 'segwit': 130 | return 'bech32' 131 | elif witness_type in [None, 'legacy', 'p2sh-segwit']: 132 | return 'base58' 133 | else: 134 | raise ValueError("Unknown witness type %s" % witness_type) 135 | 136 | 137 | def deprecated(func): 138 | """This is a decorator which can be used to mark functions 139 | as deprecated. It will result in a warning being emitted 140 | when the function is used.""" 141 | 142 | @functools.wraps(func) 143 | def new_func(*args, **kwargs): 144 | logging.warning("Call to deprecated function {}.".format(func.__name__)) 145 | return func(*args, **kwargs) 146 | return new_func 147 | -------------------------------------------------------------------------------- /anydex/wallet/dummy_wallet.py: -------------------------------------------------------------------------------- 1 | import string 2 | from random import choice 3 | 4 | from ipv8.util import succeed 5 | 6 | from anydex.wallet.wallet import InsufficientFunds, Wallet 7 | 8 | 9 | class BaseDummyWallet(Wallet): 10 | """ 11 | This is a dummy wallet that is primarily used for testing purposes 12 | """ 13 | MONITOR_DELAY = 1 14 | 15 | def __init__(self): 16 | super(BaseDummyWallet, self).__init__() 17 | 18 | self.balance = 1000 19 | self.created = True 20 | self.unlocked = True 21 | self.address = ''.join([choice(string.ascii_lowercase) for _ in range(10)]) 22 | self.transaction_history = [] 23 | 24 | def get_name(self): 25 | return 'Dummy' 26 | 27 | def get_identifier(self): 28 | return 'DUM' 29 | 30 | def create_wallet(self, *args, **kwargs): 31 | return succeed(None) 32 | 33 | def get_balance(self): 34 | return succeed({ 35 | 'available': self.balance, 36 | 'pending': 0, 37 | 'currency': self.get_identifier(), 38 | 'precision': self.precision() 39 | }) 40 | 41 | async def transfer(self, quantity, candidate): 42 | self._logger.info("Transferring %s %s to %s from dummy wallet", quantity, self.get_identifier(), candidate) 43 | 44 | balance = await self.get_balance() 45 | if balance['available'] < quantity: 46 | raise InsufficientFunds() 47 | self.balance -= quantity 48 | self.transaction_history.append({ 49 | 'id': str(quantity), 50 | 'outgoing': True, 51 | 'from': self.address, 52 | 'to': '', 53 | 'amount': quantity, 54 | 'fee_amount': 0.0, 55 | 'currency': self.get_identifier(), 56 | 'timestamp': '', 57 | 'description': '' 58 | }) 59 | return str(quantity) 60 | 61 | def monitor_transaction(self, transaction_id): 62 | """ 63 | Monitor an incoming transaction with a specific ID. 64 | """ 65 | def on_transaction_done(): 66 | self.transaction_history.append({ 67 | 'id': transaction_id, 68 | 'outgoing': True, 69 | 'from': '', 70 | 'to': self.address, 71 | 'amount': float(str(transaction_id)), 72 | 'fee_amount': 0.0, 73 | 'currency': self.get_identifier(), 74 | 'timestamp': '', 75 | 'description': '' 76 | }) 77 | 78 | self.balance += float(str(transaction_id)) # txid = amount of money transferred 79 | 80 | if self.MONITOR_DELAY == 0: 81 | return succeed(on_transaction_done()) 82 | else: 83 | return self.register_anonymous_task('monitor_transaction', on_transaction_done, delay=self.MONITOR_DELAY) 84 | 85 | def get_address(self): 86 | return self.address 87 | 88 | def get_transactions(self): 89 | return succeed(self.transaction_history) 90 | 91 | def min_unit(self): 92 | return 1 93 | 94 | def precision(self): 95 | return 0 96 | 97 | 98 | class DummyWallet1(BaseDummyWallet): 99 | 100 | def get_name(self): 101 | return 'Dummy 1' 102 | 103 | def get_identifier(self): 104 | return 'DUM1' 105 | 106 | 107 | class DummyWallet2(BaseDummyWallet): 108 | 109 | def __init__(self): 110 | super(DummyWallet2, self).__init__() 111 | self.balance = 10000 112 | 113 | def get_name(self): 114 | return 'Dummy 2' 115 | 116 | def get_identifier(self): 117 | return 'DUM2' 118 | 119 | def precision(self): 120 | return 1 121 | -------------------------------------------------------------------------------- /anydex/wallet/wallet.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import logging 3 | import random 4 | import string 5 | 6 | from ipv8.taskmanager import TaskManager 7 | 8 | 9 | class InsufficientFunds(Exception): 10 | """ 11 | Used for throwing exception when there isn't sufficient funds available to transfer assets. 12 | """ 13 | pass 14 | 15 | 16 | class Wallet(TaskManager, metaclass=abc.ABCMeta): 17 | """ 18 | This is the base class of a wallet and contains various methods that every wallet should implement. 19 | To create your own wallet, subclass this class and implement the required methods. 20 | """ 21 | def __init__(self): 22 | super(Wallet, self).__init__() 23 | self._logger = logging.getLogger(self.__class__.__name__) 24 | self.created = False 25 | self.unlocked = False 26 | 27 | def generate_txid(self, length=10): 28 | """ 29 | Generate a random transaction ID 30 | """ 31 | return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(length)) 32 | 33 | @abc.abstractmethod 34 | def get_identifier(self): 35 | return 36 | 37 | @abc.abstractmethod 38 | def get_name(self): 39 | return 40 | 41 | @abc.abstractmethod 42 | def create_wallet(self, *args, **kwargs): 43 | return 44 | 45 | @abc.abstractmethod 46 | def get_balance(self): 47 | return 48 | 49 | @abc.abstractmethod 50 | async def transfer(self, *args, **kwargs): 51 | return 52 | 53 | @abc.abstractmethod 54 | def get_address(self): 55 | return 56 | 57 | @abc.abstractmethod 58 | def get_transactions(self): 59 | return 60 | 61 | @abc.abstractmethod 62 | def min_unit(self): 63 | return 64 | 65 | @abc.abstractmethod 66 | def precision(self): 67 | """ 68 | The precision of an asset inside a wallet represents the number of digits after the decimal. 69 | For fiat currency, the precision would be 2 since the minimum unit is often 0.01. 70 | """ 71 | return 72 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import signal 4 | import sys 5 | from asyncio import ensure_future, gather, get_event_loop, sleep 6 | 7 | import anydex # To set the IPv8 path 8 | 9 | from ipv8.attestation.trustchain.community import TrustChainCommunity, TrustChainTestnetCommunity # noqa 10 | from ipv8.dht.discovery import DHTDiscoveryCommunity 11 | from ipv8.peerdiscovery.discovery import RandomWalk 12 | 13 | from ipv8_service import IPv8 14 | 15 | from anydex.config import get_anydex_configuration 16 | from anydex.core.community import MarketTestnetCommunity 17 | from anydex.restapi.rest_manager import RESTManager 18 | from anydex.wallet.dummy_wallet import DummyWallet1, DummyWallet2 19 | 20 | 21 | class AnyDexService(object): 22 | 23 | def __init__(self): 24 | """ 25 | Initialize the variables of the AnyDexServiceMaker and the logger. 26 | """ 27 | self.ipv8 = None 28 | self.restapi = None 29 | self.trustchain = None 30 | self.dht = None 31 | self.market = None 32 | self.wallets = {} 33 | self._stopping = False 34 | 35 | async def start_anydex(self, options): 36 | """ 37 | Main method to startup AnyDex. 38 | """ 39 | config = get_anydex_configuration() 40 | 41 | if options.statedir: 42 | # If we use a custom state directory, update various variables 43 | for key in config["keys"]: 44 | key["file"] = os.path.join(options.statedir, key["file"]) 45 | 46 | for community in config["overlays"]: 47 | if community["class"] == "TrustChainCommunity": 48 | community["initialize"]["working_directory"] = options.statedir 49 | 50 | if options.testnet: 51 | for community in config["overlays"]: 52 | if community["class"] == "TrustChainCommunity": 53 | community["class"] = "TrustChainTestnetCommunity" 54 | 55 | self.ipv8 = IPv8(config, enable_statistics=options.statistics) 56 | await self.ipv8.start() 57 | 58 | print("Starting AnyDex") 59 | 60 | if not options.no_rest_api: 61 | self.restapi = RESTManager(self.ipv8) 62 | await self.restapi.start(options.apiport) 63 | 64 | async def signal_handler(sig): 65 | print("Received shut down signal %s" % sig) 66 | if not self._stopping: 67 | self._stopping = True 68 | if self.restapi: 69 | await self.restapi.stop() 70 | self.restapi = None 71 | await self.ipv8.stop() 72 | get_event_loop().stop() 73 | 74 | signal.signal(signal.SIGINT, lambda sig, _: ensure_future(signal_handler(sig))) 75 | signal.signal(signal.SIGTERM, lambda sig, _: ensure_future(signal_handler(sig))) 76 | 77 | # Get Trustchain + DHT overlays 78 | for overlay in self.ipv8.overlays: 79 | if isinstance(overlay, (TrustChainCommunity, TrustChainTestnetCommunity)): 80 | self.trustchain = overlay 81 | elif isinstance(overlay, DHTDiscoveryCommunity): 82 | self.dht = overlay 83 | 84 | # Initialize wallets 85 | dummy_wallet1 = DummyWallet1() 86 | self.wallets[dummy_wallet1.get_identifier()] = dummy_wallet1 87 | 88 | dummy_wallet2 = DummyWallet2() 89 | self.wallets[dummy_wallet2.get_identifier()] = dummy_wallet2 90 | 91 | # Load market community 92 | self.market = MarketTestnetCommunity(self.trustchain.my_peer, self.ipv8.endpoint, self.ipv8.network, 93 | trustchain=self.trustchain, 94 | dht=self.dht, 95 | wallets=self.wallets, 96 | working_directory=options.statedir, 97 | record_transactions=False, 98 | is_matchmaker=not options.no_matchmaker) 99 | 100 | self.ipv8.overlays.append(self.market) 101 | self.ipv8.strategies.append((RandomWalk(self.market), 20)) 102 | 103 | 104 | def main(argv): 105 | parser = argparse.ArgumentParser(add_help=False, description=('Anydex')) 106 | parser.add_argument('--help', '-h', action='help', default=argparse.SUPPRESS, help='Show this help message and exit') 107 | parser.add_argument('--no-rest-api', '-a', action='store_const', default=False, const=True, help='Autonomous: disable the REST api') 108 | parser.add_argument('--no-matchmaker', action='store_const', default=False, const=True, help='Disable matchmaker functionality') 109 | parser.add_argument('--statistics', action='store_const', default=False, const=True, help='Enable IPv8 overlay statistics') 110 | parser.add_argument('--testnet', '-t', action='store_const', default=False, const=True, help='Join the testnet') 111 | parser.add_argument('--statedir', '-s', default='.', type=str, help='Use an alternate statedir') 112 | parser.add_argument('--apiport', '-p', default=8090, type=int, help='Use an alternative port for the REST api') 113 | 114 | args = parser.parse_args(sys.argv[1:]) 115 | service = AnyDexService() 116 | 117 | loop = get_event_loop() 118 | coro = service.start_anydex(args) 119 | ensure_future(coro) 120 | 121 | if sys.platform == 'win32': 122 | # Unfortunately, this is needed on Windows for Ctrl+C to work consistently. 123 | # Should no longer be needed in Python 3.8. 124 | async def wakeup(): 125 | while True: 126 | await sleep(1) 127 | ensure_future(wakeup()) 128 | 129 | loop.run_forever() 130 | 131 | 132 | if __name__ == "__main__": 133 | main(sys.argv[1:]) 134 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='anydex', 8 | author='Tribler', 9 | description='The Universal Decentralized Exchange', 10 | long_description=long_description, 11 | long_description_content_type='text/markdown', 12 | version='0.1.0', 13 | url='https://github.com/Tribler/anydex-core', 14 | package_dir={'': 'anydex'}, 15 | packages=find_packages("anydex", exclude=["ipv8"]), 16 | py_modules=[], 17 | install_requires=[ 18 | "autobahn", 19 | "bitcoinlib==0.4.5", 20 | "cryptography", 21 | "libnacl", 22 | "netifaces", 23 | "aiohttp", 24 | "pyOpenSSL" 25 | ], 26 | classifiers=[ 27 | "Development Status :: 5 - Production/Stable", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 30 | "Natural Language :: English", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python :: 3.6", 33 | "Programming Language :: Python :: 3.7", 34 | "Topic :: Scientific/Engineering", 35 | "Topic :: Software Development :: Libraries :: Python Modules", 36 | "Topic :: System :: Distributed Computing", 37 | ] 38 | ) 39 | --------------------------------------------------------------------------------