def _start_push_service(self): logger.info("Start pushnotification service") path = self.config["push_notification"]["firebase_credentials_path"] app = create_firebase_app_from_path_to_keyfile(path) self._firebase_raw_push_service = FirebaseRawPushService(app) self._client_token_db = ClientTokenDB(engine=create_engine()) logger.info("Firebase pushservice started") self._start_pushnotifications_for_registered_users()
def test_add_multiple_client_token(client_token_db: ClientTokenDB): client_token_db.add_client_token("0x123", "token1") client_token_db.add_client_token("0x123", "token2") client_token_db.add_client_token("0x123", "token3") assert set(client_token_db.get_client_tokens("0x123")) == { "token1", "token2", "token3", }
def test_all_tokens(client_token_db: ClientTokenDB): client_token_db.add_client_token("0x123", "token1") client_token_db.add_client_token("0x124", "token2") client_token_db.add_client_token("0x125", "token3") assert set(client_token_db.get_all_client_tokens()) == { ("0x123", "token1"), ("0x124", "token2"), ("0x125", "token3"), }
def _start_push_service(self): path = self.config.get("firebase", {}).get("credentialsPath", None) if path is not None: self._firebase_raw_push_service = FirebaseRawPushService(path) self._client_token_db = ClientTokenDB(engine=create_engine()) logger.info("Firebase pushservice started") self._start_pushnotifications_for_registered_users() else: logger.info( "No firebase credentials in config, pushservice disabled")
class TrustlinesRelay: def __init__(self, config, addresses_json_path="addresses.json"): self.config = config self.addresses_json_path = addresses_json_path self.currency_network_proxies: Dict[str, CurrencyNetworkProxy] = {} self.currency_network_graphs: Dict[str, CurrencyNetworkGraph] = {} self.subjects = defaultdict(Subject) self.messaging = defaultdict(MessagingSubject) self.contracts = {} self.node: Node = None self._web3 = None self.orderbook: OrderBookGreenlet = None self.unw_eth_proxies: Dict[str, UnwEthProxy] = {} self.token_proxies: Dict[str, TokenProxy] = {} self._firebase_raw_push_service: Optional[ FirebaseRawPushService] = None self._client_token_db: Optional[ClientTokenDB] = None self.fixed_gas_price: Optional[int] = None self.known_identity_factories: List[str] = [] self._log_listener = None @property def network_addresses(self) -> Iterable[str]: return self.currency_network_proxies.keys() @property def exchange_addresses(self) -> Iterable[str]: if self.orderbook is not None: return self.orderbook.exchange_addresses else: return [] @property def unw_eth_addresses(self) -> Iterable[str]: return list(self.unw_eth_proxies) @property def token_addresses(self) -> Iterable[str]: return list(self.token_proxies) + list(self.unw_eth_addresses) @property def enable_ether_faucet(self) -> bool: return self.config["faucet"]["enable"] @property def enable_relay_meta_transaction(self) -> bool: return self.config["delegate"]["enable"] @property def enable_deploy_identity(self) -> bool: return self.config["delegate"]["enable_deploy_identity"] def get_ethindex_db_for_currency_network( self, network_address: Optional[str] = None ) -> ethindex_db.CurrencyNetworkEthindexDB: """return an EthindexDB instance. This is being used from relay.api to query for events. """ return ethindex_db.CurrencyNetworkEthindexDB( ethindex_db.connect(""), address=network_address, standard_event_types=currency_network_events.standard_event_types, event_builders=currency_network_events.event_builders, from_to_types=currency_network_events.from_to_types, ) def get_ethindex_db_for_token(self, address: str): """return an EthindexDB instance This is being used from relay.api to query for events. """ return ethindex_db.EthindexDB( ethindex_db.connect(""), address=address, standard_event_types=token_events.standard_event_types, event_builders=token_events.event_builders, from_to_types=token_events.from_to_types, ) def get_ethindex_db_for_unw_eth(self, address: str): """return an EthindexDB instance This is being used from relay.api to query for events. """ return ethindex_db.EthindexDB( ethindex_db.connect(""), address=address, standard_event_types=unw_eth_events.standard_event_types, event_builders=unw_eth_events.event_builders, from_to_types=unw_eth_events.from_to_types, ) def get_ethindex_db_for_exchange(self, address: Optional[str] = None): """return an EthindexDB instance This is being used from relay.api to query for events. """ return ethindex_db.ExchangeEthindexDB( ethindex_db.connect(""), address=address, standard_event_types=exchange_events.standard_event_types, event_builders=exchange_events.event_builders, from_to_types=exchange_events.from_to_types, ) def is_currency_network(self, address: str) -> bool: return address in self.network_addresses def is_currency_network_frozen(self, address: str) -> bool: return self.currency_network_proxies[address].is_frozen def is_trusted_token(self, address: str) -> bool: return address in self.token_addresses or address in self.unw_eth_addresses def get_network_info(self, network_address: str) -> NetworkInfo: proxy = self.currency_network_proxies[network_address] graph = self.currency_network_graphs[network_address] assert (proxy.address is not None), "Invalid currency network proxy with no address." return NetworkInfo( address=proxy.address, name=proxy.name, symbol=proxy.symbol, decimals=proxy.decimals, num_users=len(graph.users), capacity_imbalance_fee_divisor=proxy. capacity_imbalance_fee_divisor, default_interest_rate=proxy.default_interest_rate, custom_interests=proxy.custom_interests, prevent_mediator_interests=proxy.prevent_mediator_interests, interest_rate_decimals=proxy.interest_rate_decimals, is_frozen=proxy.is_frozen, ) def get_network_infos(self) -> List[NetworkInfo]: return [ self.get_network_info(network_address) for network_address in self.network_addresses ] def get_users_of_network(self, network_address: str): return self.currency_network_graphs[network_address].users def get_friends_of_user_in_network(self, network_address: str, user_address: str): return self.currency_network_graphs[network_address].get_friends( user_address) def get_list_of_accrued_interests_for_trustline( self, network_address: str, user_address: str, counterparty_address: str, start_time: int = 0, end_time: int = None, ): if not end_time: end_time = int(time.time()) event_selector = self.get_ethindex_db_for_currency_network( network_address) return EventsInformationFetcher( event_selector ).get_list_of_paid_interests_for_trustline_in_between_timestamps( network_address, user_address, counterparty_address, start_time, end_time) def get_transfer_information_for_tx_hash(self, tx_hash: str): fetcher = EventsInformationFetcher( self.get_ethindex_db_for_currency_network()) return fetcher.get_transfer_details_for_tx(tx_hash) def get_transfer_information_from_event_id(self, block_hash, log_index): fetcher = EventsInformationFetcher( self.get_ethindex_db_for_currency_network()) return fetcher.get_transfer_details_for_id(block_hash, log_index) def get_paid_delegation_fees_for_tx_hash(self, tx_hash): event_proxy = IdentityProxy(self._web3, abi=self.contracts["Identity"]["abi"]) return event_proxy.get_transaction_events( tx_hash, event_types=FeePaymentEventType) def deploy_identity(self, factory_address, implementation_address, signature): return self.delegate.deploy_identity(factory_address, implementation_address, signature) def delegate_meta_transaction(self, meta_transaction: MetaTransaction): return self.delegate.send_signed_meta_transaction(meta_transaction) def get_meta_transaction_status(self, identity_address, hash): return self.delegate.get_meta_transaction_status( identity_address, hash) def meta_transaction_fees(self, meta_transaction: MetaTransaction): return self.delegate.calculate_fees_for_meta_transaction( meta_transaction) def get_identity_info(self, identity_address: str): return { "balance": self.node.balance(identity_address), "identity": identity_address, "nextNonce": self.delegate.calc_next_nonce(identity_address), "implementationAddress": self.delegate.get_implementation_address(identity_address), } def _install_w3_middleware(self): """install signing middleware if an account is configured""" if "account" not in self.config: logger.warning("No account configured") return keystore_path = self.config["account"]["keystore_path"] keystore_password_path = self.config["account"][ "keystore_password_path"] with open(keystore_password_path, "r") as password_file: password = password_file.readline().strip() try: private_key = eth_keyfile.extract_key_from_keyfile( keystore_path, password.encode("utf-8")) except ValueError: raise ValueError( "Could not decrypt keystore. Please make sure the password is correct." ) account = eth_account.Account.from_key(private_key) signing_middleware.install_signing_middleware(self._web3, account) logger.info( f"private key for address {account.address} loaded for signing transactions" ) def _make_w3(self): """create and set the w3 object as self._web3""" rpc_config = self.config["node_rpc"] self._web3 = Web3(create_provider_from_config(rpc_config)) def _start_delegate(self): default_fee_recipient = self._web3.eth.defaultAccount or self.node.address delegation_fees = [ DelegationFees( fee_recipient=d.get("fee_recipient", default_fee_recipient), base_fee=d["base_fee"], gas_price=d["gas_price"], currency_network_of_fees=d["currency_network"], ) for d in self.config["delegate"]["fees"] if d ] logger.info( f"Started delegate with delegation fees: {delegation_fees}") self.delegate = Delegate( self._web3, default_fee_recipient, self.contracts["Identity"]["abi"], self.known_identity_factories, delegation_fees=delegation_fees, config=self.config["delegate"], ) def start(self): self._load_gas_price_settings( self.config["relay"]["gas_price_computation"]) self.contracts = load_packaged_contracts() if self.config["exchange"]["enable"]: self._load_orderbook() if self.config["push_notification"]["enable"]: self._start_push_service() self._make_w3() self._install_w3_middleware() self.node = Node(self._web3, fixed_gas_price=self.fixed_gas_price) self._log_listener = LogFilterListener(self._web3) if self.config["delegate"]["enable"]: self._start_delegate() self._load_addresses() def new_network(self, address: str) -> None: assert is_checksum_address(address) if address in self.network_addresses: return logger.info("New network: {}".format(address)) self.currency_network_proxies[address] = CurrencyNetworkProxy( self._web3, self.contracts["CurrencyNetwork"]["abi"], address) currency_network_proxy = self.currency_network_proxies[address] self.currency_network_graphs[address] = CurrencyNetworkGraph( capacity_imbalance_fee_divisor=currency_network_proxy. capacity_imbalance_fee_divisor, default_interest_rate=currency_network_proxy.default_interest_rate, custom_interests=currency_network_proxy.custom_interests, prevent_mediator_interests=currency_network_proxy. prevent_mediator_interests, ) self._log_listener.add_proxy(currency_network_proxy) self._start_listen_network(address) def new_exchange(self, address: str) -> None: assert is_checksum_address(address) if address not in self.exchange_addresses: logger.info("New Exchange contract: {}".format(address)) if self.orderbook is not None: proxy = ExchangeProxy( self._web3, self.contracts["Exchange"]["abi"], self.contracts["Token"]["abi"], address, self, ) self.orderbook.add_exchange(proxy) else: raise ValueError( "Exchange address given but no orderbook loaded") def new_unw_eth(self, address: str) -> None: assert is_checksum_address(address) if address not in self.unw_eth_addresses: logger.info("New Unwrap ETH contract: {}".format(address)) proxy = UnwEthProxy(self._web3, self.contracts["UnwEth"]["abi"], address) self.unw_eth_proxies[address] = proxy def new_token(self, address: str) -> None: assert is_checksum_address(address) if address not in self.token_addresses: logger.info("New Token contract: {}".format(address)) proxy = TokenProxy(self._web3, self.contracts["Token"]["abi"], address) self.token_proxies[address] = proxy def new_known_factory(self, address: str) -> None: assert is_checksum_address(address) if address not in self.known_identity_factories: logger.info("New identity factory contract: {}".format(address)) self.known_identity_factories.append(address) def get_networks_of_user(self, user_address: str) -> List[str]: assert is_checksum_address(user_address) networks_of_user: List[str] = [] for network_address in self.network_addresses: if user_address in self.currency_network_graphs[ network_address].users: networks_of_user.append(network_address) return networks_of_user def add_push_client_token(self, user_address: str, client_token: str) -> None: if self._firebase_raw_push_service is not None: self._start_pushnotifications(user_address, client_token) try: if self._client_token_db is not None: self._client_token_db.add_client_token( user_address, client_token) except ClientTokenAlreadyExistsException: pass # all good def delete_push_client_token(self, user_address: str, client_token: str) -> None: if self._firebase_raw_push_service is not None: if self._client_token_db is not None: self._client_token_db.delete_client_token( user_address, client_token) self._stop_pushnotifications(user_address, client_token) def _start_pushnotifications(self, user_address: str, client_token: str) -> None: assert self._firebase_raw_push_service is not None if not self._firebase_raw_push_service.check_client_token( client_token): raise InvalidClientTokenException for subscription in self.subjects[user_address].subscriptions: if (isinstance(subscription.client, PushNotificationClient) and subscription.client.client_token == client_token): return # Token already registered logger.debug("Add client token {} for address {}".format( client_token, user_address)) client = PushNotificationClient(self._firebase_raw_push_service, client_token) self.subjects[user_address].subscribe(client) # Silent: Do not mark notifications as read, so that we can query them later self.messaging[user_address].subscribe(client, silent=True) def _stop_pushnotifications(self, user_address: str, client_token: str) -> None: success = False subscriptions = self.subjects[user_address].subscriptions # Copy the list because we will delete items while iterating over it for subscription in subscriptions[:]: if (isinstance(subscription.client, PushNotificationClient) and subscription.client.client_token == client_token): logger.debug("Remove client token {} for address {}".format( client_token, user_address)) subscription.client.close() success = True if not success: raise TokenNotFoundException def get_user_network_events( self, network_address: str, user_address: str, type: str = None, from_block: int = 0, ) -> List[BlockchainEvent]: ethindex_db = self.get_ethindex_db_for_currency_network( network_address) if type is not None: events = ethindex_db.get_network_events( type, user_address, from_block=from_block, ) else: events = ethindex_db.get_all_network_events(user_address, from_block=from_block) return events def get_trustline_events( self, network_address: str, user_address: str, counterparty_address: str, type: str = None, from_block: int = 0, ): if type is None: event_types = None else: event_types = [type] ethindex = ethindex_db.CurrencyNetworkEthindexDB( ethindex_db.connect(""), address=network_address, standard_event_types=currency_network_events.trustline_event_types, event_builders=currency_network_events.event_builders, from_to_types=currency_network_events.from_to_types, ) events = ethindex.get_trustline_events( network_address, user_address, counterparty_address, event_types, from_block=from_block, ) return events def get_network_events(self, network_address: str, type: str = None, from_block: int = 0) -> List[BlockchainEvent]: ethindex_db = self.get_ethindex_db_for_currency_network( network_address) if type is not None: events = ethindex_db.get_events(type, from_block=from_block) else: events = ethindex_db.get_all_events(from_block=from_block) return events def get_user_events( self, user_address: str, event_type: str = None, from_block: int = 0, contract_type: ContractTypes = None, ) -> List[BlockchainEvent]: """ Get all events of users for user_address. Filter with from_block, event_type and contract_type """ assert is_checksum_address(user_address) event_types: Optional[List[str]] if event_type: event_types = [event_type] else: event_types = None address_to_contract_types: Dict[str, str] = {} if contract_type == ContractTypes.CURRENCY_NETWORK or contract_type is None: for address in self.network_addresses: address_to_contract_types[ address] = ContractTypes.CURRENCY_NETWORK.value if contract_type == ContractTypes.EXCHANGE or contract_type is None: for address in self.exchange_addresses: address_to_contract_types[ address] = ContractTypes.EXCHANGE.value if contract_type == ContractTypes.TOKEN or contract_type is None: for address in self.token_addresses: address_to_contract_types[address] = ContractTypes.TOKEN.value if contract_type == ContractTypes.UNWETH or contract_type is None: for address in self.unw_eth_addresses: address_to_contract_types[address] = ContractTypes.UNWETH.value ethindex = ethindex_db.EthindexDB( ethindex_db.connect(""), standard_event_types=all_standard_event_types, event_builders=all_event_builders, from_to_types=all_from_to_types, address_to_contract_types=address_to_contract_types, ) return ethindex.get_all_contract_events( event_types, user_address=user_address, from_block=from_block, ) def get_user_token_events( self, token_address: str, user_address: str, type: str = None, from_block: int = 0, ) -> List[BlockchainEvent]: if token_address in self.unw_eth_addresses: ethindex_db = self.get_ethindex_db_for_unw_eth(token_address) else: ethindex_db = self.get_ethindex_db_for_token(token_address) if type is not None: events = getattr(ethindex_db, "get_user_events")(event_type=type, user_address=user_address, from_block=from_block) else: events = getattr(ethindex_db, "get_all_contract_events")( user_address=user_address, from_block=from_block) return events def get_token_events(self, token_address: str, type: str = None, from_block: int = 0) -> List[BlockchainEvent]: if token_address in self.unw_eth_addresses: ethindex_db = self.get_ethindex_db_for_unw_eth(token_address) else: ethindex_db = self.get_ethindex_db_for_token(token_address) if type is not None: events = ethindex_db.get_events(type, from_block=from_block) else: events = ethindex_db.get_all_events(from_block=from_block) return events def get_exchange_events(self, exchange_address: str, type: str = None, from_block: int = 0) -> List[BlockchainEvent]: ethindex_db = self.get_ethindex_db_for_exchange(exchange_address) if type is not None: events = ethindex_db.get_events(type, from_block=from_block) else: events = ethindex_db.get_all_events(from_block=from_block) return events def get_user_exchange_events( self, exchange_address: str, user_address: str, type: str = None, from_block: int = 0, ) -> List[BlockchainEvent]: ethindex_db = self.get_ethindex_db_for_exchange(exchange_address) if type is not None: events = ethindex_db.get_user_events( event_type=type, user_address=user_address, from_block=from_block, ) else: events = ethindex_db.get_all_contract_events( user_address=user_address, from_block=from_block, ) return events def get_all_user_exchange_events( self, user_address: str, type: str = None, from_block: int = 0, ) -> List[BlockchainEvent]: assert is_checksum_address(user_address) exchange_ethindex = self.get_ethindex_db_for_exchange() return exchange_ethindex.get_all_exchange_events_of_user( user_address=user_address, all_exchange_addresses=self.exchange_addresses, type=type, from_block=from_block, ) def _load_gas_price_settings(self, gas_price_settings: Dict): method = gas_price_settings["method"] methods = ["fixed", "rpc"] if method not in methods: raise ValueError( f"Given gasprice computation method: {method} must be on of {methods}" ) if method == "fixed": logger.info(f"Set gasprice method to {method}") fixed_gas_price = gas_price_settings.get("gasPrice", 0) if not isinstance(fixed_gas_price, int) or fixed_gas_price < 0: raise ValueError( f"Given gasprice: {fixed_gas_price} must be a non negative integer" ) self.fixed_gas_price = fixed_gas_price def _load_orderbook(self): logger.info("Start exchange orderbook") self.orderbook = OrderBookGreenlet() self.orderbook.connect_db(engine=create_engine()) self.orderbook.start() def _load_addresses(self): addresses = {} with open(self.addresses_json_path) as data_file: content = data_file.read() if content: try: addresses = json.loads(content) except json.decoder.JSONDecodeError as e: logger.error("Could not read addresses.json:" + str(e)) else: logger.warning(f"{self.addresses_json_path} file is empty") network_addresses = addresses.get("networks", []) for address in network_addresses: self.new_network(to_checksum_address(address)) exchange_address = addresses.get("exchange", None) if exchange_address is not None: self.new_exchange(to_checksum_address(exchange_address)) new_unw_eth_address = addresses.get("unwEth", None) if new_unw_eth_address is not None: self.new_unw_eth(to_checksum_address(new_unw_eth_address)) known_factories = addresses.get("identityProxyFactory", []) if type(known_factories) is list: for address in known_factories: self.new_known_factory(address) else: self.new_known_factory(known_factories) self._log_listener.start() def _start_listen_network(self, address): assert is_checksum_address(address) graph = self.currency_network_graphs[address] proxy = self.currency_network_proxies[address] proxy.start_listen_on_full_sync( _create_on_full_sync(graph), self.config["trustline_index"]["full_sync_interval"], ) proxy.start_listen_on_balance(self._process_balance_update, start_log_filter=False) proxy.start_listen_on_trustline(self._process_trustline_update, start_log_filter=False) proxy.start_listen_on_transfer(self._process_transfer, start_log_filter=False) proxy.start_listen_on_trustline_request( self._process_trustline_request, start_log_filter=False) proxy.start_listen_on_trustline_request_cancel( self._process_trustline_request_cancel, start_log_filter=False) proxy.start_listen_on_network_freeze(self._process_network_freeze, start_log_filter=False) def _start_push_service(self): logger.info("Start pushnotification service") path = self.config["push_notification"]["firebase_credentials_path"] app = create_firebase_app_from_path_to_keyfile(path) self._firebase_raw_push_service = FirebaseRawPushService(app) self._client_token_db = ClientTokenDB(engine=create_engine()) logger.info("Firebase pushservice started") self._start_pushnotifications_for_registered_users() def _start_pushnotifications_for_registered_users(self): number_of_new_listeners = 0 for token_mapping in self._client_token_db.get_all_client_tokens(): try: self._start_pushnotifications(token_mapping.user_address, token_mapping.client_token) number_of_new_listeners += 1 except InvalidClientTokenException: # token now invalid, so delete it from database self._client_token_db.delete_client_token( token_mapping.user_address, token_mapping.client_token) pass logger.debug( "Start pushnotifications for {} registered user devices".format( number_of_new_listeners)) def _process_balance_update(self, balance_update_event): graph = self.currency_network_graphs[ balance_update_event.network_address] graph.update_balance( balance_update_event.from_, balance_update_event.to, balance_update_event.value, balance_update_event.timestamp, ) self._publish_trustline_events( user1=balance_update_event.from_, user2=balance_update_event.to, network_address=balance_update_event.network_address, timestamp=balance_update_event.timestamp, ) def _process_transfer(self, transfer_event): self._publish_blockchain_event(transfer_event) def _process_trustline_request(self, trustline_request_event): logger.debug("Process trustline request event: %s", trustline_request_event) self._publish_blockchain_event(trustline_request_event) def _process_trustline_request_cancel(self, trustline_request_cancel_event): logger.debug("Process trustline request cancel event: %s", trustline_request_cancel_event) self._publish_blockchain_event(trustline_request_cancel_event) def _process_network_freeze(self, network_freeze_event): logger.info( f"Currency network frozen: {network_freeze_event.network_address}") self.currency_network_proxies[ network_freeze_event.network_address].is_frozen = True def _generate_trustline_events(self, *, user1, user2, network_address, timestamp): events = [] graph = self.currency_network_graphs[network_address] for (from_, to) in [(user1, user2), (user2, user1)]: events.append( BalanceEvent( network_address, from_, to, graph.get_account_sum(from_, to, timestamp=timestamp), timestamp, )) for user in [user1, user2]: events.append( NetworkBalanceEvent( network_address, user, graph.get_account_sum(user, timestamp=timestamp), timestamp, )) return events def _publish_trustline_events(self, *, user1, user2, network_address, timestamp): events = self._generate_trustline_events( user1=user1, user2=user2, network_address=network_address, timestamp=timestamp, ) for ev in events: self._publish_user_event(ev) def _process_trustline_update(self, trustline_update_event): logger.debug("Process trustline update event: %s", trustline_update_event) graph = self.currency_network_graphs[ trustline_update_event.network_address] graph.update_trustline( creditor=trustline_update_event.from_, debtor=trustline_update_event.to, creditline_given=trustline_update_event.creditline_given, creditline_received=trustline_update_event.creditline_received, interest_rate_given=trustline_update_event.interest_rate_given, interest_rate_received=trustline_update_event. interest_rate_received, ) self._publish_blockchain_event(trustline_update_event) self._publish_trustline_events( user1=trustline_update_event.from_, user2=trustline_update_event.to, network_address=trustline_update_event.network_address, timestamp=trustline_update_event.timestamp, ) def _publish_blockchain_event(self, event): for user in [event.from_, event.to]: event_with_user = deepcopy(event) event_with_user.user = user self._publish_user_event(event_with_user) def _publish_user_event(self, event): assert event.user is not None self.subjects[event.user].publish(event)
class TrustlinesRelay: def __init__(self, config=None, addresses_json_path="addresses.json"): if config is None: config = {} self.config = config self.addresses_json_path = addresses_json_path self.currency_network_proxies: Dict[str, CurrencyNetworkProxy] = {} self.currency_network_graphs: Dict[str, CurrencyNetworkGraph] = {} self.subjects = defaultdict(Subject) self.messaging = defaultdict(MessagingSubject) self.contracts = {} self.node: Node = None self._web3 = None self.orderbook: OrderBookGreenlet = None self.unw_eth_proxies: Dict[str, UnwEthProxy] = {} self.token_proxies: Dict[str, TokenProxy] = {} self._firebase_raw_push_service: Optional[ FirebaseRawPushService] = None self._client_token_db: Optional[ClientTokenDB] = None self.fixed_gas_price: Optional[int] = None self.known_identity_factories: List[str] = [] @property def network_addresses(self) -> Iterable[str]: return self.currency_network_proxies.keys() @property def exchange_addresses(self) -> Iterable[str]: return self.orderbook.exchange_addresses @property def unw_eth_addresses(self) -> Iterable[str]: return list(self.unw_eth_proxies) @property def token_addresses(self) -> Iterable[str]: return list(self.token_proxies) + list(self.unw_eth_addresses) @property def enable_ether_faucet(self) -> bool: return self.config.get("enableEtherFaucet", False) @property def enable_relay_meta_transaction(self) -> bool: return self.config.get("enableRelayMetaTransaction", False) @property def enable_deploy_identity(self) -> bool: return self.config.get("enableDeployIdentity", False) @property def event_query_timeout(self) -> int: return self.config.get("eventQueryTimeout", 20) @property def use_eth_index(self) -> bool: return os.environ.get("ETHINDEX", "1") == "1" def get_event_selector_for_currency_network(self, network_address): """return either a CurrencyNetworkProxy or a EthindexDB instance This is being used from relay.api to query for events. """ if self.use_eth_index: return ethindex_db.EthindexDB( ethindex_db.connect(""), address=network_address, standard_event_types=currency_network_events. standard_event_types, event_builders=currency_network_events.event_builders, from_to_types=currency_network_events.from_to_types, ) else: return self.currency_network_proxies[network_address] def get_event_selector_for_token(self, address): """return either a proxy or a EthindexDB instance This is being used from relay.api to query for events. """ if self.use_eth_index: return ethindex_db.EthindexDB( ethindex_db.connect(""), address=address, standard_event_types=token_events.standard_event_types, event_builders=token_events.event_builders, from_to_types=token_events.from_to_types, ) else: return self.token_proxies[address] def get_event_selector_for_unw_eth(self, address): """return either a proxy or a EthindexDB instance This is being used from relay.api to query for events. """ if self.use_eth_index: return ethindex_db.EthindexDB( ethindex_db.connect(""), address=address, standard_event_types=unw_eth_events.standard_event_types, event_builders=unw_eth_events.event_builders, from_to_types=unw_eth_events.from_to_types, ) else: return self.unw_eth_proxies[address] def get_event_selector_for_exchange(self, address): """return either a proxy or a EthindexDB instance This is being used from relay.api to query for events. """ if self.use_eth_index: return ethindex_db.EthindexDB( ethindex_db.connect(""), address=address, standard_event_types=exchange_events.standard_event_types, event_builders=exchange_events.event_builders, from_to_types=exchange_events.from_to_types, ) else: return self.orderbook._exchange_proxies[address] def is_currency_network(self, address: str) -> bool: return address in self.network_addresses def is_currency_network_frozen(self, address: str) -> bool: return self.currency_network_proxies[address].is_frozen def is_trusted_token(self, address: str) -> bool: return address in self.token_addresses or address in self.unw_eth_addresses def get_network_info(self, network_address: str): return self.currency_network_proxies[network_address] def get_network_infos(self): return [ self.get_network_info(network_address) for network_address in self.network_addresses ] def get_users_of_network(self, network_address: str): return self.currency_network_graphs[network_address].users def deploy_identity(self, factory_address, implementation_address, signature): return self.delegate.deploy_identity(factory_address, implementation_address, signature) def delegate_meta_transaction(self, meta_transaction: MetaTransaction): return self.delegate.send_signed_meta_transaction(meta_transaction) def meta_transaction_fees(self, meta_transaction: MetaTransaction): return self.delegate.calculate_fees_for_meta_transaction( meta_transaction) def get_identity_info(self, identity_address: str): return { "balance": self.node.balance_wei(identity_address), "identity": identity_address, "nextNonce": self.delegate.calc_next_nonce(identity_address), } def start(self): self._load_gas_price_settings( self.config.get("gasPriceComputation", {})) self._load_contracts() self._load_orderbook() self._start_push_service() url = "{}://{}:{}".format( "https" if self.config["rpc"]["ssl"] else "http", self.config["rpc"]["host"], self.config["rpc"]["port"], ) logger.info("using web3 URL {}".format(url)) self._web3 = Web3(Web3.HTTPProvider(url)) self.node = Node(self._web3, fixed_gas_price=self.fixed_gas_price) delegation_fees = [ DelegationFees(value=d["value"], currency_network=d["currencyNetwork"]) for d in self.config.get("delegationFees", []) if d ] self.delegate = Delegate( self._web3, self.node.address, self.contracts["Identity"]["abi"], self.known_identity_factories, delegation_fees=delegation_fees, ) self._start_listen_on_new_addresses() def new_network(self, address: str) -> None: assert is_checksum_address(address) if address in self.network_addresses: return logger.info("New network: {}".format(address)) self.currency_network_proxies[address] = CurrencyNetworkProxy( self._web3, self.contracts["CurrencyNetwork"]["abi"], address) currency_network_proxy = self.currency_network_proxies[address] self.currency_network_graphs[address] = CurrencyNetworkGraph( capacity_imbalance_fee_divisor=currency_network_proxy. capacity_imbalance_fee_divisor, default_interest_rate=currency_network_proxy.default_interest_rate, custom_interests=currency_network_proxy.custom_interests, prevent_mediator_interests=currency_network_proxy. prevent_mediator_interests, ) self._start_listen_network(address) def new_exchange(self, address: str) -> None: assert is_checksum_address(address) if address not in self.exchange_addresses: logger.info("New Exchange contract: {}".format(address)) self.orderbook.add_exchange( ExchangeProxy( self._web3, self.contracts["Exchange"]["abi"], self.contracts["Token"]["abi"], address, self, )) def new_unw_eth(self, address: str) -> None: assert is_checksum_address(address) if address not in self.unw_eth_addresses: logger.info("New Unwrap ETH contract: {}".format(address)) self.unw_eth_proxies[address] = UnwEthProxy( self._web3, self.contracts["UnwEth"]["abi"], address) def new_token(self, address: str) -> None: assert is_checksum_address(address) if address not in self.token_addresses: logger.info("New Token contract: {}".format(address)) self.token_proxies[address] = TokenProxy( self._web3, self.contracts["Token"]["abi"], address) def new_known_factory(self, address: str) -> None: assert is_checksum_address(address) if address not in self.known_identity_factories: logger.info("New identity factory contract: {}".format(address)) self.known_identity_factories.append(address) def get_networks_of_user(self, user_address: str) -> List[str]: assert is_checksum_address(user_address) networks_of_user: List[str] = [] for network_address in self.network_addresses: if user_address in self.currency_network_graphs[ network_address].users: networks_of_user.append(network_address) return networks_of_user def add_push_client_token(self, user_address: str, client_token: str) -> None: if self._firebase_raw_push_service is not None: self._start_pushnotifications(user_address, client_token) try: if self._client_token_db is not None: self._client_token_db.add_client_token( user_address, client_token) except ClientTokenAlreadyExistsException: pass # all good def delete_push_client_token(self, user_address: str, client_token: str) -> None: if self._firebase_raw_push_service is not None: if self._client_token_db is not None: self._client_token_db.delete_client_token( user_address, client_token) self._stop_pushnotifications(user_address, client_token) def _start_pushnotifications(self, user_address: str, client_token: str) -> None: assert self._firebase_raw_push_service is not None if not self._firebase_raw_push_service.check_client_token( client_token): raise InvalidClientTokenException for subscription in self.subjects[user_address].subscriptions: if (isinstance(subscription.client, PushNotificationClient) and subscription.client.client_token == client_token): return # Token already registered logger.debug("Add client token {} for address {}".format( client_token, user_address)) client = PushNotificationClient(self._firebase_raw_push_service, client_token) self.subjects[user_address].subscribe(client) self.messaging[user_address].subscribe(client) def _stop_pushnotifications(self, user_address: str, client_token: str) -> None: success = False subscriptions = self.subjects[user_address].subscriptions # Copy the list because we will delete items while iterating over it for subscription in subscriptions[:]: if (isinstance(subscription.client, PushNotificationClient) and subscription.client.client_token == client_token): logger.debug("Remove client token {} for address {}".format( client_token, user_address)) subscription.client.close() success = True if not success: raise TokenNotFoundException def get_user_network_events( self, network_address: str, user_address: str, type: str = None, from_block: int = 0, ) -> List[BlockchainEvent]: proxy = self.get_event_selector_for_currency_network(network_address) if type is not None: events = proxy.get_network_events( type, user_address, from_block=from_block, timeout=self.event_query_timeout, ) else: events = proxy.get_all_network_events( user_address, from_block=from_block, timeout=self.event_query_timeout) return events def get_network_events(self, network_address: str, type: str = None, from_block: int = 0) -> List[BlockchainEvent]: proxy = self.get_event_selector_for_currency_network(network_address) if type is not None: events = proxy.get_events(type, from_block=from_block, timeout=self.event_query_timeout) else: events = proxy.get_all_events(from_block=from_block, timeout=self.event_query_timeout) return events def get_user_events( self, user_address: str, type: str = None, from_block: int = 0, timeout: float = None, ) -> List[BlockchainEvent]: assert is_checksum_address(user_address) network_event_queries = self._get_network_event_queries( user_address, type, from_block) unw_eth_event_queries = self._get_unw_eth_event_queries( user_address, type, from_block) exchange_event_queries = self._get_exchange_event_queries( user_address, type, from_block) results = concurrency_utils.joinall( network_event_queries + unw_eth_event_queries + exchange_event_queries, timeout=timeout, ) return sorted_events(list(itertools.chain.from_iterable(results))) def _get_network_event_queries(self, user_address: str, type: str = None, from_block: int = 0): assert is_checksum_address(user_address) queries = [] for network_address in self.network_addresses: currency_network_proxy = self.get_event_selector_for_currency_network( network_address) if type is not None and type in currency_network_proxy.event_types: queries.append( functools.partial( currency_network_proxy.get_network_events, type, user_address=user_address, from_block=from_block, )) else: queries.append( functools.partial( currency_network_proxy.get_all_network_events, user_address=user_address, from_block=from_block, )) return queries def _get_unw_eth_event_queries(self, user_address: str, type: str = None, from_block: int = 0): assert is_checksum_address(user_address) queries = [] for unw_eth_address in self.unw_eth_addresses: unw_eth_proxy = self.get_event_selector_for_unw_eth( unw_eth_address) if type is not None and type in unw_eth_proxy.event_types: queries.append( functools.partial( unw_eth_proxy.get_unw_eth_events, type, user_address=user_address, from_block=from_block, )) else: queries.append( functools.partial( unw_eth_proxy.get_all_unw_eth_events, user_address=user_address, from_block=from_block, )) return queries def _get_exchange_event_queries(self, user_address: str, type: str = None, from_block: int = 0): assert is_checksum_address(user_address) queries = [] for exchange_address in self.exchange_addresses: exchange_proxy = self.get_event_selector_for_exchange( exchange_address) if type is not None and type in exchange_proxy.standard_event_types: queries.append( functools.partial( exchange_proxy.get_exchange_events, type, user_address=user_address, from_block=from_block, )) else: queries.append( functools.partial( exchange_proxy.get_all_exchange_events, user_address=user_address, from_block=from_block, )) return queries def get_user_token_events( self, token_address: str, user_address: str, type: str = None, from_block: int = 0, ) -> List[BlockchainEvent]: if token_address in self.unw_eth_addresses: proxy: Union[UnwEthProxy, TokenProxy] = self.get_event_selector_for_unw_eth( token_address) func_names = ["get_unw_eth_events", "get_all_unw_eth_events"] else: proxy = self.get_event_selector_for_token(token_address) func_names = ["get_token_events", "get_all_token_events"] if type is not None: events = getattr(proxy, func_names[0])(type, user_address, from_block=from_block) else: events = getattr(proxy, func_names[1])(user_address, from_block=from_block) return events def get_token_events(self, token_address: str, type: str = None, from_block: int = 0) -> List[BlockchainEvent]: if token_address in self.unw_eth_addresses: proxy: Union[UnwEthProxy, TokenProxy] = self.get_event_selector_for_unw_eth( token_address) else: proxy = self.get_event_selector_for_token(token_address) if type is not None: events = proxy.get_events(type, from_block=from_block) else: events = proxy.get_all_events(from_block=from_block) return events def get_exchange_events(self, exchange_address: str, type: str = None, from_block: int = 0) -> List[BlockchainEvent]: proxy = self.get_event_selector_for_exchange(exchange_address) if type is not None: events = proxy.get_events(type, from_block=from_block) else: events = proxy.get_all_events(from_block=from_block) return events def get_user_exchange_events( self, exchange_address: str, user_address: str, type: str = None, from_block: int = 0, ) -> List[BlockchainEvent]: proxy = self.get_event_selector_for_exchange(exchange_address) if type is not None: events = proxy.get_exchange_events( type, user_address, from_block=from_block, timeout=self.event_query_timeout, ) else: events = proxy.get_all_exchange_events( user_address, from_block=from_block, timeout=self.event_query_timeout) return events def get_all_user_exchange_events( self, user_address: str, type: str = None, from_block: int = 0, timeout: float = None, ) -> List[BlockchainEvent]: assert is_checksum_address(user_address) exchange_event_queries = self._get_exchange_event_queries( user_address, type, from_block) results = concurrency_utils.joinall(exchange_event_queries, timeout=timeout) return sorted_events(list(itertools.chain.from_iterable(results))) def _load_gas_price_settings(self, gas_price_settings: Dict): method = gas_price_settings.get("method", "rpc") methods = ["fixed", "rpc"] if method not in methods: raise ValueError( f"Given gasprice computation method: {method} must be on of {methods}" ) if method == "fixed": fixed_gas_price = gas_price_settings.get("gasPrice", 0) if not isinstance(fixed_gas_price, int) or fixed_gas_price < 0: raise ValueError( f"Given gasprice: {fixed_gas_price} must be a non negative integer" ) self.fixed_gas_price = fixed_gas_price def _load_contracts(self): with open( os.path.join(sys.prefix, "trustlines-contracts", "build", "contracts.json")) as data_file: self.contracts = json.load(data_file) def _load_orderbook(self): self.orderbook = OrderBookGreenlet() self.orderbook.connect_db(engine=create_engine()) self.orderbook.start() def _load_addresses(self): addresses = {} with open(self.addresses_json_path) as data_file: content = data_file.read() if content: try: addresses = json.loads(content) except json.decoder.JSONDecodeError as e: logger.error("Could not read addresses.json:" + str(e)) else: logger.warning("addresses.json file is empty") network_addresses = addresses.get("networks", []) for address in network_addresses: self.new_network(to_checksum_address(address)) exchange_address = addresses.get("exchange", None) if exchange_address is not None: self.new_exchange(to_checksum_address(exchange_address)) new_unw_eth_address = addresses.get("unwEth", None) if new_unw_eth_address is not None: self.new_unw_eth(to_checksum_address(new_unw_eth_address)) known_factories = addresses.get("identityProxyFactory", []) if type(known_factories) is list: for address in known_factories: self.new_known_factory(address) else: self.new_known_factory(known_factories) def _start_listen_network(self, address): assert is_checksum_address(address) graph = self.currency_network_graphs[address] proxy = self.currency_network_proxies[address] proxy.start_listen_on_full_sync(_create_on_full_sync(graph), self.config.get("syncInterval", 300)) proxy.start_listen_on_balance(self._process_balance_update) proxy.start_listen_on_trustline(self._process_trustline_update) proxy.start_listen_on_transfer(self._process_transfer) proxy.start_listen_on_trustline_request( self._process_trustline_request) proxy.start_listen_on_network_freeze(self._process_network_freeze) def _start_listen_on_new_addresses(self): def listen(): while True: try: self._load_addresses() except Exception: logger.critical("Error while loading addresses", exc_info=sys.exc_info()) sleep(self.config.get("updateNetworksInterval", 120)) gevent.Greenlet.spawn(listen) def _start_push_service(self): path = self.config.get("firebase", {}).get("credentialsPath", None) if path is not None: self._firebase_raw_push_service = FirebaseRawPushService(path) self._client_token_db = ClientTokenDB(engine=create_engine()) logger.info("Firebase pushservice started") self._start_pushnotifications_for_registered_users() else: logger.info( "No firebase credentials in config, pushservice disabled") def _start_pushnotifications_for_registered_users(self): number_of_new_listeners = 0 for token_mapping in self._client_token_db.get_all_client_tokens(): try: self._start_pushnotifications(token_mapping.user_address, token_mapping.client_token) number_of_new_listeners += 1 except InvalidClientTokenException: # token now invalid, so delete it from database self._client_token_db.delete_client_token( token_mapping.user_address, token_mapping.client_token) pass logger.debug( "Start pushnotifications for {} registered user devices".format( number_of_new_listeners)) def _process_balance_update(self, balance_update_event): graph = self.currency_network_graphs[ balance_update_event.network_address] graph.update_balance( balance_update_event.from_, balance_update_event.to, balance_update_event.value, balance_update_event.timestamp, ) self._publish_trustline_events( user1=balance_update_event.from_, user2=balance_update_event.to, network_address=balance_update_event.network_address, timestamp=balance_update_event.timestamp, ) def _process_transfer(self, transfer_event): self._publish_blockchain_event(transfer_event) def _process_trustline_request(self, trustline_request_event): logger.debug("Process trustline request event") self._publish_blockchain_event(trustline_request_event) def _process_network_freeze(self, network_freeze_event): logger.info( f"Currency network frozen: {network_freeze_event.network_address}") self.currency_network_proxies[ network_freeze_event.network_address].is_frozen = True def _generate_trustline_events(self, *, user1, user2, network_address, timestamp): events = [] graph = self.currency_network_graphs[network_address] for (from_, to) in [(user1, user2), (user2, user1)]: events.append( BalanceEvent( network_address, from_, to, graph.get_account_sum(from_, to, timestamp=timestamp), timestamp, )) for user in [user1, user2]: events.append( NetworkBalanceEvent( network_address, user, graph.get_account_sum(user, timestamp=timestamp), timestamp, )) return events def _publish_trustline_events(self, *, user1, user2, network_address, timestamp): events = self._generate_trustline_events( user1=user1, user2=user2, network_address=network_address, timestamp=timestamp, ) for ev in events: self._publish_user_event(ev) def _process_trustline_update(self, trustline_update_event): logger.debug("Process trustline update event") graph = self.currency_network_graphs[ trustline_update_event.network_address] graph.update_trustline( creditor=trustline_update_event.from_, debtor=trustline_update_event.to, creditline_given=trustline_update_event.creditline_given, creditline_received=trustline_update_event.creditline_received, interest_rate_given=trustline_update_event.interest_rate_given, interest_rate_received=trustline_update_event. interest_rate_received, ) self._publish_blockchain_event(trustline_update_event) self._publish_trustline_events( user1=trustline_update_event.from_, user2=trustline_update_event.to, network_address=trustline_update_event.network_address, timestamp=trustline_update_event.timestamp, ) def _publish_blockchain_event(self, event): for user in [event.from_, event.to]: event_with_user = deepcopy(event) event_with_user.user = user self._publish_user_event(event_with_user) def _publish_user_event(self, event): assert event.user is not None self.subjects[event.user].publish(event)
def test_get_client_tokens(client_token_db: ClientTokenDB): client_token_db.add_client_token("0x123", "token1") client_token_db.add_client_token("0x124", "token2") client_token_db.add_client_token("0x125", "token3") assert client_token_db.get_client_tokens("0x124") == ["token2"]
def test_delete_non_existent_token(client_token_db: ClientTokenDB): client_token_db.delete_client_token("0x123", "token") assert client_token_db.get_client_tokens("0x123") == []
def test_cannot_add_same_token_twice(client_token_db: ClientTokenDB): client_token_db.add_client_token("0x123", "token") with pytest.raises(ClientTokenAlreadyExistsException): client_token_db.add_client_token("0x123", "token") assert client_token_db.get_client_tokens("0x123") == ["token"]
def test_add_client_token(client_token_db: ClientTokenDB): client_token_db.add_client_token("0x123", "token") assert client_token_db.get_client_tokens("0x123") == ["token"]
def client_token_db(): return ClientTokenDB(create_engine("sqlite:///:memory:"))